Книга о Cintu. Часть 2. Применение системы. Глава 9. Работа с текстами средствами CLI

Книга о Cintu. Часть 2. Применение системы. Глава 9. Работа с текстами средствами CLI

Система Cintu создавалась в первую очередь для работы с текстами — и потому приложения этой направленности для нас архинужные и архиважные. И разбиваются они у нас на три категории: средства CLI для обработки текстов, простые текстовые редакторы для мелких поделок, и «продвинутые» текстовые редакторы для работы со сложным контентом. В настоящей главе речь пойдёт только о первой категории. Это — стандартные UNIX-утилиты или их GNU-версии, и потому ничего специфичного для Cintu здесь не будет.

Вступление

Итак, предметом настоящего раздела будут штатные средства POSIX-систем, позволяющие в той или иной мере просматривать контент файлов и манипулировать им. Разумеется, речь пойдёт о файлах текстовых. Однако круг объектов таких команд не столь уж узок, как может показаться. Ведь именно в виде обычных текстовых файлов хранится масса общесистемной информации. Не говоря уже о собственно нарративных текстов любого содержания: ведь чисто текстовый формат для них куда роднее, чем всякого рода *.doc‘и и *rtf‘ы. Ну и никем не возбраняется использовать такие команды в отношении текстов с разметкой — HTML ли, XML, TeX или еще чего. Так что поле приложения рассматриваемых команд — достаточно обширно.

Конечно, все необходимые действия с текстами можно проделать и с помощью текстовых редакторов. Однако в ряде случаев прямой командой — быстрей и проще. Хотя и далеко не всегда. Поэтому приводимые ниже команды — просто примеры их использования в тех случаях, когда, по нашему с Мануалом мнению, это действительно проще. Так что приводимое ниже — вовсе не обязательно заучивать, как устав внутренней службы. Достаточно помнить о существовании таких команд и случаев, когда их применение целесообразно. А за деталями можно обратиться к этому разделу данной книжки (для того он и написан). Или, разумеется, к официальной документации.

Создание файлов

Работа с файлами начинается с их создания. Конечно, в большинстве случаев файлы (вместе с их контентом) создаются соответствующими приложениями (в нашеми случае — текстовыми редакторами). Однако имеется несколько команд, специально предназначенных для создания файлов. Это — touch, cat и tee.

Первая из указанных команд в форме

$ touch filename

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

Для чего может потребоваться создание пустого файла? Например, для создания скелета web-сайта с целью проверки целостности ссылок. Поскольку число аргументов команды touch не ограничено ничем (вернее, ограничено только максимальным количеством символов в командной строке:-)), это можно сделать одной командой:

$ touch index.html about.html content.html [...]

Можно, воспользовавшись приёмом группировки аргументов, и заполнить файлами все подкаталоги текущего каталога:

$ touch dirname1/{filename1,filename2} dirname2/{filename3,filename4}

и так далее. Правда, сама команда touch создавать подкаталоги не способна — это следует сделать предварительно командой mkdir.

Команда cat также может быть использована для создания пустого регулярного файла. Для этого нужно просто перенаправить ее вывод в файл:

$ cat > filename

создать новую строку (нажатие клавиши Enter) и ввести символ конца файла (комбинацией клавиш Control+Z). Разумеется, предварительно в этот файл можно и ввести какой-нибудь текст, однако это уже относится к управлению контентом. Почему мы и рассмотрим команду cat подробнее в следующем разделе.

Интересно создание файлов с помощью команды tee. Смысл её — в раздвоении выходного потока, выводимого одновременно и на стандартный вывод, и в файл, указанный в качестве ее аргумента. То есть если использовать ее для создания файла с клавиатуры, это выглядит, будто строки удваиваются на экране. Но это не так: просто весь вводимый текст копируется одновременно и на экран, и в файл. И потому ее удобно применять в командных конструкциях, когда требуется одновременно и просмотреть результаты исполнения какой-либо команды, и запечатлеть их в файле:

$ ls dir | tee filename

По умолчанию команда tee создаёт новый файл с указанным именем, или перезаписывает одноимённый, если он существовал ранее. Однако данная с опцией -a, она добавляет новые данные в конец существующего файла.

Поиск файлов: утилита find

Поиск файлов с текстовым контентом — необходимый эпизод при работе с ними. Особенно если файлов этих за годы сочинительской деятельности накопилось без счёта. Средств для поиска файлов нынче очень много. Однако в серьёзных случаях мы с Мануалом обращаемся к старой доброй команде find. Ибо это — апофигей файловых операций.

Строго говоря, команда find, вопреки своему имени, выполняет не поиск файлов как таковой, но рекурсивный обход дерева каталогов, начиная с заданного в качестве аргумента, отбирает из них файлы в соответствие с заданными критериями и выполняет над отобранным файловым хозяйством некоторые действия.

Команда find по своему синтаксису существенно отличается от большинства прочих Unix-команд. В обобщённом виде формат её можно представить следующим образом:

$ find аргумент [опция_поиска] [значение] [опция_действия]

Аргумент — это путь поиска, то есть каталог, начиная с которого следует искать файлы, например, корневой, /, или домашний каталог пользователя.

