sed — проста текстова магія (частина 3)

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

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


3. перетворення тексту і заміни


3.1. перетворити символи нового рядка з формату dos/windows на unix

задача: перетворити комбінацію символів нового рядка в стилі dos/windows (cr/lf) до стилю unix (lf)
sed 's/.$//'
команда покладається на те, що кожен текстовий рядок закінчується послідовністю символів cr/lf (не перевіряючи), і що виконується вона в середовищі *nix. прочитавши новий рядок в робочий буфер, sed відкидає кінцевий символ нового рядка (lf). команда шукає останній символ рядка в буфері (/.$/) і видаляє, не замінюючи нічим (//).

важливо! жодної перевірки, чи справді текст містить переноси в стилі dos/windows, або пошуку саме такої комбінації в тексті — не виконується. тому спосіб цікавий лише академічно: реальні задачі варто вирішувати інакше…

кращий спосіб:
sed 's/^M$//'
передумова — середовище *nix. команда шукає символ переводу рядка cr (^M), який є останнім в рядку (/^M$/, і видаляє, не замінюючи нічим (//).

важливо! тут ^M — не дослівно набрана з клавіатури комбінація дашка і великої літери m, а один символ, який можна ввести з клавіатури в терміналі, набравши спершу ctrl+v, а тоді одразу ctrl+m.

найкраща варіація цього способу:
sed 's/\x0D$//'
по суті, та сама команда, в якій замість самого символа cr вказано його код (13) в шістнадцятковій нотації (0x0D). щоправда, не всі версії sed цей варіант підтримують — потрібна версія gnu sed, але усі версії лінук саме її й використовують.


3.2. перетворити символи нового рядка з формату unix на dos/windows

задача: зворотня до попередньої; перетворити символ нового рядка у стилі unix (lf) наприкінці рядка на комбінацію cr/lf у стилі dos/windows.
sed "s/$/`echo -e \\\r`/"
передумова — середовище *nix: використано вставку echo -e \r (кожен з двох останніх символів екрановано), котра є командою оболонки (shell) і видає символ cr, який додається в кінець рядка (/$/) в робочому буфері — під час виводу рядка у вихідний потік sed автоматично додасть до нього ще символ lf.

інший спосіб:
sed 's/$/\r/'
цей спосіб працює в gnu sed, і працює аналогічно — але без необхідності застосовувать команду оболонки: просте \r позначає символ cr.

зауваження. тут я свідомо пропускаю всі способи аналогічного перетворення рядків у середовищі dos/windows, бо мені без потреби, а якщо доведеться — завжди можна пошукати sed one-liners у тенетах.


3.3. прибрати пробіли і табуляцію на початку рядка

задача: прибрати пробіли і символи табуляції на початку кожного рядка тексту.
sed 's/^[ \t]*//'
проста команда: шукає будь-яку кількість (*) пробілів і символів табуляції ([ \t]) на початку рядка (^ — і заміняє «нічим» (//), тобто видаляє.

приклад:
cat test.txt | sed 's/^[ \t]*//'
результат:
Бринь бандура, та й замовкне…
Чом же не заграє?
Стоїть старець під віконцем, —
Чом же не співає?


3.4. прибрати пробіли в кінці рядка

задача: прибрати зайві пробіли в кінці кожного рядка тексту.
sed 's/[ \t]*$//'
аналогічно попередньому прикладу — але шаблон в команді заміни (s///) відбирає пробіли і табуляцію в кінці ($) рядка.


3.5. видалити пробіли на початку і в кінці рядка

задача: об’єднати в одній команді рішення обох попередніх задач.
sed 's/^[ \t]*//;s/[ \t]*$//'
можна було б і просто запустити двічі sed… але красивіший спобів — виконати його один раз, вказавши дві команди через крапку з комою, вони виконуватимуться послідовно над вмістом робочого буфера.


3.6. відступ зліва

задача: утворити відступ зліва для всього тексту, додавши до кожного рядка по 5 пробілів на початку.
sed 's/^/     /'
проста команда заміни: пошук початку рядка в робочому буфері (/^/) і вставка в цій позиції потрібної кількості пробілів (/ /).


3.7. виключка тексту вправо

задача: виключити текст вправо, вирівнявши кожен рядок до 79-ї позиції.
sed -e :a -e 's/^.\{1,78\}$/ &/;ta'
значно складніший приклад. рішення використовує нову опцію (-e) і дві команди (: та t). опція -e подібно до ; дозволяє поєднувати кілька послідовних команд sed в одному рядку (і процесі):
sed -e <команда 1> -e <команда 2>
команда : створює мітку, а t реалізує логіку умовного розгалуження, як у мовах програмування: якщо попередня команда змінила текст в робочому буфері, t повертає sed до виконання до першої команди після вказаної мітки, якщо з змін не було — перехід не відбувається.
отже, логіка команди така: фрагмент -e :a створює мітку з ім’ям a; команда s вибирає в робочому буфері фрагмент тексту, який містить не більше 78 будь-яких символів (.{1,78}) від початку (^) до кінця ($), і заміняє його увесь на цей же фрагмент (&), але з пробілом попереду (/ &/) — таким чином текст в буфері стає довшим на один передній пробіл. наступна команда ta переходить назад до мітки a… і так відбувається доти, поки довжина тексту в буфері є не більшою заданої в шаблоні (78 символів) — але як тільки вона стає 79, перехід на мітку a більше не виконується, sed завершує виконання послідовності — і виводить результат у вихідний потік.

важливо! для правильної роботи цієї команди її варто об’єднати з видаленням табуляції на початку рядка та кінцевих пробілів:
sed -e 's/^[ \t]*//;s/[ \t]*$//' -e :a -e 's/^.\{1,78\}$/ &/;ta'


3.8. виключка тексту по центру

задача: відцентрувати кожен рядок тексту в рамках простору шириною 79 символів.
sed -e :a -e 's/^.\{1,77\}$/ & /;ta'
все аналогічно до попередньому прикладу, за винятком того, що центрувати можна лише ті рядки, котрі хоча б на 2 символи коротші за максимальну ширину (79), а доповнювати пробілами — з обох сторін (/ & /). і теж варто спершу видаляти передні та кінцеві пробіли й табуляцію.

важливо! недолік цього рішення в тім, що — сюрприз! — він генерує кінцеві пробіли. а вони ж насправді зайві. звісно, можна результат роботи команди пропустити ще раз через знайоме видалення кінцевих пробілів, але є елегантніший варіант:
sed -e :a -e 's/^.\{1,77\}$/ &/;ta' -e 's/\( *\)\1/\1/'
перша частина зрозуміла — це виключка вправо, а от друга цікава, бо використовує тимчасову змінну, але як красиво використовує! шаблон ( *)\1 (прибрав екрануючі зворотні риски) означає: знайти в тексті першу найбільшу доступну послідовність пробілів (запам’ятавши її в змінній \1), але так, щоби за нею слідувала… така ж точно (оте \1) послідовність! і вибрати для наступної операції обидві.

що це значить? якщо в тексті є послідовність із п’яти пробілів поспіль — цей шаблон знайде перші два (за ними ж ідуть ще два!), збереже їх в змінну \1 — і вибере для редагування два+два пробіли, залишивши п’ятий (зайвий) поза увагою.

останній фрагмент цієї команди (/\1/) — заміна вибраного фрагменту (х+х пробілів) збереженою в \1 половинкою (х пробілів). по суті текстовий рядок спершу виключено вправо, а тоді з передніх пробілів видалено меншу половину.


заміна фрагмента тексту

задача: замінити довільний (наприклад, перший або другий) фрагмент в тексті згідно шаблону іншим фрагментом.
sed 's/foo/bar/x'
тут foo — фрагмент, який треба знайти, bar — текст для заміни, а x — номер фрагменту (1 або 2 в цьому прикладі). якщо параметр x опустити — матиметься на увазі перший, а якщо вказати g (від global) — буде замінено всі знайдені за шаблоном фрагменти.

важливо! лік фрагментів ведеться не в тексті загалом, а в кожному рядку окремо!

приклад (використано раніше створений тестовий файл):
cat ~/test.txt | sed 's/а/А/'
результат:
Бринь бАндура, та й замовкне…
   Чом же не зАграє?
Стоїть стАрець під віконцем, —
   Чом же не співАє?
інший приклад:
cat ~/test.txt | sed 's/а/А/2'
…і результат:
Бринь бандурА, та й замовкне…
   Чом же не загрАє?
Стоїть старець під віконцем, —
   Чом же не співає?
ще один приклад:
cat ~/test.txt | sed 's/а/А/g'
…і результат:
Бринь бАндурА, тА й зАмовкне…
   Чом же не зАгрАє?
Стоїть стАрець під віконцем, —
   Чом же не співАє?
для одного допису досить. далі буде!

оновлення. аж через рік я взявся пригадувати просту текстову магію для цілком практичної задачки.