Почему-то поголовно все руководства по 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 для ссылки на группы в регулярном выражении. Диалект выражений зависит от опции
-rp - вывести текущее содержимое области удержания. Если не использовать опцию командной строки
-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/'