Опция поиска — критерий, по которому следует искать файл (файлы). В качестве таковых могут выступать имя файла (-name), его тип (-type), атрибуты принадлежности, доступа или времени.

Ну а опция действия определяет, что же надлежит сделать с найденным файлом или файлами. А сделать с ними, надо заметить, можно немало — начиная с вывода на экран (опция -print) и кончая передачей в качестве аргументов любой другой команде (опция -exec).

Как можно заметить, опция поиска и опция действия предваряются знаком дефиса, значение первой отделяется от её имени пробелом.

Опции поиска команды find позволяют выполнить вышеозначенный поиск по следующим критериям:

  • -name — поиск по имени файла или по маске имени; в последнем случае метасимволы маски должны обязательно экранироваться (например, -name \*.tar.gz) или заключаться в кавычки; этот критерий чувствителен к регистру, но близкий по смыслу критерий -iname позволяет производить поиск по имени без различения строчных и заглавных букв;
  • -type — поиск по типу файла; этот критерий принимает следующие значения: -f (регулярный файл), -d (каталог) и другие, для нас в данном контексте не важные;
  • -user и -group — поиск по имени или идентификатору владельца или группы, выступающим в качестве значения критерия;
  • -size — поиск по размеру;
  • -perm — поиск файлов по значениям их атрибутов доступа, задаваемых в символьной форме;
  • -atime, -mtime, -ctime — поиск файлов с временными атрибутами (доступа, изменения и изменения атрибутов, соответственно);
  • -newer — поиск файлов, изменённых после файла, указанного в качестве значения критерия (то есть имеющего меньшее значение mtime);
  • -maxdepth и -mindepth позволяют конкретизировать глубину поиска во вложенных подкаталогах — меньшую или равную численному значению для первого критерия и большую или равную — для второго;
  • -depth — производит отбор в обратном порядке, то есть не от каталога, указанного в качестве аргумента, а с наиболее глубоко вложенных подкаталогов; смысл этого действия — получить доступ к файлам в каталоге, для которого пользователь не имеет права чтения и исполнения;
  • -prune — позволяет указать подкаталоги внутри пути поиска, в которых отбора файлов производить не следует.

Кроме этого, существует еще одна опция поиска — -fstype, предписывающая выполнять поиск только в файловой системе указанного типа, но для нас сейчас она не важна.

Критерии отбора файлов могут группироваться практически любым образом. Так, в форме

$ find ~/ -name \*.html newer filename

она выберет в домашнем каталоге пользователя все html-файлы, созданные после файла с именем filename. По умолчанию между критериями отбора предполагается наличие логического оператора «И». То есть будут отыскиваться файлы, удовлетворяющие и маске имени, и соответствующему атрибуту времени. Если требуется использование оператора «ИЛИ», он должен быть явно определён в виде дополнительной опции -o между опциями поиска. Так, команда:

$ find ~/ -mtime -2 -o newer filename

призвана отобрать файлы, созданные менее двух суток назад, или же позднее, чем файл filename.

Особенность GNU-реализации команды find — то, что она по умолчанию выводит список отобранных в соответствии с заданными критериями файлов на экран, не требуя дополнительной опции действия -print. Однако более иные опции действия должны быть указаны явным образом. Так что рассмотрим их по порядку.

Сходное с -print действие выполняет и опция -ls, однако она выводит более полные сведения о найденных файлах:

$ find /home/data/cinia -name \*blank.html -ls
8032606 4 -rw-rw-r-- 1 alv alv 224 фев 14  2018 /home/data/cinia/chronics/chronics-blank.html
281624580 4 -rw-rw-r-- 1 alv alv 278 мая 23 15:01 /home/data/cinia/cintu/cintu-blank.html
8881696 4 -rw-rw-r-- 1 alv alv 235 фев 14  2018 /home/data/cinia/manuals/manuals-blank.html
543626447 4 -rw-rw-r-- 1 alv alv 260 фев 14  2018 /home/data/cinia/features/features-blank.html

Идём далее. Опция -delete уничтожит все файлы, отобранные по указанным критериям. Так, командой

$ find ~ -atime +100 -delete

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

И, наконец, опция -exec — именно ею обусловлено величие утилиты find. В качестве значения её можно указать любую команду с необходимыми опциями — и она будет выполнена над отобранными файлами, которые будут рассматриваться в качестве её аргументов. Проиллюстрируем это на примере.

Использовать для удаления файлов опцию -delete, как мы это только что сделали — не самое здоровое решение, ибо файлы при этом удаляются без запроса, и можно случайно удалить что-нибудь нужное. И потому достигнем той же цели следующим образом:

$ find ~/ -atime +100 -exec rm -i {} \;

В этом случае на удаление каждого отобранного файла будет запрашиваться подтверждение.

Обращаю внимание на последовательность символов {} \; (с пробелом между закрывающей фигурной скобкой и обратным слэшем) в конце строки. Пара фигурных скобок {} символизирует, что свои аргументы исполняемая команда (в примере — rm) получает от результатов отбора команды find, точка с запятой означает завершение команды-значения опции -exec, а обратный слэш экранирует её специальное значение от интерпретации командной оболочкой.

