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 обязательна.