понедельник, 17 марта 2008 г.

Просто о 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/'

48 комментариев:

Solnce комментирует...

Так уж получилось что случилось страшное. Мне как раз понадобилось заменить в 50+ текстовых файлах кое-какое значение. Дёрнулся в сторону man sed, но засторелой виндузовое мышление и слабые знания в буржуйном языке сделаи саму идею восприятия такого объёма текста невозможной.
А тут действительно просто и доступно. Как раз то что нужно.
Будем тестировать.

Анонимный комментирует...

Полностью согласен с solnce
Автору огромное спасибо)
(кстати это первый линк который google дал=)

Анонимный комментирует...

Ребята подскажите как удалить все до и после определенных символов пример:

Имеем строку:

4877:45ClientUserinfoChanged: 0 n\ExcessivePlayer\t\0\model\sarge\hmodel\sarge\c\5???5\c1\4\c2\5\hc\100\w\0\l\0\tt\0\tl\0

надо сделать так чтобы осталась только имя игрока (ExcessivePlayer), тоесть всё до "n\" и постле "\t" надо убрать. Читал ман по седу но бъюсь уже который час с тем что он не понимает "\t" как текст. Спасибо!

Сергей Азаркевич комментирует...

Я так понял \t это не табуляция а именно слеш и t. Тогда нужно просто экраниовать \t - \\t
Вот решение в 2 строки.
# sed -re 's/^.*n\\//
s/\\t.*$//' x

обязательно в 2, чтоб сед понял, что это 2 команды s///. Выводит

# ExcessivePlayer

Можно и в одну. Что-то вроде sed -e 's///' | sed -e 's///' x

Сергей Азаркевич комментирует...

А впрочем чего я туплю - можно и в одну строку.
sed -re 's/^.*n\\//; s/\\t.*$//' file

Анонимный комментирует...

Вот ето класс! Надо тоже будет с седом посидеть, хорошая вещь. В общем заработало на ура, но есть еще один прикол. Ниже копирую линию которая даёт косяк из-за того что имя игрока заканчивается на n\

sed -re 's/^.*n\\//; s/\\t.*$//'

201:50 ClientUserinfoChanged: 0 n\baklazhan\t\0\model\sarge\hmodel\sarge\g_redteam\Stroggs\g_blueteam\Pagans\c1\4\c2\5\hc\100\w\0\l\0\tt\0\tl\0

Выходит что первая n\ и пытается считать строку до \t, надо как-то заставит его читать только первую n\ а последующие просто игнорировать до появления \t. Огромное спасибо за помощь!

Сергей Азаркевич комментирует...

Нет где проверить, но думаю должно помочь перемена местами команд. Сначала удалить всё после \t а потом всё до n\
Как-то так:
sed -re 's/\\t.*$//; s/^.*n\\//' file

Анонимный комментирует...

Огромное спасибо! Теперь игроков нормально видно при любом имени.
Делюсь скриптом, может кому пригодится:

#пример для кваки хотя можно и на моды ставить
Q3in=$(cat /х/ioquake3/.q3a/baseq3/лог.log | grep ClientConnect | wc -l)
Q3out=$(cat /х/ioquake3/.q3a/baseq3/лог.log | grep ClientDisconnect | wc -l)
Q3online=$(cat /х/ioquake3/.q3a/baseq3/лог.log | grep ClientUserinfoChanged | tail -n 1 | sed -re 's/\\t.*$//; s/^.*n\\//')

echo "Q3: $((Q3in-$Q3out))";
if [ $(($Q3in-$Q3out)) != 0 ];then
echo "Online: $Q3online";
fi

В конфиге надо иметь:
set g_logfile "лог.log"
set g_logfileSync "1"
set g_logsync "1"
set g_loghits "0"


Еще раз спасибо, удачи в новом году!

Анонимный комментирует...

не подскажите что это значит, уже второй день разбираюсь
sed -n '/scattered/s/.*p3=//p' filename | sort -n | tail -1

Анонимный комментирует...

то что мне понятно это выводит последнюю строку из файла, но похоже как-то перед эти строки сортирует? как мне найти ручками эту самую строку?

Сергей Азаркевич комментирует...

Ищет строки в которых сктречается подстрока scattered, удаляет в них часть '.*p3=' и выводит. Потом эти строки сортируются и выводится последняя.

Как найти. Думаю должно помочь вот что:

sed -n 'p;=' filename | sed 'N;s/\n/ #/' | sed -n '/scattered/ s/.*p3=//p' | /bin/sort -n | tail -1

