Open Way | Systems | Distro | Shell | Desktop | Applications
Network | Development | Download | OfftopicКарта сайта
FreeNotesФорум POSIX.ru
На главную страницу

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

Сергей Майков aka Madskull
2005, весна

От редактора: Этим начинается цикл заметок, который можно озаглавить примерно как "Готовые решения по shell-скриптингу" - Алексей Федорчук.

Дана задача - изменить во всех 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):

Строка (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. Поэтому наш скрипт разбивается на два варианта: с использованием find -exec и использование 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'а обратным слэшем .

Обработка всех файлов в каталоге и подкаталогах. Вариант 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 и мы гарантированно получаем полное имя файла.


Вы знаете, что пух сваливается - пуховое одеяло. Ищете одеяло? У нас их 118 видов. . Переезд 180 руб/час: переезд. Хотите поднять ванную на 5 этаж? . Ищете одежду недорого? Жмите - футболки. Футболки, майки камуфляж в Москве. . Ремонт грузовиков, н/час от 550 р - грузовой ремонт. Ремонт всех грузовых автомобилей.