Глава 7. Примеры из info sed.

Содержание

Центрирование строк.
Увеличение числа.
Переименование файлов.
Вывод окружения bash.
Инверсия порядка символов в строке.
Реверс строк в файле.
Нумерация строк
Нумерация строк с использованием счётчика в области удержания.
Нумерация не пустых строк.
Подсчёт символов.
Подсчёт слов.
Подсчёт строк.
Вывод первых 10 строк.
Вывод последних строк
Вывод не повторяющихся строк.
Печать только строк, которые повторяются - uniq -d
Удаление всех повторяющихся строк.
Удаление пустых строк и быстродействие.

Здесь предоставлены разные примеры из info sed.

Центрирование строк.

Этот скрипт центрирует строки в 80колоночный формат, если 80 колонок вам не нравится, поменяйте число пробелов в первом блоке {...}

Замечание

А ещё дальше в скрипте встречается число 81 - его тоже надо поменять.

Пример 7.1.

#!/usr/bin/sed -f
# Загрузка 80и пробелов в буфер2
1 {
	x
	s/^$/          /
	s/^.*$/&&&&&&&&/
	x
}

# удаление табуляции, начальных и конечных пробелов
y/tab/ /
s/^ *//
s/ *$//

# добавление 80 пробелов в конец строки
G

# извлечение первых 81го символа (80 + a '\n')
s/^\(.\{81\}\).*$/\1/

# Теперь в буфере у нас m символов, затем \n, а потом 80-m пробелов
# и мы ищем совпадение пробелов с пробелами (после \n)
# т.к. * у нас жадная, то она захватывает максимальное число пробелов,
# т.е. ровно половину(если 80-m чётное, иначе захватывается (80-m-1)/2
# пробелов).
# вот эти 	(80-m)/2 пробелов мы переносим в начало строки.
s/^\(.*\)\n\(.*\)\2/\2\1/

Увеличение числа.

Этот скрипт демонстрирует арифметику в sed. Далее, будут описаны и более быстрые примеры, этот уж откровенно тормозной, впрочем для подсчёта 1-1000 элементов и он подойдёт.

Пример 7.2.

#!/usr/bin/sed -rf
/[^0-9]/ d
# Заменяем последние девятки на '_' (подойдёт любой другой символ,
# кроме цифр).
# {drBatty} пришлось делать циклом - если использовать s///g,
# то поменяется все девятки, а надо только последние, их можно найти
# s/9+$/но, на что менять?/
:d
	s/9(_*)$/_\1/
td
# тут увеличивается только последняя цифра. (не считая оконечных девяток)
# если строка пустая, или в ней только ____ тогда она меняется на "1____"
# остальные цифры просто увеличиваются на 1, и хвост из "_" сохраняется.
#
# tn команды не обязательны, но мы думаем, что так будет быстрее.
# {drBatty} щаз! В первой строке обязательно tn надо - а то получится
# 9 + 1 = 20 :)
s/^(_*)$/1\1/; tn
s/8(_*)$/9\1/; tn
s/7(_*)$/8\1/; tn
s/6(_*)$/7\1/; tn
s/5(_*)$/6\1/; tn
s/4(_*)$/5\1/; tn
s/3(_*)$/4\1/; tn
s/2(_*)$/3\1/; tn
s/1(_*)$/2\1/; tn
s/0(_*)$/1\1/; tn
# 99999 -> _____ -> 00000
:n
y/_/0/

`sed' гуру Greg Ubben написал калькулятор `dc' !

Замечание

{drBatty} - совсем крыша поехала у человека... Хотя там не очень сложно... Но всё одно - писать устанешь.

Переименование файлов.

Это чрезвычайно важный пример для русского программера; кроме того, что тут показано, как обрабатывать вывод одной команды передачей его другой, вы так-же поймёте как можно переименовывать файлы

Замечание

на Руси эта задача встречается сплошь и рядом - всё из-за того, что
  1. большинство Linux-систем считают правильной кодировку UTF-8,
  2. Однако, старые *nux системы (например моя) работают в KOI8-R,
  3. Windows работает в кодировке cp1251
  4. Та-же венда использует в своей консоли cp866 (а так-же эта кодировка обычно на CD дисках|образах)
  5. В почте используется традиционно KOI8-R
  6. Многие программы и БД используют какую-то стандартную (в Америке) Русскую(sic!) кодировку ISO-непомню-сколько
  7. На русских сайтах и форумах вообще чёрт ногу сломит (они в нескольких кодировках).
  8. В некоторых случаях(например в реестре) венда решила догнать и перегнать, и как всегда, ректальным путём - используя UTF-16
  9. можно ещё много чего весёлого вспомнить... )
