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

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

продовжую вивчати потоковий неінтерактивний текстовий редактор sed. попередній допис містив приклади використання для додавання в текст інтервалів між рядками та вибіркового видалення рядків. найцікавіші приклади були такі:
ходімо далі.


2. нумерація рядків


2.1. проста нумерація рядків

задача: пронумерувати кожен рядок вхідного файлу.
sed = | sed 'N;s/\n/\t/'
ага, починаються значно складніші приклади. тут маємо насправді дві різних команди. перший sed застосовує команду =, котра просто виводить у вихідний потік номер поточного рядка, а тоді — новим рядком — виводить текст з робочого буфера. побачити, як це працює, можна на простому прикладі:
cat test.txt | sed =
результат буде такий:
1
Бринь бандура, та й замовкне…
2
     Чом же не заграє?
3
Стоїть старець під віконцем, —
4
     Чом же не співає?
…і т.д. не зовсім те, що треба — хотілося би об’єднати номер і подальший текст в одному рядку. для цього потрібен другий sed: він завантажує перший рядок виводу («1») в робочий буфер, команда N (від next — наступний) додає до робочого буфера (pattern space) символ переносу рядка (\n), а тоді завантажує і приєднує наступний рядок з вхідного потоку («Бринь бандура, та й замовкне…») — це вже близько, але треба позбутися того зайвого \n посередині… тут працює команда s///, котра шукає в робочому буфері фрагмент тексту за шаблоном і заміняє на вказаний фрагмент — в нашому випадку шукає символ \n (новий рядок) і заміняє на \t (табуляція).

приклад:
cat test.txt | sed = | sed 'N;s/\n/\t/'
результат: вивід тексту з нумерацією рядків і табульованим відступом зліва.


2.2. нумерація рядків з вирівнюванням номеру вправо

задача: пронумерувати кожен рядок вхідного тексту, вирівнявши номери по правій стороні.
sed = | sed 'N; s/^/     /; s/ *\(.\{6,\}\)\n/\1  /'
важливо! sed дуже чутливий до синтаксису рядка з параметрами… кожен символ важливий. буквально наявність чи відсутність пробілу перед зірочкою * в команді може означати помилку в роботі команди. враховуючи загальну нечитабельність синтаксису команд sed і регулярних виразів — тут є над чим задуматися новачкові.

перша частина зрозуміла (див. п. 2.1) — це вставка нумерації перед кожним текстовим рядком. далі: команда N з’єднує два рядки (номер і наступний текстови й рядок) в один, з переносом рядка (\n) десь посередині. команда s/^/<тут_5_пробілів>/ — це вже знайома команда пошуку і заміни (див. п. 2.1), котра вставляє необхідну кількість пробілів на початку кожного рядка (регулярний вираз ^ означає якраз початок рядка).

отже, якщо взяти для прикладу перший рядок тестового файлу, на цьому етапі в робочому буфері sed матиме таке (пробіли заміняю мінусами - для наглядности):
-----1\nБринь бандура, та й замовкне…
наступна команда s містить складний регулярний вираз, якщо прибрати зворотні похилі риски, що позначають екранування спеціальних символів, маємо такий шаблон: -*(.{6,})\n (пробіл замінено на мінус). він означає, що треба знайти у робочому буфері нуль або більше символів -, за котрим йтиме фрагмент з 6 будь-яких символів (.{6,}), за котрим у свою чергу йтиме символ нового рядка \n.

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

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

поекспериментувавши з наглядними мінусами замість пробілів у команді, їх треба знову замінити на пробіли — і готово, задачу виконано.

приклад:
cat test.txt | sed = | sed 'N; s/^/     /; s/ *\(.\{6,\}\)\n/\1  /'
результат:
     1  Бринь бандура, та й замовкне…
     2       Чом же не заграє?
     3  Стоїть старець під віконцем, —
     4       Чом же не співає?


2.3. нумерація лише непустих рядків

задача: пронумерувати лише непусті рядки в тексті.
sed '/./=' | sed '/./N; s/\n/  /'
перший sed відбирає лише ті рядки, в яких є бодай один символ (/./) — і додає перед ними поточний номер рядка (див. п. 2.1), пусті рядки виводяться без змін. другий sed знову вибирає непустий рядок, з номером (/./), поєднує його з наступним текстовим (N), пропускаючи пусті рядки без змін; наступна команда заміни s позбувається символа \n всередині кожного рядка, заміняючи його на пару пробілів для відступу.

приклад:
cat test.txt | sed '/./=' | sed '/./N; s/\n/  /'
результат:
1  Бринь бандура, та й замовкне…
2      Чом же не заграє?
3  Стоїть старець під віконцем, —
4    Чом же не співає?

6  Ой ходив би я по селах
7    Од хати до хати,
8  Ой співав би на ввесь голос, —
9    Нікому співати!
важливо! очевидно, що результат дивний — пустий рядок не має номера, але нумерація таки враховує його як п’ятий. це особливості роботи команди =, вона ж бо виводить поточний номер зчитаного рядка, незалежно від того, пустий він чи ні! щоби реалізувати правильну нумерацію, котра справді ігноруватиме пусті рядки — треба подумати. пізніше візьмуся до цього.


2.4. підрахувати кількість рядків у тексті

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

приклад:
cat test.txt | sed -n '$='
для тестового файлу результат буде такий:
9
команда нарахувала 9 рядків — враховано і пустий рядок у файлі, як і в попередньому прикладі (див. п. 2_3).

наразі все з нумерацією. далі ще буде.