SED и AWK
На этот раз я намерен рассмотреть два малых языка, популярных в Линуксе — sed и awk. Оба широко применяются в системном администрировании и очень в этом помогают, если вы конечно же, понимаете код, даже не написав ни строчки. Посмотрим же на sed.
Является ли sed языком?
Можно было бы согласиться с тем, что набор команд sed не является языком программирования. На самом деле, замечательный скрипт Кристофера Блесса подтверждает, что sed полный по Тьюрингу, а это значит теоретическое соответствие другим языкам программирования. Джулия Джоманте даже написала игру Тетрис на sed. Но, конечно же, никто в ближайшее время не напишет ядро Линукса на нём.
Для начала — sed является потоковым редактором и работает как классический фильтр, принимая на вход обрабатываемый файл и передавая обработанные данные следующей команде в потоке. sed читает из входного файла или из стандартного ввода (stdin) по одной строке, выполняет предписанное действие и передаёт итог на стандартный вывод (stdout). Затем читает следующую строку и т.д. В отличие от обычного текстового редактора, принимающего весь файл в буфер, sed помещает в него только одну строку, результативно обрабатывая огромные файлы.
Замещение с помощью sed
Начнём с обыденного для администратора примера выполнения замены. Предположим, что мы переместили домашние каталоги из /home в /users и нам нужно заменить в /etc/passwd все строки вида
chris:x:501:501::/home/chris:/bin/bash
на
chris:x:501:501::/users/chris:/bin/bash
Это можно сделать такой командой:
sed s/home/users/ /etc/passwd
Здесь sed читает файл passwd по одной строке, производит замену и выводит результат в stdout. Он не изменяет оригинального файла, а если же его нужно изменить, то выполняем:
sed s/home/users/ /etc/passwd > /etc/passwd
Но здесь скрывается подвох — шелл, обнаружив перенаправление вывода, обрежет выходной файл до нулевой длины, прежде чем sed увидит его, и прощай файл паролей! Это обычное дело для фильтров — нельзя перенаправить входной файл на себя же. Вместо этого нужно делать так:
sed s/home/users/ /etc/passwd > /tmp/passwd
 mv /tmp/passwd /etc/passwd'
