2005 г.

Использование bash и утилит для обработки текста

Сергей Майков aka Madskull
unix.ginras.ru

Дана задача - изменить во всех html-файлах какие-либо атрибуты.

Например, надо изменить во всех тэгах <body> фоновый цвет на черный, цвет букв - на белый. Для определенности предположим, что нам не важно, какой цвет был раньше. При этом следует либо заменить значения атрибутов bgcolor и text, либо добавить их, но не "попортить" другие возможные атрибуты.

Начнем с наиболее простого случая - единичной замены с помощью sed, а потом уже завернем все это в find для замены множественной.

Мне кажется, что проще сперва удалить наши атрибуты цвета (если они есть), а потом уже добавить наши новые значения:

cat test.html | sed -e ' 
# (0)
s/\(<body[^>]*\) bgcolor=[^ >]*/\1/i;
# (1) убираем атрибут bgcolor
s/\(<body[^>]*\) text=[^ >]*/\1/i;
# (2) убираем атрибут text
s/\(<body\)/\1 bgcolor="#000000" text="  #ffffff"/i
# (3) вставляем новые значения атрибутов

Попробуем понять, что же мы тут понаписали. Для начала (строка (0)) выводим содержимое обрабатываемого файла командой cat и передаем его по конвейеру утилите sed. Далее...

...Строка (1):

  • s/ - начинаем команду поиска и замены. Команда имеет вид s/SEARCH/REPLACE/FLAGS, где:
    SEARCH - то, что ищем;
    REPLACE - то, на что заменяем;
    FLAGS - флаги режима поиска.
    Самые распространенные флаги, это i - регистронезависимый поиск и g - искать во всем тексте, а не только первое совпадение. Вместо симолов / в качестве разделителя могут выступать любые другие символы.
  • \(<body[^>]*\) - ищем тэг body и засылаем его и все что следует за ним до нужного атрибута (слово bgcolor) во внутреннюю переменную 1. Для этого мы используем скобки. Здесь мы применяем регулярное выражение [^>]* (ноль или больше любых символов, кроме >) вместо .* (ноль или больше любых символов) для того, чтобы наша звездочка (.*) не "сожрала" весь текст до последнего вхождения ограничивающего атрибута bgcolor в одной строке. Это случится, например, если после тэга body будет идти тэг html с атрибутом bgcolor. А применяя [^>] мы ограничиваем его аппетит первым же символом >, то есть концом тэга body. Заключение в скобки позволяет нам запомнить найденный текст (то есть то, что удалять не надо).
  • bgcolor=[^ >]* - ищем слово bgcolor= и любое количество любых символов, кроме пробела и >, после него. То есть, как раз то, что нам надо удалить.
  • /\1/ - это часть REPLACE, в которой мы подставляем сохраненный с помощью скобок текст. i - это упоминаемый выше флаг игнорирования регистра.

Строка (2) - идентична первой.

Строка (3) - Ищется начало тэга body и подставляются наши атрибуты.

Собственно, непосредственную замену мы разобрали и переходим к обработке множества файлов. Тут позволю себе небольшое лирическое отступление.

Относительно недавно в sed появилась очень приятная опция -i. Она позволяет производить изменения непосредственно в файле. Если раньше для изменения файла приходилось делать что-то вроде:

$ sed -e 'что-то делаем' test.txt > test.txt.tmp ;\
mv -f test.txt.tmp test.txt

То теперь достаточно

$ sed -i -e 'что-то делаем' test.txt

Чтобы узнать, поддерживается ли опция -i, достаточно выполнить sed --help.

Следовательно, если наш sed не поддерживает опции -i, то придется разбивать команду на две и это мешает нам использовать команду find с опцией действия -exec. Поэтому наш скрипт разбивается на два варианта: 1) с использованием find -exec и 2) использование find с циклом for.

Обработка всех файлов в каталоге и подкаталогах. Вариант 1.

Тут все просто. Используем возможность find задавать действие для каждого найденного файла и получаем окончательный вариант нашего скрипта:

$ find -name '*.html' -exec sed -i -e
's/\(<body[^>]*\) bgcolor=[^ >]*/\1/i;
s/\(<body[^>]*\) text=[^ >]*/\1/i;
s/\(<body\)/\1 bgcolor="#000000"
text="#ffffff"/i' "{}" \;

Тут отмечаем, что имя файла передается в виде {}, которые мы заключили в двойные кавычки на тот случай, если в имени файла содержатся пробелы. Конец команды -exec - это ;, "заэкранированная" от bash'а обратным слэшем (подробности об использовании команды find - здесь).

Обработка всех файлов в каталоге и подкаталогах. Вариант 2.

Если у нас древний sed, то придется разбивать действие с файлом на две команды, как показано выше. Из-за этого мы не можем воспользоваться командой find в сочетании с -exec. Чтож, это не беда, будем использовать возможности bash'а, в частности его циклы.

Первое, что приходит в голову, это использовать что-то вроде:

for i in `find -name '*.html'`; do
	sed -i -e 's/\(<body[^>]*\) bgcolor=[^ >]*/\1/i;
	s/\(<body[^>]*\) text=[^ >]*/\1/i; s/\(<body\)/\1
	bgcolor="#000000" text="#ffffff"/i' "$i" > $i.tmp
	mv -f $i.tmp $i
done

Все замечательно, но если у нас есть файлы с пробелами в имени, то мы получаем кучу сообщений о том, что файлы не найдены. Так как при подстановке списка файлов от find ... bash обрабатывает его как список слов, разделенных пробелами (точнее, символами, указанными в переменной $IFS), то вместо файла Name with spaces.html, мы получаем три "псевдоимени" файлов; Name, with и spaces.html (от редактора: лишний стимул не следовать порочной практике бездумного формирования имен файлов из первой фразы текстового документа, принятой... ну сами знаете где - А.Ф.).

Поэтому я предлагаю воспользоваться следующей конструкцией:

find -name '*.html' | while read i; do
	sed -i -e 's/\(<body[^>]*\) bgcolor=[^ >]*/\1/i;
	s/\(<body[^>]*\) text=[^ >]*/\1/i; s/\(<body\)/\1
	bgcolor="#000000" text="#ffffff"/i' "$i" > "$i.tmp"
	mv -f "$i.tmp" "$i"
done

Здесь read читает строки в переменную i и мы гарантированно получаем полное имя файла.

Другие примеры применения редактора sed, а также памятку по его командам, можно найти здесь.

Новости IT
8 мая 2026
Релиз Chrome 148

Связь с редакцией