Кроме опции действия -exec, у команды find есть еще одна, близкая по смыслу, опция — -ok. Она также вызывает некую произвольную команду, которой в качестве аргументов передаются имена файлов, отобранные по критериям, заданным опцией (опциями) поиска. Однако перед выполнением каждой операции над каждым файлом запрашивается подтверждение.

Приведённый пример, хотя и вполне жизненный, достаточно элементарен. Рассмотрим более сложный случай — собирание в один каталог всех чисто текстовых файлов, разбросанных по древу каталога с данными (у нас с Мануалом это всегда /home/data):

$ find ~/ -name \*.txt -exec cp {} textdir \;

В результате все txt-файлы будут изысканы и скопированы (или — перемещены, если воспользоваться командой mv вместо cp) в одно место.

А теперь — вариант решения задачи, которая казалась мне сначала трудно разрешимой: рекурсивное присвоение необходимых атрибутов доступа в разветвлённом дереве каталогов — различных для регулярных файлов и каталогов.

Зачем и отчего это нужно? Поясню на примере. Как-то раз, обзаведясь огромным по тем временам (40 Гбайт) винчестером, я решил собрать на него все нужные мне данные, рассеянные по дискам CD/DVD-R/RW (суммарным объёмом с полкубометра) и нескольким сменным винчестерам, одни из которых были отформатированы в FAT16, другие — в FAT32, третьи — в разных вариантах ext*. Сгрузив все это богачество в один каталог на новом диске, я создал в нем весьма неприглядную картину.

Ну, во-первых, все файлы, скопированные с CD и FAT-дисков, получили биты исполнения, хотя все они были файлами данных. Казалось бы, мелочь, но иногда очень мешающая; иногда это не позволяет, например, просмотреть html-файл в Midnight Commander простым нажатием Enter. Во-вторых, для некоторых каталогов, напротив, исполнение не было предусмотрено ни для кого — то есть я же сам перейти в них не мог. В-третьих, каталоги (и файлы) с CD/DVD, часто не имели атрибута изменения — а они нужны мне были для работы (в т.ч. и редактирования).

Так что ситуация явно требовала исправления, однако проделать вручную такую работу над данными более чем в 20 Гбайт виделось немыслимым. Да так оно, собственно, и было бы, если б не опция -exec утилиты find. Каковая позволила изменить права доступа требуемым образом.

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

$ find ~/dir_data -type f -exec chmod a-x,u+w {} \;

Далее — поиск каталогов и обратная процедура над итоговой выборкой:

$ find ~/dir_data -type d -exec chmod a+xr,u+w {} \;

И дело в шляпе, все права доступа стали единообразными (и теми, что мне нужны). Именно после этого случая я, подобно митьковскому Максиму, проникся величием философии марксизма (пардон, утилиты find). А ведь это ещё не предел ее возможностей — последний устанавливается только встающими задачами и собственной фантазией…

Ещё один практически полезный вариант использования команды find в мирных целях — периодическое добавление отдельно написанных фрагментов к итоговому труду жизни. Например, к этой книжке, когда (и если) она будет почти дописана. Впрочем, чтобы сделать это, необходимо сначала ознакомиться с командами обработки текстовых файлов, до которых мы скоро доберёмся.

Вместо замечательной сцепки команды find с опцией действия -exec (или опцией -ok) можно использовать отдельную команду xargs — построитель и исполнитель командной строки со стандартного ввода. А поскольку на стандартный ввод может быть направлен вывод команды findxargs воспримет результаты её работы как аргументы какой-либо команды, которую, в свою очередь, можно рассматривать как аргумент её самоё (по умолчанию такой командой-аргументом является /bin/echo). Однако ни у меня, ни у Мануала не возникало необходимости в команде xargs и, соответственно, мы не занимались её изучением. Так что заинтересованных отсылаю к соответствующей man-странице.

Просмотр файлов

Для файлов с уже существующим контентом, перед любыми манипуляциями, его желательно некоторым образом просмотреть. И тут можно вспомнить о команде cat, посредством которой мы только что создавали файлы. Данная с именем файла в качестве аргумента, она выведет его содержимое на экран. Можно использовать и конструкцию перенаправления:

$ cat < filename

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

Недостаток команды cat как средства просмотра — невозможность перемещения по телу файла: выведя содержимое, она завершает свою работу. Конечно, «пролистывание» выведенного возможно, но — средствами системной консоли (или терминала), а не самой команды.

Поэтому обычно для просмотра содержимого файлов используются специальные программы постраничного просмотра — т.н. pager’ы, очередной пример того, что передача этого термина исконно русским словом «пейджер» (а мне попадалось и такое) может создать совершенно превратное представление о сути дела.

В Unix-системах имеется две основные программы pager’а — more и less. Первая из них допускает только однонаправленный (вперёд) просмотр и имеет слабые интерактивные возможности. Почему ныне и представляет лишь исторический интерес, так что о ней я говорить не буду. Тем более, что в современных системах она как таковая отсутствует, так что дальше мы буду говорить только о команде less.

Самый простой способ вызова команды

$ less filename