На самом деле, в GNU версии sed есть ключ -i, позволяющий это, и команда:
sed -i s/home/users/ /etc/passwd
сделает все правильно, но, берегитесь трогать файл passwd, если не уверены в том, что ваша версия sed делает так, как задумано.
Следующий пример даже проще, команда df выводит таблицу использования дисков для всех файловых систем в системе, но также и заголовок, мешающий в потоковой обработке. Его можно убрать командой
df | sed 1d
Здесь sed читает входной поток, являющийся выходным для df. Команда d означает — удалить строку, а 1 — только строку 1. Следовательно, первая строка вырезается, а всё остальное отдаётся без изменений. Это соответствует tail -n +2.
Посмотрим опять на команду s (substitute/заменить). Предположим, что нам нужно получить имена пользователей из файла /etc/passwd. Видно, что имя находится в первой колонке. Нетрудно обнаружить, что часть старого шаблона замены — это регулярное выражение
sed s/:/*// /etc/passwd
Этот очень хитрый пример, здесь старый шаблон, это regex ':.*' означающий, от первой колонки, до конца строки. Здесь мы полагаемся на жадность регекспа — соответствие начинается как можно раньше и продолжается насколько возможно. Новый шаблон пуст, поэтому всё, что регексп найдёт, удаляется. Просто волшебство!
Ещё пример на замену: требуется заменить строки "$25" на 25 USD. Это немного сложнее, так как GBP должно стоять после числа.
sed -r 's/$([0-9]*)/\1 USD/g' prices
что изменит строку
fees range from $25 to $40 typically
на
fees range from 25 USD to 40 USD typically
Синтаксис sed усложняется очень быстро. Разберём этот пример обратной замены. Ключ -r включает расширенный режим, '$([0-9]*)' — это старый шаблон, где [0-9]* отмеченная часть регекспа соответствующая любой последовательности чисел. \1 USD — это новый шаблон, где \1, обратная замена, вставляет соответствующую отмеченную часть регулярного выражения. команда g производит замену всех соответствий строки.
Сложно? Но в административных скриптах можно найти и более замечательные примеры команды sed. Например, в файле /etc/init/rc-sysinit/conf в Убунте́ можно увидеть:
sed -nre 's/^[^#][^:]*:(0-6sS]):initdefault:.*/DEFAULT_RUNLEVEL="\1|;/p' /etc/inittab
эта команда просто извлекает default run level из файла inittab.
Обычно для разделения частей в командах замены используются прямые слеши, и, если сами шаблоны содержат их, это приводит к очень навороченным строкам, например:
sed 's/\/home\/chris\/bin/\/opt\/bin/' foo.txt
В этом случае можно использовать другой разделитель, например ':'. Так и выглядит получше:
sed 's:/home/chris/bin"/opt/bin:' foo.txt
Выбор строк
Для редактирования можно выбирать одну строку или интервал строк. Ранее мы видели команду 1d для выбора 1-й строки. Для удаления интервала строк, например, с 1-й по 10-ю, даём команду 1,10d или 5,$d для удаления с 5-й строки до конца файла. Также можно выбирать строки с помощью регулярный выражений.
Такая команда
sed '/^#/d' /etc/fstab
удалит строки, начинающиеся с символа #, обычно так отмечаются комментарии. Это как бы обратный grep (печатает несовпадающие строки). Для получения обычного поведения grep, нужно, во-первых, добавить ключ -n выключающий автоматическую печать строк, во-вторых, недвусмысленно сказать ему печатать нужные строки /p:
sed -n '/^#/p' /etc/fstab
Заметьте одинарные кавычки в команде, для предотвращения неоднозначностей в командной строке Линукс.
Более интересный пример: имеется шелл-скрипт с множеством определений функций, разбросанных по нему, и нужно извлечь их в отдельный файл.
#!/bin/bash
 echo привет
 function foo(){
 echo это первая
 }
 # вызов первой функции
 foo
 function bar(){
 echo это вторая
 }
 # вызов второй функции
 bar
Для начала, сделаем скрипт с вырезанными определениями функций:
sed '/^function/,/^}/d' demo.sh > demo2.sh
Здесь мы определили интервал номеров строк на основе соответствия регулярному выражению. Текст между строкой с началом функции и до } удаляется, и если есть несколько таких блоков, все они удалятся. Далее, остаётся только извлечь их в нужный файл:
sed -n '/^function/,/^}/d' demo.sh > funcs.sh
Грепом так не получится!
Шаблоны и пространство удержания
Даже несколькими простыми командами, вместе с хитрым использованием регулярных выражений, мы смогли сделать очень много и это не предел возможностей sed. Но во всех наших примерах выходные строки идут в том же порядке, что и входные. Изменить порядок строк в файле не получится. Для этого нужно понять что такое пространство шаблонов и пространство удержания. Пространство шаблонов — это текстовый буфер, использующийся в нормальном, строка за строкой, редактировании. Команда замены, например, работает в нём и команда p выводит его содержимое на печать.
Пространство удержания — это буфер, где задерживается текст, для, например, изменения порядка следования строк. Три основные команды h, H и x переносят текст в и из него (есть и другие, см. man sed).
Для использования пространства удержания необходимо выполнения двух или более команд sed за один вызов, и вот как это можно сделать. Первый способ, задать ключ -e в командной строке:
sed -e 's/linux/windows/' -e 's/good/bad/' somefile.txt
здесь производятся обе замены в каждой строке. Второй способ, разделить команды точкой с запятой:
sed -e 's/linux/windows/;s/good/bad/' somefile.txt
Это всё хорошо, если команд мало, но если их становится всё больше, то лучше записать их в файл и ссылаться на него в командной строке. Например, в файле script.sed есть такие строки:
s/linux/windows/
s/good/bad/
Теперь можно вызвать sed так:
sed -f script.sed somefile.txt
Преимущества такого способа в том, что не нужно брать команды в кавычки, потому что шеллу уже не нужно их интерпретировать и готовый скрипт можно использовать повторно.
Учитывая всё это, вернёмся к нашему скрипту и переместим определения функция в начало файла, с остатком скрипта внизу:
# sed-скрипт для перемещения функций в шелл-скрипте
 /^function/,/^}/!H
 /^function/,/^}/!p
 $ { x; p }