Тогда к концу каждой строки прибавится её номер. Но это может повлиять на сортировку, хотя и не должно.

Анонимный комментирует...
Этот комментарий был удален администратором блога.
Анонимный комментирует...

Подскажите пожалуйста, как мне удалить из файла фигурные скобки и их содержимое... ничего не понимаю уже...
то есть из строки
{абырвалг}Нужный текст{йцукен}
получить: Нужный текст

Сергей Азаркевич комментирует...

Предполагаю, что так:
sed -re 's/{[^}]*}//g' file
Нет под рукой работающего cygwin-а, чтоб проверить...

Анонимный комментирует...

> sed -re 's/{[^}]*}//g' file

Спасибо за ответ, конечно, результат:

sed: -e выражение #1, символ 12: Неверное предшествующее регулярное выражение

Сергей Азаркевич комментирует...

Да, не экранировал {}
Правильный вариант:
sed -re 's/\{[^}]*\}//g' file

Анонимный комментирует...

Сергей, прошу помощи с sed-ом )
вобщем имеется огромный текстовый файл, каждая строка имеет вид:
2010/08/23-23:13:19.801|1733146|123456789 |9876543210 |...,
мне нужно вытащить только 123456789, т.е., скопировать, начиная со второго "|", и до третьего "|", исключая пробелы...
буду очень признателен за помощь, спасибо!

Сергей Азаркевич комментирует...

Вытащить строку между | можно утилитой cut:
cat file | cut -d \| -f 3
но, к сожалению, остаются пробелы.

Как избавиться от пробелов более элегантно не знаю, поэтому как-то так:

cat file | cut -d \| -f 3 | sed -re 's/^[ \t]*([^ \t]*)[ \t]*$/\1/'

Если пробелы могут быть только в конце и(и табуляция не может присутствовать), то сокращается до такого:

cat file | cut -d \| -f 3 | sed -re 's/^([^ ]*) *$/\1/'

Сергей Азаркевич комментирует...

Нашёл. Можно удалить все пробелы (и в середине тоже, так что может не подойти):
cat file | cut -d \| -f 3 | tr -d ' '

Анонимный комментирует...

Спасибо, вам огромное! ;)

Children of koRn комментирует...

Добрый день.
Не могли бы вы помочь со следующей задачей по sed'у:
есть последовательность символов в файле [то что в кавычках] "пробел*" нужно заменить на "пробелпробел./", ну или "*" на "пробел./" равнозначно как бы, но хотелось бы всё же понять как в условии замены прописывать пробел/ы.

Children of koRn комментирует...

Хах. как только отправил комментарий сразу в голову пришло и решенние:
sed -i 's/\ \*/\ \.\//g' '[2006]. Pale Ravine.md5'
или
sed -i 's/*/\ \.\//g' '[2006]. Pale Ravine.md5'
ну или сразу так чтобы конвертировать файл CRLF > LF
sed -i 's/\ \*/\ \.\//g
s/\r$//' '[2006]. Pale Ravine.md5'

а целью было конвертирование файла с md5 хешами от Total Commander для скармливания md5sum в убунте:
md5sum -c '[2006]. Pale Ravine.md5'
теперь прекрасно работает :)

Сергей Азаркевич комментирует...

Хм. А зачем это? Неужели убунта не понимает исходный формат от тотала, который точно такой как от утилиты md5sum ?

Children of koRn комментирует...

Неа, там немного другой синтаксис файла + CRLF, так что md5sum его хавать не хочет (%
по крайней мере так в Ubuntu Server 9.04 и md5sum (GNU coreutils) 6.10 в ней.

Children of koRn комментирует...

а нет, вы оказались частично правы, достаточно CRLF > LF и тогда всё нормально.
Просто вывод был:
: No such file or directory
: FAILED open or read
и заметив разницу в синтаксисе файлов подумал что нужна замена, когда решил эту задачу, понял, что ещё нужно конвертировать в другой EOL.
в любом случае теперь знаю как и то и то делать, что думаю хорошо (=

Сергей Азаркевич комментирует...

Есть ещё утилитка dos2unix.

Children of koRn комментирует...

Да, я в гугле встречал и этот вариант тоже, но хотелось sed'ом (=

mysikov комментирует...

Добрый день!
Как поменять одну строку на другую в потоке?
Пользоваться sed'ом или можно обойтись grep и echo?

mysikov комментирует...

Уже нашел как это делать:)
Текст

root tty1 Mar 13 17:23
mas tty2 Mar 13 18:50
sae tty6 Mar 13 17:24
sae tty5 Mar 13 17:24

Команда

who | sed '/sae/ c\
cтрока замены
'
Результат

root tty1 Mar 13 17:23
mas tty2 Mar 13 18:50
строка замены
строка замены

Сергей Азаркевич комментирует...

Можно проще:
who | sed '/sae/c строка замены'
менее громоздко

mysikov комментирует...

Добрый день:)
Возникла снова проблема:)
#текст
#111
#222
#333
#текст
Нужно удалить 2 строки, а вместо третьей вставить другую, чтобы получилось
#текст
#444
#текст
У меня получается только 2мя командами. Можно ли как-то это оптимизировать?