после чего на экран выводится содержимое файла, указанного в качестве аргумента, по которому можно перемещаться в обоих направлениях, как построчно, так и постранично. В нижней строке экрана можно видеть символ двоеточия — приглашения для ввода команд различного назначения. В частности, нажатие клавиши h выводит справку по использованию less, а клавиши q — выход из программы просмотра (или из просмотра справочной системы, если она была перед этим вызвана). Если команда была вызвана в <«code>more-подобном» виде (это достигается специальной опцией — less -m), вместо символа двоеточия в нижней строке будет выведено имя файла с указанием процента просмотра:

command.txt 3%

что, однако, не воспрещает и здесь давать её встроенные команды — вводом символа двоеточия (:) и закреплённой за командой литеры (или их сочетания).

Большинство встроенных команд less предназначено для навигации по телу файла. Осуществляется она несколькими способами:

  • с помощью стандартных клавиш управления курсором: PageDown или Spacebar (вперед на один экран), PageUp (назад на один экран), Down или Enter (вперед на одну строку), Up (назад на одну строку), Right (на пол-экрана вправо), Left (на пол-экрана влево);
  • с помощью предопределенных клавишных комбинаций, сходных с управляющими клавиатурными последовательностями командных оболочек и таких текстовых редакторов, как emacs и joe (хотя и не всегда с ними совпадающими): Control+V (на один экран вперед), EscapeV (на один экран назад), Control+N (на одну строку вперед), Control+P (на одну строку назад);
  • с помощью фиксированных символьных клавиш, иногда подобных таковым командного режима редактора vi: z и w (вперед и назад на один экран), e и y (вперед и назад на одну строку, можно использовать также привычные по vi клавиши jи k, соответственно), d и u (вперёд и назад на пол-экрана).

Последний способ интересен тем, что допускает численные аргументы перед символьной командой: так, нажатие 3e приведёт к перемещению на три строки вперёд, а 2w — на два экрана назад.

Помимо «плавной», так сказать, навигации, можно перемещаться по файлу и скачками (jumping): нажатие клавиши с символом g (или последовательности Escape<) позволяет переместиться к первой строке файла, клавиши G (регистр важен! дублирующий вариант — Escape>) — к последней его строке, клавиши p — к началу файла.

Кроме навигации, имеется и возможность двустороннего поиска — в направлении как конца, так и начала файла. Для поиска вперёд требуется ввести символ прямого слэша (/) и за ним — искомое сочетание символов. Поиск в обратном направлении предваряется символом вопроса (?). В обоих случаях в шаблоне поиска можно использовать стандартные регулярные выражения *, ?, [список_символов] или [диапазон_символов]. Нажатие клавиши n (в нижнем регистре) приводит к повторному поиску в заданном ранее направлении, клавиши N (в верхнем регистре) — к поиску в направлении противоположном.

Управляющие комбинации команды less могут быть переопределены с помощью команды lesskey. Формат её

$ lesskey -o output input

В качестве входных данных выступает простой текстовый файл (по умолчанию — ~/.lesskey, однако его следует создать самостоятельно), описывающий клавишные последовательности в следующем, например, виде:

#command
r        forw-line
n        forw-line
...
k         back-line
...

Выходные данные — создаваемый из текстового двоичный файл, который собственно и используется командой less. Стандартное для него имя — ~/.less.

Команда less допускает одновременный просмотр нескольких файлов. Для этого ее следует вызвать в форме

$ less file1 file2 ... file#

после чего между открытыми файлами можно переключаться посредством :n (к следующему файлу), :p (к предыдущему файлу), 😡 (к первому файлу). Путём нажатия :d текущий файл исключается из списка просмотра. Символ двоеточия во всех этих случаях вводится с клавиатуры в дополнение к приглашению на ввод команд.

Команда less имеет великое множество опций — описание их на странице экранной документации занимает более дюжины страниц, поэтому задерживаться на них я не буду. Следует заметить только, что опции эти могут использоваться не только в командной строке при запуске less, но и интерактивно — после символа дефиса в приглашении ввода. Так, указав там -m, можно включить т.н. промежуточный формат приглашения (с отображением процентов просмотренного объема файла), а с помощью -M — длинный («more-подобный») формат, при котором в приглашении дополнительно указываются имя файла, его положение в списке загруженных файлов, просматриваемые ныне строки:

command.html (file 2 of 10) lines 1-29/1364 2%

Значение команд постраничного просмотра файлов ещё и в том, что именно с их помощью осуществляется доступ к экранной документации (man-страницам). Команда

$ man cmd_name

на самом деле вызывает определённый по умолчанию pager для просмотра соответствующего файла /usr/share/man/man#/cmd_name.gz. Какой именно — определяется переменной PAGER в пользовательских настройках.

Отступление: как было сказано в главе о Zsh, в этой командной оболочке (и, соответственно, в Cintu, в которой она является умолчальной) очень часто для просмотра содержимого текстового файла можно обойтись без специальной программы-pager’а. Ибо конструкция вида

$ < filename

выводит содержимое указанного файла и предоставляет почти все возможности навигации по тексту, что и code. Но не все: в частности, здесь недоступен «more-подобный» вид. И потому, хотя в большинстве случае использование указанной конструкции проще, иногда без команды less не обойтись. Так что знание её возможностей очень не вредно.

