Сергей Майков 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, а также памятку по его командам, можно найти здесь.