Просто о sed

:

Почему-то поголовно все руководства по sed, которые мне попадались на глаза, являлись простым переводом соответствующей страницы MAN руководства. Причём некоторые предложения я вообще не мог понять, а они так и продолжали кочевать из одного руководства к другому.

Поэтому я решил написать своё руководство, не такое полное, зато более понятное.

1. Что такое sed, и зачем его едят
Подробностей я здесь разговаривать не буду. В кратце, sed (streaming editor), позволяет обрабатывать огромные объёмы текста, если нужно выполнить над ним шаблонные действия. То есть, заменить то на сё, склеить строки там и сям, удалить то и это.
Как работает sed? Утилита берёт строку текста и пробует к ней применить скрипт редактирования. Затем следующую строку, следующую, и так пока не достигнет конца текста.

2. Командная строка sed
Тут только необходимые основы. Подробности в MAN.
Запускаем sed так:
sed [опции] [имя файла(ов)]
Если имя файла не задано, то читается стандартный вход. Результат подаётся на стандартный выход.
Опции:
-e 'скрипт' - задаёт скрипт обработки данных в виде строки. Скрипт можно написать на нескольких строках!
-f файл-со-скриптом - задаёт файл со скриптом.
-r - использовать расширенные регулярные выражения (более удобно на мой взгляд)
-n - не выводить текст пока мы явно об этом не попросим в скрипте.
Вобщем всё просто.

3. Области редактирования и удержания
sed имеет 2 области (буфера), которые могут содержать текст:

  • Область редактирования (pattern space), куда помещается очередная строка. Именно над этой областью производится редактирование.
  • Область удержания (hold space) - буфер куда можно положить промежуточный результат. Что-то вроде регистра калькулятора :)
4. Синтаксис команд редактирования
Команды редактирования отделяются друг от друго символом ';' или разносятся на разные строки. Однако после некоторых команд, ';' игнорируется (такие команды как a, c, i)

Синтаксис команды довольно прост:
<условие применения> <команда> <аргумент команды>
Не все команды содержат по 3 составляющие. Единственным пожалуй обязательным элементом является сама команда. Между всеми тремя составляющими можно вставлять пробелы, а можно и не вставлять - как кому нравится.

Условие применения
Условие применения, это некоторое условие, которое указывает, нужно ли применять команду к текущей области редактирования (обычно это очередная строка из файла).
Для людей, знакомых с функциональным программированием, очевидно, что <условие применения> есть ни что иное, как pattern matching
Остальные могут представить команду, как
if(pattern_space like <условие применения> ) then <команда> <аргумент команды>
То есть, если область редактирования удовлетворяет условию применения, то выполнить команду.

Есть несколько видов условий, наиболее простые это:

  • Номер строки. Применить команду только к определённой строке ($ - к последней строке). Примеры:
    1 - применить к первой строке
    10 - применить к 10-ой
  • Регулярное выражение. Применить команду только если текущая область редактирования совпадает с регулярным выражением. Примеры:
    /^Hello/ - применить к строкам, начинающимся с Hello
Условие может быть диапазоном строк и состоять из 2-х элементарных условий. Первое указывает начало а второе - конец диапазона. Например:
1,10 - строки с первой по 10-ую.
/^A/,20 - строки с строки начинающейся с A по 20-ую строку.
2,+10 - 10 строк, начиная со второй строки.
Однако нужно помнить, что не все команды принимают диапазоны.

Символ ! после условия, инвертирует его.
5,10 ! - обрабатывать всё, кроме строк с 5-ой по 10-ую

Остальные виды адресации можно посмотреть в MAN

Группировка команд
Под одно условие может попадать несколько команд. В таком случае необходимо использовать группирующие скобки - {}
1 {
<здесь несколько строк с командами>
...
}