Кроме команд постраничного просмотра, существуют команды для просмотра фрагментарного. Это — head и tail, выводящие на экран некоторую фиксированную порцию файла, указанного в качестве их аргумента, с начала или с конца, соответственно. По умолчанию эта порция для обеих команд составляет десять строк (включая пустые). Однако её размер можно переопределить произвольным образом, указав опции -n [число_линий] или -c [количество_байт]. Например, команда

$ head -n 3 filename

выведет три первые строки файла filename, а команда

$ tail -c 100 filename

его последние 100 байт. При определении выводимого фрагмента в строках название опции (n) может быть опущено — достаточно числа после знака дефиса.

Существуют и средства просмотра компрессированных файлов. Для файлов, сжатых программой gzip, можно использовать команды zcat и zmore, для спрессованных командой bzip2 — команду bzcat. Использование их ничем не отличается от аналогов для несжатых файлов — в сущности, именно они и вызываются для обеспечения просмотра. В случае команды zmore, как нетрудно догадаться, на самом деле используется команда less (сама по себе она аналога для компрессированных файлов не имеет).

Сравнение файлов

Следующая важная группа операций над контентом файлов — сравнение файлов по содержанию и различные формы объединения файлов и их фрагментов. Начнём со сравнения. Простейшая команда для этого — cmp в форме

$ cmp file1 fil2

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

file1 file2 differ: char 27, line 4

Это означает, что различия между файлами начинаются с 27-го от начала файла символа (включая пробелы, символы конца строк и т.д.), который имеет место быть в строке 4. С помощью опций -l и -z можно заставить команду cmp вывести номера всех различающихся символов в десятичном или шестнадцатеричном формате, соответственно.

Более информативный вывод обеспечивает команда diff. Она также осуществляет построчное сравнение двух файлов, но выводит список строк, в которых обнаружены отличия. Например, для двух файлов вида

$ less file1
line 1
line 2
line 3
line 4
line 5

и

$less file2
line 1
line 2
line 3
line 3a
line 4
line 5

это будет выглядеть следующим образом:

$ diff file1 file2
3a4
> line 3a

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

  • a (от append) указывает на строку, отсутствующую в первом файле, но присутствующую во втором;
  • c (от change) фиксирует строки с одинаковым номером, но разным содержанием;
  • d (от delete) определяет строки, уникальные для первого файла.

То есть в данном примере для преобразования file1 в file2 в него после строки 3 должна быть вставлена строка 4 из второго файла, что символизирует вторая линия блока — > line 3a, где > означает строку из первого сравниваемого файла. Если же аргументы команды diff дать в обратном порядке, вывод ее будет выглядеть следующим образом:

$ diff file2 file1
4d3
< line 3a

показывающим, что для достижения идентичности из file2 должна быть удалена четвёртая строка (<>, где < означает строку из второго файла). Если же произвести сравнение file1 с file3, имеющим вид

$ less file3
line 1
line 2
line 3a
line 4
line 5

то вывод команды

$ diff file1 file3
3c3
< line 3
---
> line 3a

будет означать необходимость замены третьей строки из file1 (символ <) на третью строку из file3 (символ >).

Такая форма вывода команды diff называется стандартной. С помощью опции -c можно задать т.н. контекстную форму вывода, при которой на экран направляется не только различающиеся строки, но и строки, их окружающие (то есть контекст, в котором они заключены):

diff -c file1 file2
*** file1       Sun May 12 11:44:44 2002
--- file2       Mon May 13 15:17:27 2002
***************
*** 1,5 ****
--- 1,6 ----
 line 1
 line 2
 line 3
+ line 3a
 line 4
 line 5

Количество строк контента задается опцией -C:

diff -C 1 file1 file2                                      ttyv1
*** file1       Sun May 12 11:44:44 2002
— file2       Mon May 13 15:17:27 2002
***************
*** 3,4 ****
— 3,5 —-
 line 3
+ line 3a
 line 4

В этом примере значение опции -C (единица) предписывает вывод по одной строке контекстного окружения вокруг различающейся строки. Сами же различающиеся строки помечаются следующим образом: знаком - (минус, или дефис) — строки, подлежащие удалению из первого файла, знаком + (как в примере) — строки, которые должны быть добавлены, знаком ! — просто различающиеся строки.

Кроме контекстного формата, используется ещё и вывод в унифицированном формате, что предписывается опцией -U [значение], в качестве значения указывается число строк. В нем для обозначения изменяемых строк используются только символы + и -, а сам вывод чуть короче, чем при использовании контекстного формата.

С помощью многочисленных опций команды diff сравнение файлов может быть детализовано и конкретизировано. Так, опция -b предписывает игнорировать «пустые» символы пробелов и табуляции в конце строк, а опция -w — вообще «лишние» пробелы (и те, и другие обычно имеют случайное происхождение). При указании опции -B игнорируются пустые строки, то есть не содержащие никаких иных символов, кроме перевода каретки; строки с символами табуляции или пробела как пустые не рассматриваются, для их игнорирования требуется опция -w. Благодаря опции -i при сравнении не принимается во внимание различие регистров символов, а опция -I regexp определяет регулярные выражения, строки с которыми также игнорируются при сравнении.