Сергей Азаркевич комментирует...

Где-то так:
cat file | sed '2,3 d; 4 c 444'
Удаляем с 2-ой по 3-ю строки и земеняем 4-ую на текс '444'

mysikov комментирует...

строки
#111
#222
не всегда находятся на 2м и 3м месте.
как тогда быть?

Сергей Азаркевич комментирует...

тогда по совпадению строк
sed '/#111/,/#222/ d; /#333/ c 444'

Анонимный комментирует...

tres interessant, merci

Анонимный комментирует...

Отличная статья. Теперь наконец-то стало понятно как в sed использовать буфер.

Анонимный комментирует...

avtor molodec!

Анонимный комментирует...

Понравился пост. спасибо

Анонимный комментирует...

I've finally found a normal presentation on this issue. Thank you.

Анонимный комментирует...

спасибо, интересный сайт, добавлю в закладки

Children of koRn комментирует...

Приветствую, не подскажете ли более красивое решение чем моё.

Задача такова, есть файл с цитатами, такого формата:
http://paste.ubuntu.com/680007/

нужно: зная id цитаты из переменной ${id} получить в переменную $quote чистый текст цитаты без тегов, т.е. найти например "< id002 #blockquote>" и скопировать текст в переменную до первого закрывающего тега "< / blockquote>".

пока что рабочий вариант выглядит так: http://paste.ubuntu.com/680010/

Во первых сомнения в нужности 3 sed'ов, мне почему то кажется это можно сделать самым первым.

Во вторых, если первый Sed пишу так
http://paste.ubuntu.com/680018/
т.е. указываю что после закрывающего тега конец строки,
то тогда копируется всё до самого последнего закрывающего тега в файле.

А ещё не проще это же сделать через grep/egrep?

Children of koRn комментирует...

Немного навёл порядок и вышло в итоге вот так:
http://paste.ubuntu.com/680253/

Вопросы пока остались тежи, единственно я допер почему когда писал ,/<\/blockquote>$/ то sed копировал до самого последнего <\/blockquote> в файле, это похоже изза того что файл был в виндовском CRLF, а если Unix LF то всё нормально, для CRLF можно написать просто ,/<\/blockquote>.$/ ну или сконвертировать файл.

little newbie комментирует...

sed -re 's/\\t.*$//; s/^.*n\\//' file
- совершенно потрясающая вещь, практически универсальный ключик к куче проблем! Спасибо!!!!!

...но не работает под FreeBSD :(((((

Alex комментирует...

Здравствуйте. Установил Sed в Windows, какие то каракули в окне, они помешают работать?
s019.radikal.ru/i642/1211/3b/07b2991859fe.jpg
Подскажите, как удалить первую строку, если она пустая, не могу сообразить, регулярками пробывал в текстовом редакторе, не получается:
vedomo.ru/files/1451478-1.txt
Еще нужно удалить строки короче 16 символов, если они не в верхнем регистре (уже в других файлах).

Александр комментирует...

Немного неправильно написал. Нужно удалить из текста строки короче 16 символов, при этом если в этих строках присутствует текст в верхнем регистре длинной более 5 символов (то есть от 5 до 16), то их удалять не надо.

Анонимный комментирует...

Ребята, подскажите, пожалуйста, как использовать сед в такой ситуации: нужно в строке: Аэрозоль для наружного применения, 2,5 г/58 г по 58 г или по 116 г в контейнерах. заменить слово Аэрозоль на аэроз. и удалить весь остальной текст...

Nicki комментирует...

У автора редкий талант к объяснению простыми словами сложных вещей. Спасибо.

Иван Прокофьев комментирует...

Спасибо!