U1 Слово Лѣтопись Имперія Вѣда NX ТЕ  

NX

       

SED и AWK


16 янв 2015 


На этот раз я намерен рассмотреть два малых языка, популярных в Линуксе — 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 обязательна.