В качестве аргументов команды diff (одного или обоих) могут выступать также каталоги. Если каталогом является только один из аргументов, для сравнения в нем отыскивается файл, одноимённый второму аргументу. Если же оба аргумента суть каталоги, в них происходит сравнение всех однимённых файлов в алфавитном порядке (вернее, в порядке ASCII-кода первого символа имени, разумеется). Благодаря опции -r сравнение файлов может осуществляться и во вложенных подкаталогах.

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

В принципе, команда diff и придумана была именно для сравнения файлов исходников, над которыми ведут работу несколько (в пределе — неограниченное количество, как в случае с Linux) человек. Однако невозбранно и ее использование в мирных целях — то есть для сравнения просто повествовательных текстов. Единственное, что следует понимать при этом абсолютно ясно — то, что diff выполняет именно построчное сравнение. То есть: сравнение последовательностей символов, ограниченных символами конце строки с обеих сторон. И, соответственно, непрерывная абзацная строка в стиле emacs и vi — совсем не то же самое, что строка, образуемая в редакторе joe на границе экрана. Впрочем, это — вопрос, к которому еще не раз придется возвращаться.

Как уже было отмечено, команда diff осуществляет сравнение двух файлов (или — попарное сравнение файлов из двух каталогов). Однако, поскольку Бог, как известно, любит троицу, есть и команда diff3, позволяющая сранить именно три файла, указываемые в качестве ее аргументов. По действию она не сильно отличается от двоичного аналога. И потому изучение ее особенностей предлагается в качестве самостоятельного упражнения приверженцам троичной идеологии.

Существуют и средства для сравнения сжатых файлов. Это — zcmp и zdiff. Подобно командам просмотра, ими просто вызываются соотвествтующие команды cmp и diff. И потому использование их не имеет никаких особенностей.

Объединение файлов

От вопроса сравнения файлов плавно перейдём к рассмотрению способов их объединения. Для этого существует немало команд, из которых по справедливости первой должна идти команда cat, поскольку именно сие есть её титульная функция (cat — от concatenation, сиречь объединения). Ранее уже упоминалось, что она способна добавлять информацию со стандартного ввода в конец существующего файла. Однако дело этим не ограничивается. В форме

$cat file1 file2 ... file# > file_all

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

А вот команда patch выступает в качестве диалектической пары для команды diff, именно она вносит в файл те изменения, которые документируются последней. Выглядит эта команда примерно так:

$ patch file1 diff_file

в ответ на что последует нечто вроде следующего вывода:

Hmm...  Looks like a normal diff to me...
Patching file file1 using Plan A...
Hunk #1 succeeded at 4.
done

В результате исходная версия file1 будет сохранена под именем file1.orig, а сам он преобразован в соответствие с описанием diff-файла. Возможна и форма

patch < diff_file

В этом случае команда patch попытается сама определить имя файла-оригинала, и, если это ей не удастся, даст запрос на его ввод. Обращаю внимание на символ перенаправления ввода, поскольку если его опустить, имя dif-файла будет воспринято как первый аргумент команды (то есть имя файла-оригинала).

В качестве второго аргумента команды patch могут использоваться dif-файлы не только в стандартном, но и в контекстном или унифицированном формате. Это следует указать посредством опции -c или -u, соответственно.

Сочетание команд diff и patch очень широко используется при внесении изменений в исходные тексты программы. Однако никто не запрещает применять их и при работе с нарративными текстами.

Деление файлов: утилита split

Не менее часто, чем в слиянии, возникает необходимость и в разделении файлов на части. Цели этой служит команда split. Формат её:

$ split [options] filename [prefix]

В результате исходный файл будет разбит на несколько отдельных файлов вида prefixaa, prefixab и так далее. Значение аргумента prefix по умолчанию — x (то есть без его указания итоговые файлы получат имена xaa, xab и т.д.).

Опции команды split задают размер выходных файлов — в байтах (опция -b) или количестве строк (опция -l). Первой опцией в качестве единицы, кроме байтов, могут быть заданы также килобайты или мегабайты — добавлением после численного значения обозначения k или m, соответственно.

Команда split может использоваться для разбиения файлового архива на фрагменты, соответствующие размеру резервных носителей. Так, в форме

$ split -b 1474560 arch_name

она обеспечит разбиение архива на части, каждая из которых может быть записана на стандартную трехдюймовую дискету. А посредством

$ split -b 650m arch_name

архив можно подготовить к записи на носители CD-R/RW. Легко догадаться, что обратное слияние таких фрагментированных файлов можно выполнить командой cat.

Произвольное разбиение: утилита csplit

Однако для нас, сочинителей, важней возможность разбивать текст не на абстрактные байты, а на произвольные фрагменты — например, сплошной текст книги на отдельные главы, для удобства размещения в сети. И для этой цели среди утилит CLI есть одна, которая как специально для того сделана. Имя этой утилите — csplit.

Разумеется, текст для этого желательно предварительно разметить, выделив, например, в html-файле заголовки различного уровня тегами h1, h2 etc. Они будут выступать как шаблоны, по которым осуществится разметка.

Итак, мы имеем книжку в html-формате, разделённую на главы, но пока — в виде единого файла, скажем, moskva-60e.html. Требуется превратить его в серию файлов, по числу глав, включая вступление — все они представляют собой заголовки 2-го уровня. Делается это такой командной конструкцией:

$ csplit -f moscva60- moskva-60e.html '/^<h2>/' '{*}'

