sed — проста вулична текстова магія

| комп'ютери, linux, підказки

вже давно я — затятий користувач мишок і графічних інтерфейсів! — зачудований прихованою магією командного рядка. якось із часом так виходить, що все більше повсякденних задачок на нетбуці вирішую радше в терміналі, аніж мишкою по кнопочках. але разом з тим чимраз сильніше відчуваю, який же ж я профан в цім ділі.

особливо дався взнаки брак знань, коли розбирався з реалізацією календаря на стільниці через conky та sed. от саме sed вразив мене найбільше, — і я пообіцяв собі колись обов’язково навчитися ним користуватися.

дещо пізніше я натрапив у тенетах на чудову статтю «famous sed one-liners explained» — але сама собою вона нікого нічого не навчить. потрібно все пропустити «через себе» і спробувати на практиці. а заразом — перекласти окремі фрагменти укранською: може, комусь ще стане в нагоді.

(пропустити вступ і перейти одразу до посібника sed)


дуже коротко про sed

досить детально про потоковий неінтерактивний текстовий редактор sed розказано у вікіпедії.

коротко: sed завантажує один рядок тексту з вхідного буфера (input stream) до сховища шаблону (pattern space), застосовує до нього набір команд, заданий параметрами у командному рядку, видає результат до вихідного потоку (output stream), — і повторює з наступним рядком тексту, аж доки не впорається з усім вмістом вхідного потоку. команди можуть бути різні, але найбільш типова — знайти певну послідовність символів згідно шаблону (регулярний вираз) і замінити чимось іншим. так — рядок за рядком — sed здатен обробити величезні об’єми тексту.

втім, досить важко зрозуміти, що воно таке і навіщо треба, доки не спробуєш на практиці.


застереження та підготовка

досвічені користувачі *nix та консолі не читатимуть цей текст — бо вже, певно, вивчили sed one-liners напам’ять =) початківців застережу: найперше я пишу для себе, це просто «нотатки на полях», тож я й не збираюся перекладати дослівно «famous sed one-liners explained». зате на відміну від оригіналу будуть живі приклади.

ще підказка для початківців: спробувати власноруч будь-який приклад з подальшого можна, просто набравши в терміналі щось таке:
echo "простий приклад" | sed <команда>
або, якщо на вході потрібен текст з кількох рядків, таке:
echo -e "перший рядок\nдругий рядок" | sed <команда>
проте набирати щоразу тексти з клавіатури набридає — тож я собі підготував простий тестовий файл з кількома рядками тексту, створивши файл test.txt у простому текстовому редакторі nano:
cd ~
nano test.txt
…і надрукувавши дві перших строфи зі «старця» пантелеймона куліша. не дивуйтеся, якщо ці рядки щоразу з’являтимуться тут в прикладах роботи sed. вихідний файл виглядає точнісінько ось так (табуляція на початку парних рядків кожної строфи, пустий рядок між строфами):
Бринь бандура, та й замовкне…
     Чом же не заграє?
Стоїть старець під віконцем, —
     Чом же не співає?

Ой ходив би я по селах
     Од хати до хати,
Ой співав би на ввесь голос, —
     Нікому співати!
тепер, щоби змусити sed обробити тексту файлі test.txt, достатньо буде скомандувать таке:
cat ~/test.txt | sed <команда>
результат виконання такої команди буде виведено в термінал — але якщо потрібно зберегти його в файл, скажімо test2.txt, необхідно додати > test2.txt на кінці:
cat ~/test.txt | sed <команда> > test2.txt
підказку (англійською) щодо використання echo, cat, sed чи nano, можна отримати, скориставшись командою man:
man echo
ну, наразі досить підказок. до справи.


1. відступи та інтервали


1.1. подвійний інтервал між рядками

задача: додати пустий рядок після кожного текстового рядка у файлі для імітації подвійного інтерліньяжу.
sed G
редактор завантажує кожен рядок запропонованого тексту, додає у хвіст символ нового рядка, видає результат у вихідний потік, — і повторює операцію з кожним наступним рядком.