Команды редактирования данных
Даю наиболее ценные, с моей точки зрения команды. (Остальные можно посмотреть.... да, в MAN)
s/регулярное выражение/замена/ - заменить текст в области редактирования, подпадающий под регулярное выражение на текст замены. Можно использовать \1 ... \9 для ссылки на группы в регулярном выражении. Диалект выражений зависит от опции -r
p - вывести текущее содержимое области удержания. Если не использовать опцию командной строки -n, то эта команда автоматически выполнится в конце скрипта.
b label - перейти к метке label. Если метка не указана - перейти в конец скрипта.
h (H) - скопировать (добавить) область редактирования в область удержания
g (G) - скопировать (добавить) область удержания в область редактирования
x - обменять содержимое области удержания и редактирования.
Менее полезные (для меня по крайней мере) команды.
a - добавить текст после результата работы. \ может экранировать новую строку. Примеры:

aline
a line
a line1\
line2
i - вставить текст перед результатом работы.
c - заменить результат работы на текст.
Пояснения к командам i, c, a. В данном случае результатом называется то, во что в конце концов превращается текущая область редактирования. То есть:
sed -e 'p'
выведет каждую строку дважды. Первый раз по команде p, а второй раз как результат обработки строки(с ней ничего не делалось, поэтому результат = самой строке)
Так вот, результатом в данном случае будет только каждая 2-ая строка.
Или ещё пример:
sed -ne 'p'
результат есть, но он равен "" из-за опции -n, и поэтому каждая строка выводится 1 раз.
Вернёмся к командам i, c, a. Эти команды работают до, вместо, и после вывода результата!
Это значит, что
sed -e 'p;ix'
выведет строку(работа p), x (работа ix) и снова строку( результат)
sed -ne 'p;ix'
Соответственно выведет строку(работа p), x(работа ix) и результат(который пуст из-за опции -n).
Аналогично, c заменяет только результат, а a - добавляет текст только после результата

r Имя-файла - работает как a, но добавляет текст из файла к результату.
R Имя-файла - добавляет только первую строку из файла к результату.

Остальные команды либо не так важны, либо я ещё не осознал их важность.

Есть ещё кроме команд комментарии и метки:
:label
- пометить строку меткой, на которую можно будет перейти командой b
# - начать комментарий до конца строки.

Ну вот вроде и всё.

5. Пример применения
Задача: Удалить все переводы строк, если они не начинают абзац. (Т.е. превратить текст в ряд длинных строк, какждая их которых - абзац)
Подумаем... Будем добавлять строки в область удержания, пока не встретим начало абзаца. Как только абзац начался - выводим из области удержания склеенную длинную строку.
И снова начнём собирать длинную строку.
Ну ещё разумеется нужно обработать последнюю строку, и вывести всё что осталось в области удержания.
Красной строкой будем считать всё что начинается длиннее 1 пробела. Вот такое регулярное выражение

/  |\t/

(без использования опции -r, символ '|' так же необходимо экранировать - / \|\t/)

Итак, при встрече начала абзаца - выведем предыдущий абзац из области удержания:

/  |\t/ {
x # меняем местами обе области
s/\n//g # удалим все лишние переводы строки
p # выводим область редактирования (в ней то, что было в области удержания)
s/.*// # обнуляем область редактирования (d не подходит)
x # снова меняем местами обе области местами - и мы готовы дальше работать с первой строкой параграфа
}

Затем нужно добавить текущую строку в область удержания:
H
Ну и, если это последняя строка, нужно вывести все остатки:
$ {
x # меняем местами обе области
s/\n//g # удалим все лишние переводы строки
p # выводим область редактирования
}
Теперь всё вместе:
sed -rne '
/ |\t/ {
x # меняем местами обе области
s/\n//g # удалим все лишние переводы строки
s/.*// # обнуляем область редактирования (d не подходит)
x # снова меняем местами обе области местами - и мы готовы дальше работать с первой строкой параграфа
}
/ |\t/ ! H
$ {
x # меняем местами обе области
s/\n//g # удалим все лишние переводы строки
p # выводим область редактирования
}
'
Да, тут есть ещё что улучшить. Например, если первая же строка - красная (что обычно так и есть), первой будет выведена пустая строка. Так же хорошо бы нормализировать начальные пробелы, то есть заменить любые пробельные последовательности в начале строки на одну табуляцию.
Это можно сделать напустив ещё один sed на выход первого. Предположим, что первый скрипт мы сохранили в файле make-para.sed
cat input.txt | sed -nrf make-para.sed | sed -re '1 d; s/[ \t]+/\t/'