Результат будет выглядеть так:

$ ls												[krotkov/moskva-60e]
moscva60-00  moscva60-02  moscva60-04  moscva60-06  moscva60-08
moscva60-01  moscva60-03  moscva60-05  moscva60-07  moskva-60e.html

Нетрудно догадаться, что moskva-60e.html — это исходный текст, файлы от moscva60-01 до moscva60-08 включают в себя главы соответствующих номеров, а файл moscva60-00 — вступительную часть.

Очевидно, что к циклу из девяти страниц не худо бы добавить оглавление. И это делается, не отходя от кассы терминала, такой командой:

$ echo 'Оглавдение' > moscva60-content

В результате чего к списку файлов текущего каталога добавляется ещё один, moscva60-content. Пока — пустой, если не считать строки заголовка. Но превратить его в болвану оглавления не трудно — понадобится только серия команд вида

$ less moscva60-01 | head -n 1 >> moscva60-content

И так далее. В результате к файлу moscva60-content будут добавлены первые строки их файлов >moscva60-01, то есть названия глав (в данном случае — просто Глава 1, Глава 2 и так далее).

Поиск в файлах: утилита grep

Здесь речь пойдёт о поиске внутри файлового контента — то есть поиске текстовых фрагментов. Для этого семейство утилит grep — собственно grep, egrep и fgrep, несколько различющихся функционально. Впрочем, в большинстве систем всё это суть разные имена (жесткие ссылки) одной и той же программы, именуемой GNU-реализацией grep, включающей ряд функций, свойственных ее расширенному аналогу, egrep. Соответственно, поиск текстовых фрагментов в файлах может быть вызван любой из этих команд, хотя в каждом случае функциональность их будет несколько различаться.

Однако начнём по порядку. Самой простой формой команды grep является следующая:

$ grep pattern files

где pattern — искомая последовательность символов, а files — файлы, среди которых должен производиться поиск (или — просто одиночный файл). В указании имен файлов допустимы обычные маски, например, командой