приклад:
cat test.txt | sed G
результат — вивід на екрані з подвійним інтервалом між рядками


1.2. потрійний інтервал між рядками

задача: додати два пустих рядки після кожного текстового.
sed 'G;G'
редактор може застосовувати до тексту кілька команд послідовно, якщо команди розділено крапкою з комою. важливо! складні команди (більше одного символа) необхідно заключати в одинарні прямі лапки.

приклад:
cat ~/test.txt | sed 'G;G'
результат очевидний: вивід на екрані з подвійним інтервалом між рядками.


1.3. «розумне» додавання подвійного інтервалу між рядками

задача: додати пусті рядки лише там, де їх ще немає, уникаючи подвоєння і прибравши зайві.
sed '/^$/d;G'
редактор дозволяє застосовувати команди вибірково — лише до рядків, що відповідають шаблону. в даному випадку шаблон /^$/ відбирає лише пусті рядки, що не містять тексту — а наступна команда d видаляє кожен такий рядок; непусті (текстові) рядки буде пропущено без змін. наступна команда G відділена крапкою з комою — і отже застосовується останньою, вже над результатом попередньої команди, тобто і над пустими (що не містять тепер навіть переносу рядка!), і над текстовими рядками.

приклад:
cat test.txt | sed '/^$/d;G'
результат: скільки б не було пустих рядків між текстовими початково, команда видалить їх усі — і додасть лише один розріджувальний.

але стривайте, як перевірити? адже у підготовленому заздалегідь тексті не було пустих рядків? а приміром ось так — застосувавши до тексту спершу потроєння інтерліньяжа (п. 1.2), а тоді цю «розумну» команду:
cat ~/test.txt | sed 'G;G' | sed '/^$/d;G'
результат: вивід на екрані лише з подвійним (!) інтервалом між рядками.


1.4. вилучити пусті рядки

задача: маючи файл з подвійним чи потрійним інтервалом між рядками, або й з нерегулярним розподілом пустих рядків, привести його до «щільного» вигляду лише з текстовими рядками.
sed '/^$/d'
оригінальний текст «famous sed one-liners» пропонує інший варіант команди: sed 'n;d'… але він покладається на те, що кожен непарний рядок у файлі — пустий… але якщо це раптом не так? насправді задача вирішується інакше — звикористанням фрагменту з попереднього прикладу: фільтр /^$/ відбирає кожен пустий рядок на вході, команда d видаляє його (тобто очищує робочий буфер), і sed завантажує наступний рядок; якщо він текстовий — фільтр його пропускає на вихід без змін.

щоб перевірити, як воно працює — спершу згенеруємо файл test2.txt, скажімо, з потрійним інтервалом (див. п. 1.2):
cat test.txt | sed G
і тепер спробуємо. приклад:
cat test.txt | sed '/^$/d'
результат: вивід на екрані без пустих інтервалів між текстовими рядками.


1.5. вилучити парні рядки

задача: вилучити усі парні (кожен другий, тобто другий, четвертий і т.д.) рядки в тексті.
sed 'n;d'
тут починається цікаве, бо логіка роботи цієї команди не одразу зрозуміла. команда n просто видає у вихідний потік поточний вміст робочого буфера, і (одразу) завантажує наступний рядок (другий) зі вхідного потоку. наступна команда d очищує вміст робочого буфера (тобто видаляє парний рядок!), не видаючи нічого у вихід — і завантажує наступний (третій!) рядок. і так далі. цікаво, що схожана перший погляд задача — видалити лише непарні рядки — так просто не вирішується. до неї доведеться взятися пізніше.

приклад:
cat test.txt | sed 'n;d'
результат: у текстовому виводі залишаться лише непарні рядки файлу test.txt.


1.6. вставити пусті рядки над вибраним текстом