Что касается содержимого файлов, то с ними проще - достаточно прогнать их через iconv -f КОДИРОВКА, и текст преобразуется из чужеродной КОДИРОВКИ в нашу, родную (не пишу какую именно, у вас может быть другая)

Далее приводится оригинальный скрипт:

Пример 7.3.

#! /bin/sh
# rename files to lower/upper case...
#
# usage:
#    move-to-lower *
#    move-to-upper *
# or
#    move-to-lower -R .
#    move-to-upper -R .
#

help()
{
	cat << eof
	Usage: $0 [-n] [-r] [-h] files...
	-n      do nothing, only see what would be done
	-R      recursive (use find)
	-h      this message
	files   files to remap to lower case

	Examples:
	$0 -n *        (see if everything is ok, then...)
	$0 *

	$0 -R .

	eof
}


apply_cmd='sh'
finder='echo "$|" tr " " "\n"'
files_only=

while :
do
	case "$1" in
		-n) apply_cmd='cat' ;;
		-R) finder='find "$-"type f';;
		-h) help ; exit 1 ;;
		*) break ;;
	esac
	shift
done

if [ -z "$1" ]; then
	echo Usage: $0 [-h] [-n] [-r] files...
	exit 1
fi

LOWER='abcdefghijklmnopqrstuvwxyz'
UPPER='ABCDEFGHIJKLMNOPQRSTUVWXYZ'

case `basename $0` in
	*upper*) TO=$UPPER; FROM=$LOWER ;;
	*)       FROM=$UPPER; TO=$LOWER ;;
esac

eval $finder | sed -n '

