Книга о 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
— построитель и исполнитель командной строки со стандартного ввода. А поскольку на стандартный ввод может быть направлен вывод команды find
— xargs
воспримет результаты её работы как аргументы какой-либо команды, которую, в свою очередь, можно рассматривать как аргумент её самоё (по умолчанию такой командой-аргументом является /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 (на один экран вперед), Escape—V (на один экран назад), 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
Ещё одно замечательное свойство команды grep
(и egrep
) — возможность получения шаблона не со стандартного ввода (то есть не путём набора его с клавиатуры), а из файла. Так, если для приведённого выше случая создать простой текстовый файл 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
. На самом деле возможности его много шире — за деталями, как обычно, следует обратиться к документации.
3 thoughts on “Книга о Cintu. Часть 2. Применение системы. Глава 9. Работа с текстами средствами CLI”
Алексей, доброго времени суток!
Какая-то не состыковка, вроде бы ищем файлы с расширением 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. Но это так, небольшое лирическое отступление.
Ну у тебя просто глаз-алмаз! Ещё раз спасибо!
Это контаминация написанного не помню когда и вытащенного из истории команд недавнего времени, отсюда и нестыковки.
Пошёл исправлять 🙂
А по поводу лирического отступления — собственно в Solaris’е наверное изначальный sed, который, видимо, от GNU sed’а отличается. В Opensolaris, когда он ещё был, помнится, был GNU sed….