задача: вставити пусті рядки вибірково, перед кожним рядком, що відповідає певному шаблону.
sed '/regex/{x;p;x;}'
тут regex — шаблон, заданий регулярним виразом. приміром, вже відомий (див. п. 1.3) вираз  /^$/ дозволяє вибрати кожен пустий рядок і застосувати до нього наступні команди. але дублювати таким чином інтервали нецікаво, бо це можна зробити простіше:
sed '/^$/p'
тут фільтр вибирає пусті рядки, а команда p просто дублює їх — текстові ж рядки пропускаються без змін. а команда {x;p;x;} значно цікавіша і корисніша, бо працює інакше.

команда x обмінює вміст робочого буфера (pattern space) і буфера пам’яті (hold buffer); якщо до цього часу в буфер пам’яті ще нічого не зберігалося — він пустий; отже, перша x запам’ятовує поточний рядок в пам’яті, а пустий рядок поміщає в робочий буфер. далі p друкує поточний буфер (пустий рядок), а наступна x знову завантажує з буфера пам’яті збережений рядок, а туди повертає пустий рядок — і оскільки це остання команда, sed друкує поточний вміст робочого буфера.

важливо! фігурні дужки групують команди, щоби вони виконувалися над рядком, що відповідає шаблону, всі разом — інакше, якщо їх опустити, до вибраного згідно шаблону рядка було б застосовано лише першу x, а наступні p;x; застосовувалися б до усіх без винятку рядків.

для прикладу якогось корисного шаблону можна взяти такий: /^[^\t]/ — він відбиратиме рядки, що не починаються з символа табуляції (див. тестовий текст у файлі test.txt).

приклад:
cat test.txt | sed '/^[^\t]/ {x;p;x;}'
результат: вивід в терміналі, в якому перед кожним текстовим рядком, що не починається табуляцією, буде додано пустий рядок.

це перший справді корисний на практиці приклад, бо вибираючи шаблони відповідно до потенційного вмісту текстового файлу, можна, скажімо, додавати відступи перед параграфами, що починаються з двох-трьох пробілів поспіль, тощо…


1.7. вставити пусті рядки під вибраним текстом

задача: вставити пусті рядки вибірково, після кожного текстового рядка, що відповідає певному шаблону.
sed '/regex/G'
комбінація фільтру з регулярним виразом (див. п. 1.6) і команди G (див. п. 1.1) — редактор завантажує рядок тексту з вхідного потоку, і якщо той відповідає шаблону, подає у вихідний поток, додавши позаду символ нового рядка.

приклад:
cat test.txt | sed '/^.*[Бб]андура.*/G'
результат: вивід в терміналі, в якому після кожного рядка, що містить слово «бандура», буде додано один пустий інтервал (в прикладі такий рядок лише один, перший).

інший приклад:
cat test.txt | sed '/^\t.*/G'
результат: після кожного рядка, що починається символом табуляції (в прикладі таких чотири) буде додано один пустий інтервал.


1.8. вставити пусті рядки над і під вибраним текстом

задача: вставити пусті рядки вибірково, над і під кожним текстовим рядком, що відповідає певному шаблону.
sed '/regexp/ {x;p;x;G;}
комбінація двох попередніх рішень (див. п. 1.6 та 1.7), згрупованих в одну команду за допомогою {}.


1.9. вилучити непарні рядки

задача: вилучити всі непарні (перший, третій і т.д.) рядки з тексту.
sed '1~2d'
попередній спосіб видалення парних рядків (див. п. 1.5), попри певну «алгоритмічну» красу, насправді поганий — бо важкозрозумілий. є значно простіший спосіб вибрати кожен k-тий рядок, починаючи з n-го: достатньо всього-лиш вказати n~k! а команда d видалить вибране. тож якщо потрібен кожен парний рядок, починаючи з другого — це 2~2, а якщо кожен непарний, починаючи з першого — це 1~2. просто, зрозуміло, і легко запам’ятати.

приклад:
cat test.txt | sed '1~2d'
результат: вивід тексту без непарних рядків.

іще приклад:
cat test.txt | sed '/^$/d' | sed '2~2d'
результат: вивід тексту, в якому спершу видалено усі пусті рядки (перший sed), а потім усі парні текстові рядки (другий sed).

наразі вистачить, далі ще обов’язково буде.