# удаляем все слеши в конце имени
s/\/*$//

# добавляем ./ если нет пути, а есть только имя.
/\//! s/^/.\//

# сохраняем путь + имя
h

# удаляем путь
s/.*\///

# преобразование имени
# тут скрипт прерывается, между кавычками не скрипт,
# а шелл, там можно переменные писать.
y/'$FROM'/'$TO'/

# после команды x буфер будет содержать путь + имя,
# а буфер2 будет содержать новое имя.
x

# добавим новое имя к пути + имя.
G

# проверяем: если новое имя такое-же как старое - ничего не делаем
# (скрипт завершается)
/^.*\/\(.*\)\n\1/b

# теперь, трансформируем
# путь/старое_имя\nновое_имя в
# mv путь/старое_имя путь/новое_имя
# и распечатываем
s/^\(.*\/\)\(.*\)\n\(.*\)$/mv \1\2 \1\3/p

' | $apply_cmd

# ппц. с sed-то мне понятно, но зачем такой скрипт накрутили...


Честно говоря, мне этот мутный скрипт не понравился - уж больно он запутанный и непонятный. Конечно хаять - самое простое, потому я оставляю это любопытное дело для вас, а сам я уже написал свой вариант, в нём основную часть скрипта мне удалось свернуть с 9и, до четырёх команд моей любимой sed. Это мне удалось благодаря во первых префиксам \L и \U, о которых рассказано выше (ими очень удобно оперировать регистром букв, к тому-же они работают и с русскими буквами). И кроме того, я использовал GNUтый модификатор e, позволяющий не только найти, и поменять строку, но и выполнять её (без всяких извратов вроде eval, как в оригинале).

Замечание

Оба скрипта имеют ограничение: нельзя переименовывать файлы с \n в имени. (впрочем и пробелы тоже не допустимы, точнее их надо экранировать, а ещё точнее - хрен его знает, проверять лень). Кроме того, я-бы не хотел, что-бы скрипт был слишком перегружен незначительными деталями.

Идеи построения данных скриптов, думаю пригодятся для решения широкого класса задач (особенно когда файлов много, а правила переименования не тривиальны).

Предостережение

Хотя в имени файла допустимы любые байты кроме \0 и '/', для sed не все байты являются СИМВОЛАМИ, несмотря на это, я всё-же писал скрипты, которые корректно обрабатывают и строки с НЕСИМВОЛАМИ. (Явно об этом нигде не сказано, но НЕСИМВОЛЫ появляются лишь при использовании кодировки UTF-8, в обычных кодировках всё работает (вроде-бы) нормально, однако, и там нужно быть очень внимательным).

Замечание

Сказано. В каталоге с исходниками, в файле BUGS.

Пример 7.4.

#!/bin/bash
# rename files to lower/upper case...
#
# usage:
#    move-to-lower *
#    move-to-upper *
# or
#    move-to-lower -R .
#    move-to-upper -R .
#

help()
{
	cat << eof
	Использование: $0 [-n] [-r] [-h] files...

	-n      Ничего не делает, только показывает, что будет делать.
	-R      Рекурсивный обход каталогов (переименовывает только файлы)
	-h      Это сообщение.
	files   Файлы подлежащие переименованию.

	Примеры:
	$0 -n *        (попробуйте сначала так, если понравится...)
	$0 *

	$0 -R .

	eof
}

finder=""
modif="ep"

while :
do
	case "$1" in
		-n) modif="p" ;;
		-R) finder="find" ;;
		-h) help ; exit 1 ;;
		*) break ;;
	esac
	shift
done

if [ -z "$1" ]; then
	echo "Использование: $0 [-h] [-n] [-r] files..."
	exit 1
fi

case `basename $0` in
	*upper*)	prefix="\\U" ;;
	*)			prefix="\\L" ;;
esac

# поиск файлов
if [ -z "$finder" ]; then
	finder=`echo "$@" | sed -r 's/\s+/\n/g'`
else
	finder=`$finder $@ -type f`
fi

echo "$finder" | sed -rn '
# удаление слешей в конце строки.
s!/+$!!

# если нет слешей, то в начеле приписываем "./"
/\//! s!^!./!

# вот и всё:
# достаточно отделить имя от пути.
# имя это \2, а путь \1
# переносим этот файл, перед новым именем
# ставим префикс
s!^(.*)(/[^/]+)$!\1\2\n\1'$prefix'\2!

# не, ещё проверить надо
/^(.*)\n\1$/ ! s/^(.*)\n(.*)/mv -v \1 \2/'$modif'
'


Вывод окружения bash.

Цель этого примера: показать как sed умеет заглядывать вперёд в тексте. То, что начинается функция будет ясно только в след. строке, а решать, печатать или нет текущую строку надо прямо сейчас, потому sed приходится запоминать строку, загружать сл. и в зависимости от результата проверки печатать или не печатать текущую.

Пример 7.5.

#!/bin/sh

set | sed -n '
:x

# Если не совпадает с "=()", печать, и загрузка след. строки
/=()/! { p; b; }
/ () $/! { p; b; }

# начинается секция с функцией
# сохраняем строку с заголовком в буфере2
h

# если след. строка начинается на {, то выходим - дальше ничего
# интересного нет.
n
/^{/ q

# вывод прошлой строки
x; p

# переход на обработку текущей.
x; bx
'

Предостережение

Обратите внимание: это вообще-то скрипт на shell (на bash), однако в нём присутствует вставка на sed. Сам sed-скрипт заключён в 'простые кавычки'.

Инверсия порядка символов в строке.

Этот скрипт меняет последовательность символов в строке на противоположную, например
$ echo "ABCDEFGHJ" | ./reverseline.sed
JHGFEDCBA

Пример 7.6.

#!/usr/bin/sed -rf

# если в строку нет 2х символов (иначе, длинна строки меньше двух),
# то ничего не делаем
/../! b

# Реверс строки

# сначала в начало и в конец строки добавляются символы \n.
s/^.*$/\
&\
/

# Обмен первого и последнего символа местами, символы меняются
# вместе с маркёрами \n, причём относительный порядок символа и маркёра
# не меняется. Поэтому маркёры оказываются "внутри" - например:
# \n F .* E \n --- до замены
# E \n .* \n F --- после
# Цикл продолжается пока внутри не останется 1 или 0 символов.
tx
:x
s/(\n.)(.*)(.\n)/\3\2\1/
tx

# Удаление маркёров.
s/\n//g

Реверс строк в файле.

Пример 7.7.

#!/usr/bin/sed -nf

# обращение строк входного файла, т.е. первая строка становится последний и т.д..

# примечание: тут *весь* файл гоняется из области удержания и обратно для каждой строки,
# например если у нас 100,000 строк по 80 байт, нам надо перенести 8,000,000 из памяти в память
# :-( это конечно очень медленно, и может привести к переполнению памяти для некоторых
# реализаций sed...


# начиная со второй строки, мы *добавляем* область удержания к буферу
# (область удержания содержит сейчас все строки, кроме текущей в обратном порядке)
1! G

# если строка последняя, то мы распечатываем все строки
$ p

# сохранение области удержания
h

Нумерация строк

Этот скрипт заменяет команду cat -n, т.е. не просто выводит файлы в stdout, но и ещё добавляет к ним номера строк.

Подсказка

Мы можем воспользоваться shell-скриптом, для того, что-бы объединить 2 команды sed: первая будет выводить номера строк и сами строки для второй, которая объединит и отформатирует вывод (попробуйте например просто набрать sed '=' test_file.txt).

Пример 7.8.

#! /bin/sh
sed -e "=" $@ | sed -e '
s/^/      /
N
s/^ *\(......\)\n/\1  /
'

Тут для склейки строк с номером используется команда N. В оригинале написано, что этот скрипт для примера, а не для обучения, однако, позволю себе заметить, что такой скрипт на длинном файле отработал за 4 секунды, а следующий пример работает уже минут 10, и вовсе не собирается останавливаться! Т.о. в данном конкретном случае использовать одну sed - плохая идея, используйте две ;-). А вся беда в том, что sed может уметь выводить номер строки только в выходной поток, по какой-такой причине не сделали за 30+ лет вывод в буфер - мне неведомо... Вот и приходится извращаться, номер вывести можно, можно использовать как адрес (в т.ч. и как диапазон или с каким-то шагом), а вот использовать - нельзя.

Нумерация строк с использованием счётчика в области удержания.

Следующий алгоритм увеличивает десятичное число используя два буфера, которые имеются у sed, при этом, конечно, содержимое области удержания необратимо теряется. Опять-таки - отсебятина: во-первых это и не важно обычно, во-вторых, можно и тут извернуться, и сохранить содержимое буфера.

Замечание

Как-же сохранить буфер? Это довольно просто: надо загружать в буфер строчки командой H, тогда мы сможем отрезать старое содержимое буфера командой
s/(.*)\n.*/\1/
а для получения новой строчки достаточно выполнить
s/.*\n//
Однако в данном примере и так используется данный приём - к буферу добавляется загруженная строка - командами x; G; h. После печати строки с номером, мы вырезаем из неё номер, для того, чтобы его увеличить на единицу.