Пример смещения функций
Здесь необходимы некоторые пояснения. Первая строка скрипта содержит ту же пару регулярных выражений для поиска тела функции, что и прежде, с добавлением знака ! — реверсирования значения. Команда H добавляет пространство шаблонов к пространству удержания, так что выстраивает в буфере удержания все строки, находящиеся вне определений функций. Вторая строка скрипта печатает те строки, что содержат определения функций, так что они выходят первыми, как и требовалось. И, наконец, последняя строка, используя сокращение $ от номера строк, означающее последнюю строку входа, меняет местами пространство удержания и пространство шаблонов и печатает их.
Проверим, что получилось:
$ sed -n -f splitout.sed demoscript.sh
 function foo(){
 echo это первая
 }
 function bar(){
 echo это вторая
 }
 #!/bin/bash
 echo привет
 # вызов первой функции
 foo
 # вызов второй функции
 bar
Почти правильно, за исключением того, что строка #!/bin/bash должна быть первой. Это не трудно исправить, но, оставлю вам для упражнения!
sed на практике
Если вы считаете sed слишком непонятным, не заслуживающим внимания, то вот вам статистика: я посчитал количество использований sed в системных скриптах Убунты́ с помощью самого же sed-а:
find /etc -type f -exec grep -w sed {}\;2> /dev/null | wc -l
Получилось 259 примеров.
В большинстве примеров sed используется в командах замещения для установки значения переменной из содержимого файла конфигурации, наподобие этого:
pid=$(sed 's/ //g' /var/spool/postfix/pid/master.pid)
Во всех этих примерах просто удаляются пробелы из входного потока. Ключ g на конце замещения говорит sed-у сделать изменения глобально — везде в этой определённой строке.
Другой обычный пример использования sed-а — взять значение какой-либо переменной и изменить её определённым образом. Пример из /etc/network/if-pre-up.d/vlan Убунты́:
VLANID=`echo $IFACE | sed "s/vlan0*//"`
Обратите внимание на другую форму записи подстановки команд.
Вот другой пример, где совместно работают awk и sed:
arch=`echo "$line" | awk '{print $4}' | sed 's/:$//'`
Здесь awk выбирает четвёртую строку из $line, а sed удаляет двоеточия. И, наконец, шедевр из /etc/bash_completion.d/sysv-rc:
valid_options=( $( \
 tr " " "\n" <<<"${COMP_WORDS[@]} ${options[@]}" \
 | sed -ne "/$( sed "s/ /\\\\|/g" <<<"${options[@]}" )/p" \
 | sort | uniq -u \
 ) )
Этот впечатляющий кусок скрипта использует sed для подстановки команд генерации команды для внешней команды sed-а. Подумать только!..
С моей стороны было нечестно приводить этот пример вне контекста. Здесь неизвестна структура входных параметров, поэтому трудно сказать, что здесь происходит. По-моему, самое главное в понимании всех этих воображаемых потоков, заключается в точном понимании структуры данных, обрабатываемых на каждом этапе потока.
В следующий раз я расскажу о другом моём любимом малом языке — awk. До встречи.
Желаете узнать больше?
Официальное руководство sed-а находится по [этому адресу]. Здесь вы найдёте не только подробный справочник по командам, но и всякие мозголомные примеры скриптов для эмуляции таких команд, как wc, cat, head, tail и uniq. Здесь даже есть скрипт для вычитания чисел, доказывающий, что с помощью sed-а можно выполнять арифметические выражения (если, конечно, захотите). Также главу sed из [Unix Power Tools]..
Перевод: [Админ].
При перепечатке ссылка на unixone.ru обязательна.