$grep line ./*

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

Шаблоны могут включать в себя регулярные выражения. Причём список таковых для команды grep существенно шире, чем для команд манипулирования файлами. Так, кроме маски любой последовательности символов (*), любого одиночного символа (?), списка и диапазона символов ([a...z] и [a-z]), могут встречаться:

  • . (точка) — маска любого одиночного (но, в отличие от маски ?, обязательно присутствующего) символа; то есть при задании шаблона lin. будут найдены строки, содержашие line или lins, но не lin;
  • ^ и $ — маски начала и конца строки, соответственно: по шаблону ^line, будут найдены строки, начинающиеся с соответствующего слова, по шаблону же line$ — им заканчивающиеся;
  • маски повторения предыдущего шаблона, \{n\} — ровно n раз, \{n,\} — не менее n раз, \{,m\} — не более m раз, \{n,m\} — не менее n раз и не более m раз.

Маски повторения относятся к так называемым расширенным регулярным выражениям. Для их использования команда grep должна быть дана с опцией -e или в форме egrep — последняя часто определяется в общесистемном или пользовательском профильном файле как псевдоним команды grep:

alias grep='egrep -s'

При этом становятся доступными и другие возможности поиска — например, нескольких текстовых фрагментов (соединенных логическим оператором «ИЛИ») одновременно. Делается это двояко. Первый способ — просто перечисление искомых фрагментов, каждый из которых предваряется опцией -e (и при необходимости экранируется кавычками):

$ grep -e pattern1 -e pattern2 files

При втором способе оператор между искомыми шаблонами задаётся в явном виде:

$ egrep 'pattern1|pattern2' files

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

$ egrep 'Часть|Глава|Раздел|Параграф' filename

Для текста, включающего html- или TeX-разметку, роль рубрик могут выполнять соответствующие её элементы, например:

$ egrep ' <h1>|<h2>|<h3>|<h3>' filename

Вывод команды grep может быть перенаправлен в файл, а при необходимости и предварительно отсортирован с помощью соответствующих командных конструкций перенаправления и конвейеризации.

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

$ egrep '<h1>|<h2>|<h3>|<h3>' path/*.html > sitemap.html

Ещё одно замечательное свойство команды grepegrep) — возможность получения шаблона не со стандартного ввода (то есть не путём набора его с клавиатуры), а из файла. Так, если для приведённого выше случая создать простой текстовый файл shablon, содержащий строку

Часть|Глава|Раздел|Параграф

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

$ egrep -f shablon filename

Опция -f и указывает команде, что список параметров должен извлекаться из файла, указанного в качестве значения опции.

Список опций команды grep не исчерпывается указанными выше. Так, опция -i предписывает игнорировать различие регистров внутри искомого выражения, опция -h — подавляет вывод имён файлов (выводится только содержание найденных строк), тогда как опция -l, напротив, выводит только имена файлов, содержащих найденный шаблон, но не текстовый фрагмент, опция -n выводит номера найденных строк в соответствующих файлах. Весьма важной представляется опция -v, обеспечивающая инверсию поиска: при указании ее выводятся строки, не содержащие шаблона поиска.

Команда grep имеет и аналог для поиска в сжатых файлах — команду zgrep. Использование её в целом аналогично, и останавливаться на нем я не буду.

Потоковое редактирование: утилита sed

Весьма часто при обработке текстов встаёт такая задача: заменить одно слово (или последовательность слов) на другое сразу во многих файлах. Она может быть решена средствами потокового (неинтерактивного ) редактирования, примером которых является sed.

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

Так, иногда во многих десятках, а то и сотнях, файлов требуется изменить одну-единственную строку, причём — одинаковым образом (например, заменить копирайт Васи Пупкина на Петю Лавочкина). И тут поможет sed, позволив выполнить изменение любого количества файлов в пакетном режиме.

Во всем блеске sed показывает себя при редактировании очень больших файлов (одно пролистывание которых требует немалого времени). А также — при редактировании сложных символьных последовательностей в нескольких файлах. Однажды, после очередной реконструкции моего сайта, передо мной встала задача тотальной модификации всех внутренних ссылок. Долго я с ужасом размышлял, как буду делать это в текстовом редакторе, и сколько ошибок при этом насажаю. Пока кот Манул не подсказал мне, как сделать это с помощью sed — быстро и, главное, безошибочно.

В самом общем виде sed требует двух аргументов — указания встроенной его команды и имени файла, к которому она должны быть применена. Впрочем, в качестве аргумента можно задать только простую команду, мало-мальски сложное действие (а команды поиска/замены принадлежат к числу сложных) необходимо определить через значения опции -e, заключённые в кавычки (одинарные или двойные — по ситуации). Что же касается имён файлов — то их в качестве аргументов можно указать сколько угодно, в том числе и с помощью масок типа *, *.txt и так далее. Правда, sed не обрабатывает содержимое вложенных подкаталогов, но это — дело поправимое (как — скоро увидим). Так что поиск и замена слова или их последовательности выполняются такой конструкцией:

$ sed -e 's/[old-url]/[new-url]/' *

Здесь s — это команда поиска, old-url — искомый текст, а new-url — текст для замены. В приведённой форме команда выполнит поиск и замену только первого вхождения искомого текста. Чтобы заменить текст по всему файлу, после последнего слэша (он обязателен в любом случае, без него sed не распознает конца заменяющего фрагмента) нужно указать флаг g (от global). Важно помнить, что если оставить заменяющее поле пустым, искомый текст будет просто удалён.

По умолчанию sed выводит результаты своей работы на стандартный вывод, не внося изменений в файлы аргументы. Так где же здесь редактирование? Оно обеспечивается другой опцией — -i, указание которой внесёт изменения непосредственно в обрабатываемый файл. В результате команда для замены, например, протокола HTTP на HTTPS в ссылках во всех файлах текущего каталога будет выглядеть так:

$ sed -i -e 's/http/https' *

А что делать, если таким же образом нужно обработать файлы во всех вложенных подкаталогах? Придётся вспомнить об универсальной команде find, описанной ранее. В форме

$ find . -name * -exec sed -i -e 's/http/https' * {}

она с успехом справится с этой задачей.

Я привёл лишь элементарные примеры использования sed. На самом деле возможности его много шире — за деталями, как обычно, следует обратиться к документации.

Книга о Cintu. Часть 2. Применение системы. Глава 9. Работа с текстами средствами CLI: 3 комментария

  1. Алексей, доброго времени суток!

    Какая-то не состыковка, вроде бы ищем файлы с расширением txt…
    >> $ find ~/ -name \*.txt -exec cp {} textdir \;
    и далее по тексту:
    >> В результате все png-файлы будут изысканы и скопированы
    ——————————
    Очепятка в слове «сравнить»:
    >> есть и команда diff3, позволяющая сранить именно три файла,
    ——————————
    Пропущен последний слэш
    >> $ sed -i -e ‘s/http/https’ *
    и здесь
    >> $ find . -name * -exec sed -i -e ‘s/http/https’ * {}
    —————————
    >> команда для замены, например, всех вхождений html на shtml во всех файлах
    Насколько я помню shtml — это расширение html-файлов, которые используют технологию SSI. Учитывая последующий контекст в статье «$ sed -i -e ‘s/http/https/’ *» правильнее будет написать:
    «команда для замены, например, протокола HTTP на HTTPS в ссылках во всех файлах». Либо заменить примеры команд на: « sed -i -e ‘s/\.html/\.shtml/’ *» и чуть ниже «$ find . -name * -exec sed -i -e ‘s/\.html/\.shtml/’ * {}»

    По опыту использования sed, могу сказать что, в solaris sed некоторые опции вообще не воспринимаются, которые нормально работают в той же Ubuntu. Приходилось писать длинный код с использованием промежуточных файлов, чтобы заменить кусок текста. Как подсказали старшие товарищи надо было использовать команду gsed. Но это так, небольшое лирическое отступление.

  2. Ну у тебя просто глаз-алмаз! Ещё раз спасибо!
    Это контаминация написанного не помню когда и вытащенного из истории команд недавнего времени, отсюда и нестыковки.
    Пошёл исправлять 🙂

  3. А по поводу лирического отступления — собственно в Solaris’е наверное изначальный sed, который, видимо, от GNU sed’а отличается. В Opensolaris, когда он ещё был, помнится, был GNU sed….

Добавить комментарий