Предостережение

Этот пример работает, но работает он медленно. Намного быстрее не мучиться, а использовать для таких целей две команды sed, конвейером (используя sed-команду = для получения номера строки). Кроме того, использование cat -n ещё быстрее, ведь это не скрипт, а специально созданная программа.

Кроме того, увеличивать номер строки быстрее с помощью bash'а, этот язык намного лучше справляется с арифметикой. Сам по себе bash умеет выполнять арифметические действия, например так bash увеличивает число на единицу (инкремент):

echo "123" | sed -r 's/^[0-9]+$/x=&; ((x++)); echo $x/e'

Тут я сначала загоняю число в bash-переменную $x, которую затем увеличиваю на 1 ((используя двойные скобки, эта непереносимая конструкция, и работает только в bash'е. Для переносимости лучше использовать например let, хотя лучше вообще ничего не использовать ;) )).

Число, которое будет найдено sed подставится в новую строчку, которая отправится через пайп (pipe) на выполнение оболочке. Это три команды bash'а. После выполнения этих команд в буфере окажется увеличенное на единицу число. Тут оно просто выведется на экран, однако его можно редактировать и проводить с ним другие действия.

Внимание

Обратите внимание на то, как я фильтрую число - тут можно было-бы написать и просто /.*/, и всё-бы работало. Однако, я пишу намного более сложное выражение: /^[0-9]+$/. Это связано с безопасностью: злоумышленник может подсунуть мне какую-нибудь гадость, которую sed отправит по конвейеру на выполнение shell. В данном случае это не получится: если подсунуть скрипту что-то отличающееся от цифр, то команда s///e попросту не сработает. Это не паранойя, это обычная практика.

Если средств bash'а вам недостаточно, то можно использовать калькулятор bc - он способен выполнять намного более сложные арифметические действия: например извлечение квадратного корня с точностью в 50 знаков после запятой.

$ echo "scale=50; sqrt(3)" |bc
1.73205080756887729352744634150587236694280525381038

Тут команда echo отправляет по конвейеру команды для калькулятора bc, и это всё можно встроить в sed, которая с лёгкостью отправит эти команды bash'у. Конечно это долго, но всё-же намного быстрее, чем вычислять корни средствами самой sed (учитывая, что никаких средств у sed для этого нет).

Подсказка

Арифметические задачи встречаются намного чаще, чем принято думать. Конечно вычисление корней нужно довольно редко, но есть и другие задачи. К примеру перевод в другие системы счисления. К примеру эта команда
echo 'ed2k://|file|Zlo.tvorimoe.ludjmi_SATRip.1984.(emule-rus.net).avi
|915735510|CA2DAD0B6FE305058D9296147FC096A5|h=ZI357V3JSPJGKKTB5A2CR4EYYAUZJJJC|/
'| sed -r 's/.*\|([[:xdigit:]]{32})\|.*/echo "ibase=16; \1" | bc/e'
268741218270185578931233294203517703845
выделит из ED2K ссылке хеш файла, а затем переведёт его в десятичную систему счисления. Хеш записан в шестнадцатеричной системе счисления, и для его извлечения я использую выражение /[[:xdigit:]]{32}/.

Пример 7.9.

#!/usr/bin/sed -nrf

# Извлекаем номер строки, если он равен "" (пустая строка),
# то устанавливаем его в 1 (такое бывает для первой строки).
x
/^$/ s/^.*$/1/

# Добавляем номер строки в её начало
G
h

# Форматирование и печать
s/^/      /
s/^ *(......)\n/\1  /p

# Извлекаем номер строки из области удержания,
g
s/\n.*$//

# Если номер начинается с девятки, добавляем перед ней ноль.
/^9*$/ s/^/0/

# тут я даже и не знаю как объяснить: короче, перед последней цифрой и (возможно)
# девятками после неё добавляем букву х, например для 123545 получится 1234х5,
# а для 12999 получится 1х2999, т.е. буква х - граница изменения, будем менять то,
# что *после* неё.
s/.9*$/x&/

# сохраняем число
h
# вырезаем ту часть, которая не поменяется
s/^.*x//
# в изменяемой части меняем 0 на 1, 1 на 2, и т.д...
y/0123456789/1234567890/
# теперь опять берём всё число
x
# однако теперь берём неизменную часть
s/x.*$//

# осталось объединить части
G
s/\n//
# и сохранить следующий номер в области удержания.
h

Нумерация не пустых строк.

Следующий алгоритм отличается от предыдущего разве-что отсутствием комментариев, единственное отличие - это в начале
/^$/ { p; b}
Это нужно для пропуска пустой строки.

Пример 7.10.

#!/usr/bin/sed -nf

/^$/ {
	p
	b
}

# Same as cat -n from now
x
/^$/ s/^.*$/1/
G
h
s/^/      /
s/^ *\(......\)\n/\1  /p
x
s/\n.*$//
/^9*$/ s/^/0/
s/.9*$/x&/
h
s/^.*x//
y/0123456789/1234567890/
x
s/x.*$//
G
s/\n//
h

Подсчёт символов.

Этот скрипт подсчитывает количество символов, он нужен для иллюстрации другого пути реализации арифметики в sed-скриптах.

Замечание

Так делал Робинзон, подсчитывая дни: он отмечал каждый день маленькой чёрточкой, которые затем, раз в неделю перечёркивал одной большой. Так-же поступим и мы: будем отмечать единицы буквой a, и как их скопится 10 штук, заменим их на b, и так далее.

Пример 7.11.

#!/usr/bin/sed -nf

# Для начала, все символы мы превращаем в `a'
s/./a/g
# и добавляем к нашему счётчику, напомню, a - это единица.
H
x
# перенос строки - тоже символ.
s/\n/a/

# а так реализован перенос разрядов, для свёртки единиц в десятки и т.д..
# Автор упоминает о том, что команды перехода тут необязательны,
# однако он полагает, что так будет быстрее.  Если честно - я не знаю,
# думаю что разница не будет слишком большой.

b a
: a;  s/aaaaaaaaaa/b/g; t b; b done
: b;  s/bbbbbbbbbb/c/g; t c; b done
: c;  s/cccccccccc/d/g; t d; b done
: d;  s/dddddddddd/e/g; t e; b done
: e;  s/eeeeeeeeee/f/g; t f; b done
: f;  s/ffffffffff/g/g; t g; b done
: g;  s/gggggggggg/h/g; t h; b done
: h;  s/hhhhhhhhhh//g

# Если строка не последняя, то мы сохраняем наш счётчик,
# и завершаем обработку этой строки.
: done
$! {
	h
	b
}

# Для последней строки необходима конвертация в десятичную
# систему счисления.

: loop
# след. команда s выполнится тогда, когда в счётчике нет букв a,
# а это бывает тогда, и только тогда, когда число кратно 10,
# в этом частном случае, мы добавляем к числу справа 0,
# при этом следующие 9 s не выполняются (так-как "a" нету).
	/a/! s/[b-h]*/&0/
	# для не кратных 10 чисел мы меняем последние буквы a на нужную цифру
	# догадайтесь сами, почему они будут последними :-)
	s/aaaaaaaaa/9/
	s/aaaaaaaa/8/
	s/aaaaaaa/7/
	s/aaaaaa/6/
	s/aaaaa/5/
	s/aaaa/4/
	s/aaa/3/
	s/aa/2/
	s/a/1/

	: next
	# Эта хитрая команда: она делит счётчик на 10, например "db" у нас равно 1010,
	# а после замены будет "ca", т.е. - 101.
	y/bcdefgh/abcdefg/
	# и так до тех пор, пока все буквы не перейдут в цифры.
	/[a-h]/ b loop
p

Замечание

Некоторые версии sed имеют ограничение 199 команд на скрипт (об этом сказано в оригинальном info sed). Я не нашёл в исходных текстах своей версии такого ограничения. Не вижу никаких причин ограничивать свои скрипты, если конечно вы не планируете использовать sed от necro$oft. Т.к. в OS Windows никакой sed нет, и её придётся ставить дополнительно, то даже в этой «ОС» можно писать скрипты из любого числа команд...

Подсчёт слов.

Этот скрипт практически не отличается от предыдущего, разве что, тут все слова превращаются сначала в "a", а затем они считаются. Кроме того, несколько изменена схема переносов: сам алгоритм не поменялся, но вместо условного перехода t используется переход
/УСЛОВИЕ/ b

Замечание

Ну тут наверное самое время выяснить, что лучше и быстрее.

Как вы уже наверное догадались, в оригинальном info об этом не сказано, уж не знаю почему. Наверное потому, что авторам info sed не хватило времени на это. Впрочем они и без этого много сделали.

На самом деле, оба способа перехода по своему хороши: переход с помощью адресного выражения и с помощью команды b обычно намного удобнее:

  1. Переход командой b с адресом не затрагивает флаг перехода, который постоянно глючит.
  2. Адресное выражение компилируется, и в дальнейшем может использоваться повторно. К примеру так:
    /XXX/{
    	s///
    }
    Тут команда s повторно ищет /XXX/ и его удаляет. Это намного быстрее, т.к. не требуется второй раз компилировать регулярное выражение.

К сожалению, часто использования ветвления адресным выражением и командой b невозможно: дело в том, что в адресном выражении недопустимы обратные ссылки, и потому там нельзя использовать многие RE.

Кроме того, команда s сама реализует ветвление - замена состоится только если RE будет найдено. При этом замена вовсе не обязательно подразумевает только замену символов - это может быть и запись в файл, или даже выполнение внешней команды.

В итоге, мы видим что в sed-скриптах можно производить несколько типов условных переходов:

  1. Можно использовать адресное выражение, которое выполнит команду (или блок команд в {фигурных скобках}) только при выполнении определённого условия.
  2. Можно использовать условный переход с помощью команды b с адресом.
  3. Также можно производить множество действий с помощью команды s, которая выполняет заданные действия только если будет найдено хотя-бы одно совпадение заданного RE.
  4. Ну и на конец, возможно использовать условные переходы, командами t и T.

Решение за вами. Каждый способ обладает своими преимуществами и недостатками.

Предостережение

Как вы сами наверное знаете, нельзя использовать GOTO - команду безусловного перехода. Вот только в sed-скриптах у нас нет другого выхода - кроме GOTO у нас ничего нет. По этой причине следует очень внимательно форматировать свои скрипты и при этом фанатично соблюдать свой стиль написания программ. Конечно, вы можете забить на эту мою рекомендацию, но это приведёт только к тому, что вы сами не сможете разобраться в своих-же скриптах. Оно вам надо? Если надо (например из соображений безопасности) - используйте другие (компилируемые) языки, либо компилируйте sed-скрипты.

Внимание

Однако, напомню вам простую истину: всегда можно сломать «закрытый код». Если вы не способны разобраться в закрытой программе, это вовсе не значит, что вскрыть код невозможно и программа «вполне безопасна». Лично я многократно взламывал чужой код - в этом нет ничего архи-сложного. Потому не следует закрывать свой код - есть множество других способов защиты.

Пример 7.12.

#!/usr/bin/sed -nf

# Convert words to a's
s/[ tab][ tab]*/ /g
s/^/ /
s/ [^ ][^ ]*/a /g
s/ //g

# Append them to hold space
H
x
s/\n//

# From here on it is the same as in wc -c.
/aaaaaaaaaa/! bx;   s/aaaaaaaaaa/b/g
/bbbbbbbbbb/! bx;   s/bbbbbbbbbb/c/g
/cccccccccc/! bx;   s/cccccccccc/d/g
/dddddddddd/! bx;   s/dddddddddd/e/g
/eeeeeeeeee/! bx;   s/eeeeeeeeee/f/g
/ffffffffff/! bx;   s/ffffffffff/g/g
/gggggggggg/! bx;   s/gggggggggg/h/g
s/hhhhhhhhhh//g
:x
$! { h; b; }
:y
	/a/! s/[b-h]*/&0/
	s/aaaaaaaaa/9/
	s/aaaaaaaa/8/
	s/aaaaaaa/7/
	s/aaaaaa/6/
	s/aaaaa/5/
	s/aaaa/4/
	s/aaa/3/
	s/aa/2/
	s/a/1/
	y/bcdefgh/abcdefg/
	/[a-h]/ by
p

Подсчёт строк.

Тут вообще не о чём думать!!! Как не странно, но sed это делает свободно:

Пример 7.13.

#!/usr/bin/sed -nf
$=

Вывод первых 10 строк.

Так-же просто реализовать head: достаточно прервать работу sed после вывода нужного числа строк.

Пример 7.14.

#!/usr/bin/sed -f
10q

Вывод последних строк

Эмуляция tail намного сложнее: sed не понимает, что такое "строка номер 10 с конца", потому-что файл просматривается построчно всего 1 раз. Мы должны сохранять где-то последние десять строк, проще всего: в области удержания:

Пример 7.15.

#!/usr/bin/sed -nf
1! {; H; g; }
1,10 !s/[^\n]*\n//
$p
h

Здесь текущая строка прибавляется к области удержания, а начиная с одиннадцатой, из этой области удаляется та строка, которая там была первой.

Быстрее должен быть (проверьте, мне лень) другой подход - использовать буфер, из которого начиная с одиннадцатой строки удаляется первая, для файлов длиннее 10 строк у нас будет всегда 11 циклов, первые 10 строк загрузятся в первых 10 циклах, а в 11ом загрузятся все остальные строки, при этом грузится они будут командой N, которая догружает строку из потока в буфер, а потом первая строка удалится командой D, которая ничего не выводит. В конце файла мы просто прервём скрипт, и он распечатает буфер, т.е. последние 10 строк.

Пример 7.16.

#!/usr/bin/sed -f
1h
2,10 {; H; g; }
$q
1,9d
N
D


Замечание

тут используется точка с запятой, для отделения команд. Это допустимо, но НЕ обязательно.

Замечание

Я всё-же проверил: получилось для первого скрипта 18.5 сек., а для второго 6.4 сек. Таким образом второй скрипт работает примерно в 3 раза быстрее. Для сравнения: tail сработала за 2.3 секунды (ага - эта задача явно не для sed, sed вообще ни в коем случае нельзя использовать, когда есть другая утилита, заточенная именно на эту задачу - что-же вы хотели, та-же tail выделяет в самом начале памяти под 10 строк, и просто читает туда данные, нам-же приходится для каждой строки удалять первую - для чего необходимо сдвинуть остальные - т.о. нам нужно не только прочитать N байт(одну строку), переместить N*10 байт! Если-бы файл был в памяти, то sed сработала-бы не в три раза, а в 11 раз медленнее! (кстати, похоже он и был в памяти(файл в 18Мб, свободно около 200), видимо для сдвига строк (D) sed применяет следующую оптимизацию: вместо того, что-бы сдвигать весть буфер, она просто считает, что после команды D буфер начинается не с первой, а со второй строки, надо исходник глянуть). В любом случае, двигать буфер на каждой строке без особой на то необходимости - плохая идея.) Проблема sed в данном случае: невозможность загружать строки в произвольное место буфера, мы можем только добавлять в конец буфера, или полностью заменить строку. С другой стороны, значительного увеличения быстродействия можно было-бы достичь другим путём: прочитать все строки, а после этого вернутся на 10 строк назад, если известна максимальная длинна строки(M), то это сделать довольно просто: посчитать все строки, а затем вернутся на M*10 байтов назад и найти десятую строчку, я, правда не знаю, чем возвращаться, разве что той-же tail с ключом -c.

Вывод не повторяющихся строк.

Этот скрипт иллюстрирует работу команд P, D, и N.

Пример 7.17.

#!/usr/bin/sed -rf
h

:b
# если строка последняя - печать и выход.
$b
# загружаем следующую строку
N
/^(.*)\n\1$/ {
	# сл. строка такая-же как прошлая, выгружаем
	# прошлую из области удержания, как-бы отменяя действие команды N
	g
	bb
}

# если команда N загрузила посл. строку, печатаем две посл. строки сразу.
$b

# строки разные, мы сначала печатаем первую из них
P
# потом её удаляем
D
# и при окончании этого цикла печатается следующая строка.

Печать только строк, которые повторяются - uniq -d

Пример 7.18.

#!/bin/sed -rnf
$b
N
/^(.*)\n\1$/ {
	# печать первой повторяющейся строки
	s/.*\n//
	p

	# цикл, читаем строки до тех пор, пока они повторяются
	:b
	$b
	N
	/^(.*)\n\1$/ {
		s/.*\n//
		bb
	}
}

# последняя строка не может повторятся
$b

# мы нашли две разные строки, мы удаляем первую, а вот следующую
# необходимо анализировать дальше
D

Удаление всех повторяющихся строк.

Этот скрипт удаляет все строки которые повторяются.

Пример 7.19.

#!/usr/bin/sed -rf

# поиск линий, которые не повторяются, до тех пор пока сл. строка
# отличается от предыдущей, печатаем те, которые отличаются.
$b
N
/^(.*)\n\1$/ ! {
	P
	D
}

:c
# Сейчас у нас две одинаковые строки в буфере,
# если это последние строки - просто выходим.
$d

# иначе отрезаем одну из одинаковых строк, добавляем ещё одну,
# и если строки одинаковые - переходим к метке :с.
s/.*\n//
N
/^(.*)\n\1$/ {
	bc
}

# Удаляем "одинаковую" строку, и переходим
# к началу скрипта.
D

Удаление пустых строк и быстродействие.

Последний пример посвящён быстрому удалению пустых строк (как cat -s).

для начала соберём все пустые строки вместе и заменим их одной пустой строкой:

Пример 7.20.

#!/usr/bin/sed -f

# для пустых строк выполняется команда N,
# которая их читает в этом цикле
# примечание: в регулярном выражении использована звёздочка
# для задания любого числа '\n', которые вставляет команда N
# (во время первого прохода этого символа в строке вообще нет)
:x
/^\n*$/ {
	N
	bx
}

# сейчас у нас имеется не пустая строка, перед которой стоят несколько
# символов '\n'
# все эти символы мы заменим на один перевод строки
s/\n*/\
/


К сожалению, этот скрипт вставляет пустые строки и между не пустыми, что нам не нужно.

Вот следующий вариант:

Пример 7.21.

#!/usr/bin/sed -f

# Удаление всех начальных пустых строк
# примечание от drBatty: непонятно почему написано /^./,
# ИМХО можно и просто /./
1,/^./{
	/./!d
}

# для пустых строк мы грузим следующую, и если она тоже пустая,
# мы удаляем загруженный командой N символ '\n'
# а потом опять загружаем следующую.
:x
/./!{
	N
	s/^\n$//
	tx
}


В оригинале написано, что этот скрипт недостаточно быстр...

А этот скрипт вроде как побыстрее...

Пример 7.22.

#!/usr/bin/sed -nf

# удаление всех (начальных) пустых строк
/./!d

# теперь у нас имеется не пустая строка
:x
	# печатаем её
	p
	# извлекаем следующую
	n
	# есть символы? тогда продолжаем печатать не пустые строки
	/./bx

# символов нет - эта строка пустая
:z
	# вынимаем следующую строку, если её нет (текущая - последняя)
	# во время выполнения n скрипт завершает работу
	n
	# ещё одна пустая? тогда мы её игнорируем, и переходим к следующей
	# этот скрипт удаляет ВСЕ пустые строки
	/./!bz

# все пустые строки были удалены/проигнорированы
# и теперь мы добавляем ещё одну пустую строку ПЕРЕД той, что сейчас напечатается
i\

bx


Ну мы сейчас проверим, создадим файл из 100000 тестовых файлов командой

for ((i=1; i<10000; i++)); do cat test.txt >> big.txt; done

и посмотрим...

Действительно, третий вариант немного быстрее второго, 1.47сек против 1.62. Хотя разница не слишком велика. (для файла в 17Мб). Если взять файл в 5 раз больше (85 248 295 байт), то картина та-же: 7.08сек против 7.88сек. Однако следует учесть, что если строки длинные и кодировка UTF-8, то разница будет намного больше (наверное на ~50%). По той причине, что в третьем варианте нет команды s///, которая очень медленно работает для длинных строк (особенно в кодировке UTF-8).

На http://unixforum.org мы сравнивали быстродействие cut, awk, sed и perl'а - как выяснилось - sed самая медленная. Но perl слишком сложен для меня, а cut и awk работают только с полями, и часто их невозможно использовать (к примеру в этом случае это не получится). В bash'е так-же имеется возможность работы с текстом. Я её даже не рассматриваю, потому как даже для создания тестового файла в 85Мб потребовалось несколько минут, видимо для его обработки потребуется десятки минут, а может и часы.

Вы можете обсудить этот документ на форуме. Текст предоставляется по лицензии GNU Free Documentation License (Перевод лицензии GFDL).

Вы можете пожертвовать небольшую сумму яндекс-денег на счёт 41001666004238 для оплаты хостинга, интернета, и прочего. Это конечно добровольно, однако это намного улучшит данный документ (у меня будет больше времени для его улучшения). На самом деле, проект часто находится на грани закрытия, ибо никаких денег никогда не приносил, и приносить не будет. Вы можете мне помочь. Спасибо.