Глава 2. Доступ к файлам

Введение
2.1. Поиск файлов
2.1.1. tree - визуализация файлового дерева
2.1.2. thead - печать начала каждого файла
2.1.3. tgrep - поиск строк в дереве файловой системы
2.1.4. paths - нахождение пути доступа к исполняемым файлам, со специальными опциями
2.2. Вывод информации
2.2.1. lc - вывод файловой информации на экран по столбцам
2.2.2. ll - вывод файловой информации в длинном формате
2.2.3. kind - вывод однотипных файлов
2.2.4. m - простой доступ к команде more
2.2.5. mmm - обработка программой nroff макрокоманд для рукописей
2.2.6. pall - печать всех файлов в дереве

ВВЕДЕНИЕ

В главе 1 был представлен обзор общей структуры системы UNIX и показано, как взаимодействуют ее различные части. Это похоже на введение в географию, когда на глобусе показывают континенты и крупные водные пространства. Такая информация, хотя и является хорошим фундаментом для общих знаний, вряд ли поможет найти наилучший путь из Сан-Франциско в Лос-Анжелес. Необходим следующий уровень детализации: названия поселений, дорог, развилок, улиц, адресов.

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

Будучи пользователями UNIX, все мы научились пользоваться основными командами файловой информации, как, например, ls с различными опциями. Мы знаем, как перемещаться между каталогами и копировать или перемещать файлы. Тем не менее, находить нужную информацию о файлах из всей массы информации не так-то легко. Нам необходимо создать инструментальные средства, которые используют древовидную структуру файлов в UNIX, чтобы находить то, что мы ищем, и, соответственно, выводить информацию о файлах на экран, печатать листинги содержимого файлов и т.д.

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

КОМБИНИРОВАНИЕ ПРОДУКТИВНЫХ ИДЕЙ

Две концепции являются общими почти для всех файловых инструментальных средств. Первая - это идея рекурсивного поиска, которая означает, что некоторые команды системы UNIX (например, find) просматривают все файловое дерево, начиная с некоторой заданной начальной точки (или с текущего каталога). Если в данном каталоге встречается подкаталог, то его содержимое тоже исследуется - и так далее вниз к самому нижнему под-подкаталогу. Так проходятся маршруты ко всем файлам в целом дереве.

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

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

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

ПОИСК ФАЙЛОВ

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

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

Другое инструментальное средство - это thead. Thead печатает несколько первых строк текстовых файлов, которые находятся в данном сегменте файлового дерева. Просматривая заголовок, т.е. первые несколько строк файла, вы можете получить достаточно информации, чтобы идентифицировать содержимое файла. При вызове thead вы можете явно задать каталог либо передать команде thead по конвейеру список полных имен файлов. Это делает команду thead фильтром - особым видом команд системы UNIX, который мы обсудим позже.

Следующее инструментальное средство - tgrep. Как следует из названия, это еще одна команда, связанная с файловым деревом, которая использует утилиту grep. Tgrep ищет символьные строки в каждом файле, который находится в данном сегменте файлового дерева. Tgrep также является фильтром, так что имена файлов можно передавать ей по конвейеру.

В нашем последнем проекте в этом разделе мы обратимся к использованию каталогов как средства "навигации". Сначала мы опишем основной алгоритм для утилиты, которая для каждого файла из заданного списка файлов проверяет, находится ли этот файл в каком-либо каталоге по указанному маршруту поиска. Затем мы построим paths - утилиту, которая дополняет функцию поиска полезными опциями.

РАСПЕЧАТКА ФАЙЛОВОЙ ИНФОРМАЦИИ

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

Первые два командных файла являются пре- и постпроцессорами для команды ls. Команда lc выводит файловую информацию по столбцам, команда ll перечисляет файлы в длинном формате. Эти командные файлы дополнены опциями команды ls, чтобы сделать распечатки более информативными. Так как команда ls используется довольно часто, упаковка наиболее часто применяемых нажатий клавиш в командные файлы представляется целесообразной. Упаковка уменьшает количество постоянно набираемых символов и упрощает использование команд, исключает необходимость запоминания подробного синтаксиса.

Третье инструментальное средство - это kind. Kind - еще один командный файл препроцессорного типа, использующий команду UNIX file. Команда file читает указанный файл и затем сообщает, является ли этот файл текстовым, архивным или исполняемым. Поскольку распечатки команды file не выбирают файлы заданного типа, возникает необходимость в создании для этого специальной утилиты. Команда kind работает с распечаткой команды file. Kind выводит на экран имена файлов только заданного типа.

Еще один командный файл - m, который облегчает работу со стандартной командой more системы UNIX, уменьшая количество необходимых для запуска команды символов и упрощая интерфейс. Делается это без потери гибкости: так же, как вы можете использовать команду more для файла или передать команде more данные по программному каналу, вы можете сделать то же самое для m.

Следующий командный файл - это mmm. Он состоит из одной заготовленной командной строки для программы nroff системы UNIX. Существует много способов вызова команды nroff и множество различных опций к ней. Если же вы редко используете nroff, у вас могут возникнуть трудности в запоминании специфических опций, необходимых для вашей работы с командой. Эти проблемы отпадут, если у вас есть команда mmm. Определите опции, которые вы обычно используете, и введите их в командный файл mmm (о том, как это сделать практически, речь пойдет ниже). Теперь достаточно набрать mmm - и вы имеете возможность работать с вашей командой nroff.

Последняя утилита - pall. Pall обходит файловое дерево, ведя поиск файлов заданного типа, и готовит их к выводу на принтер. Команда pr системы UNIX используется для разбивки на страницы всех файлов вместе и включения заголовков. Эта команда предлагает на рассмотрение принтеру один большой файл и наиболее полезна в тех случаях, когда у вас имеется множество каталогов с текстовыми файлами или с исходными файлами программ.

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

2.1. ПОИСК ФАЙЛОВ

2.1.1. tree - визуализация файлового дерева


 ИМЯ: TREE

tree - вывод на экран структуры файлового дерева

НАЗНАЧЕНИЕ

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

ФОРМАТ ВЫЗОВА
 tree [dir]
ПРИМЕР ВЫЗОВА
 $ tree $HOME

Выводит структуру файлового дерева регистрационного каталога.

ТЕКСТ ПРОГРАММЫ
 1 :
 2 # @(#) tree v1.0 Visual display of a file tree Author: Russ Sage
 2а     вывод на экран структуры файлового дерева
 
 4 if [ "$#" -gt 1 ]
 5 then echo "tree: wrong arg count">&2
 6   echo "usage: tree [dir]" >&2
 7   exit 2
 8 fi
 9 if [ "$#" -eq 1 ]
 10 then if [ ! -d $1 ]
 11 then echo "$0: $1 not a directory">&2
 12  echo "usage: tree [dir]"  >&2
 13  exit 2
 14   fi
 15 fi
 
 17 find ${1:-.} -print | sort | sed -e "1p" -e "1d"   
 18         -e "s|[^/]*/|  /|g" 
 19         -e "s|[^ */|/|"   
 20         -e "s|/([^/]*)$|1|"
ОПИСАНИЕ

Зачем нам нужен командный файл tree?

Как мы уже отмечали, вся система UNIX строится вокруг файловой системы, которая похожа на дерево. Дерево, с которым мы работаем в системе UNIX, растет вверх ногами: корень находится вверху, а ветви и листва растут вниз от корня. Физическая структура реальных деревьев и файловых деревьев, используемых в системе UNIX, очень сходна: один корень (начальная точка) и один ствол. Как глубоко и как далеко могут уходить ветви от основного ствола - не ограничивается ничем, кроме ограничений физического пространства. Аналогично, число листьев, которые может иметь каждая ветвь, фактически не ограничено.

Многое в системе UNIX задумано для того, чтобы приспособиться к дереву. Некоторые команды обходят дерево и сообщают о его компонентах, но обычно их сообщения выдаются в форме, не очень удобной для чтения человеком. Это делает командные файлы весьма мощными инструментами. Перевести необработанные, недружественные сообщения командных файлов в удобный, информативный вид довольно легко.

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

Что делает tree?

Команда tree - это постпроцессор для команды UNIX find. Find просматривает сегмент файлового дерева и полные имена всех файлов, которые соответствуют заданному критерию. Команда tree использует утилиту sed системы UNIX, чтобы перевести выход команды find в наглядную форму.

Входным параметром для команды tree является имя каталога, которое может быть указано в любом абсолютном виде, например, /usr/spool/uucp, или в относительном, например, ../../bin. Если никакого имени не указано, подразумевается ., что является текущим каталогом.

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

Рассмотрим пример структуры каталога. Пусть корневым каталогом будет /tmp с двумя каталогами: a и b. В каталоге a находится подкаталог aa, который содержит файл file1, а в каталоге b , соответственно, подкаталог bb, содержащий файл file2. Команда find выдаст распечатку такого вида:

 # find /tmp -print
 /tmp
 /tmp/a
 /tmp/a/aa
 /tmp/a/aa/file1
 /tmp/b
 /tmp/b/bb
 /tmp/b/bb/file2

Как видно из этого листинга, файлы a и aa есть каталоги, а файл file1 находится внизу файлового дерева. Сравните этот результат с результатом, который выдает команда tree, используя утилиту sed.

 # tree /tmp
 /tmp
 /  a
 /  /  aa
 / /  /  file1
 / b
 / /  bb
 / /  /  file2

Корневым каталогом в этом листинге является каталог /tmp. Там, где дерево переходит на более глубокий уровень, печатаются только символы косой черты. Первый уровень - /tmp, под этим уровнем находятся файлы-каталоги a и b, затем, соответственно, их подкаталоги aa и bb. Исходя из этого листинга, мы делаем вывод, что на первом уровне каталога находятся два файла (и эти файлы в действительности являются каталогами) и что два файла находятся в подчиненных каталогах. Отметим, что мы смогли идентифицировать aa и bb как каталоги только потому, что в них присутствуют файлы file1 и file2.

Сравните этот листинг с выходом "необработанной" команды find. Выход команды tree исключает отвлекающее внимание повторение элементов путей доступа при каждом переходе к более низкому уровню. Благодаря этому, сразу же видно СУЩЕСТВЕННУЮ информацию. Вот что мы имеем в виду, когда говорим о создании более наглядного для человека интерфейса с системой UNIX.

ПРИМЕРЫ
 1. $ tree

Использует подразумеваемый каталог (текущий каталог, что равносильно команде "$ tree .") в качестве начала файлового дерева.

 2. $ tree /

Печатает древовидный листинг для КАЖДОГО файла всей системы. Команда find при таком ее запуске начинает с корневого каталога и выдает информацию о всех файлах системы.

 3. $ tree $HOME/..

Показывает древовидный формат для всех других пользователей системы (предполагается, что все пользовательские каталоги находятся в одном и том же каталоге, например /usr/*).

ПОЯСНЕНИЯ

Первая строка содержит только знак двоеточия (:) - "нулевую команду". Это связано с тем, что все командные файлы, описываемые в этой книге, сделаны так, чтобы их можно было запускать в среде интерпретатора Bourne shell. Наш комментарий в строке 2, идентифицирующий версию, начинается со знака решетки (#). Си-shell ищет этот знак как первый знак командного файла. Если он найден, то предпринимается попытка выполнить данный командный файл. В противном случае Си-shell передает командный файл интерпретатору Bourne shell. Вот почему мы не хотим начинать первую строку со знака #. Мы, конечно, могли бы оставить первую строку чистой, но чистая строка невидима и может быть случайно удалена. Соответственно мы будем использовать чистые строки в других случаях, чтобы выделить важные участки программы.

Строка 2 идентифицирует версию. Символьная строка @(#) есть специальная последовательность в строке комментария, которая распознается как строка "what" ("что"). Команда what в системе UNIX читает файл и печатает сообщение, которое следует за строкой "what". Чтобы идентифицировать версию данного командного файла, наберите

 # what tree
и будет напечатано следующее сообщение:
 tree:
 tree v1.0 Visual display of a file tree Author: Russ Sage

Строки 4-7 проверяют, не слишком ли много аргументов было передано командной строке. Это осуществляется путем исследования переменной $#, которая представляет собой счетчик числа позиционных параметров командной строки. Если насчитывается более одного параметра, печатается соответствующее сообщение об ошибке в стандартный файл ошибок (stderr) и программа останавливается с плохим значением статуса.

Отметим, что команда echo обычно печатает в стандартный выход (stdout). Мы можем перенаправить stdout в другой файловый дескриптор, указав его. В данном случае мы собираемся печатать в stderr. Синтаксис переводится так: "вывести эту строку и перенаправить ее в файловый дескриптор (&) стандартного файла ошибок (2)". Печать сообщений об ошибках в stderr обеспечивает согласованное поведение командного файла независимо от среды, в которой он запущен.

Отметим также, что коды статуса выхода в интерпретаторе shell противоположны тем, которые используются при программировании на языке Си. В Си истинное значение есть 1, ложное отлично от 1. При программировании на языке shell успешным статусом выхода (истиной) является 0, а плохим статусом (ложью) ненулевое значение.

Вы, возможно, удивитесь, почему мы так беспокоимся о том, чтобы вернуть ложный статус выхода, если командный файл собирается просто напечатать сообщение об ошибке и прекратить работу. Дело в том, что все инструментальные средства системы UNIX должны быть спроектированы так, чтобы они могли быть связаны с другими командами и процессами, в которые они могут быть встроены. Возможно, что другой команде необходимо будет вызвать команду tree и проверить, корректно ли она отработала. Хорошим стилем проектирования программных средств является прогнозирование и разрешение многих способов использования программы.

Строки 9-15 проверяют, чтобы любые параметры, передаваемые командной строке, были действительно каталогами, как указано в нашем синтаксисе. Напомним, что в командной строке может быть помещен только один каталог. Если мы используем только один параметр и этот параметр не является каталогом, то мы печатаем сообщение об ошибке и выходим. Таким образом, операторы проверки гарантируют, что либо не используется ни один параметр, либо единственный используемый параметр является корректным каталогом.

Мы подошли к сердцу команды tree - это строки 17-20. Главным здесь является команда find системы UNIX. Каталог, в котором ведется поиск, определяется при запуске команды. Синтаксис ${1:-.} является формой параметрической подстановки и означает следующее: если $1 (что является первым позиционным параметром) установлен (иными словами, если аргумент был передан командной строке и был ненулевым), то нужно использовать это значение. В противном случае следует использовать каталог . (текущий каталог). Этот тип подстановки дает нам возможность запускать команду tree без указания имени каталога (когда после tree на командной строке ничего не следует),- то есть работать в режиме "по умолчанию", что часто используется в различных файловых инструментах.

Команда find выводит на печать полное имя каждого файла, который ей встречается. Поскольку не используется никакая специальная опция для селекции файлов, печатаются все имена. После этого все полные имена файлов сортируются для более удобного чтения. Такая сортировка несколько увеличивает время работы команды, однако наглядность результата говорит о том, что это время было потрачено с пользой.

Далее, отсортированные полные имена файлов передаются по программному каналу команде sed системы Unix. Sed - это "потоковый редактор", очень гибкое средство, которое может быть использовано для идентификации и обработки различных образцов текста. Опции -e являются операциями редактирования, применяемыми к поступающим данным. Первый оператор просто сообщает команде sed, что нужно напечатать первую строку, затем удалить строку 1. Это делается для того, чтобы напечатать название корневого каталога, который исследуется. Этой строке не требуется никакой дальнейшей модификации, так как корневой каталог не имеет никаких дополнительных элементов путей доступа, которые нужно было бы трансформировать в символы косой черты для показа отступов. Удаление первой строки связано с тем, что она не нужна в дальнейшей работе.

Вторая операция редактирования является командой подстановки. Она заменяет каждый символ, отличный от символа косой черты (вплоть до первого символа /) на последовательность пробелов и затем один символ (/). Это избавляет нас от печатания имен промежуточных каталогов впереди полного имени файла. Буква g в конце этой строки означает, что эта операция выполняется глобально, то есть для всех считываемых символов. Итак, теперь строка состоит из начального элемента пути и одной или более последовательностей пробелов, разделенных символами косой черты. Символы обратной косой черты () в конце операций редактирования - это символы продолжения, которые сообщают команде sed, что нужно продолжить работу со следующей строкой в текущем пакете операций редактирования.

Третья операция редактирования (строка 19) также является командой подстановки и заменяет каждый символ, который не является пробелом (вплоть до символа /) на "не символ" и один символ косой черты. Этот оператор удаляет пробелы из предыдущего результата редактирования и смещает символ в самую левую позицию. Это создает гнездовую индикацию, которую мы видели в предыдущем примере.

Последняя операция редактирования (в строке 20) заменяет символ косой черты и все отличные от него символы (до конца строки) просто на символы, отличные от /. Отметим, что это устраняет самый правый символ /, который присутствует в листинге команды find. В результате остается имя подчиненного файла, сдвинутое вправо.

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

2.1.2. thead - печать начала каждого файла


 ИМЯ: thead

thеаd Печатает заголовок (первые несколько строк) файлов.

НАЗНАЧЕНИЕ

Пройти файловое дерево и напечатать первые несколько строк каждого файла. Если не указан каталог, то thead действует как фильтр.

ФОРМАТ ВЫЗОВА
 thead [dir...]
ПРИМЕР ВЫЗОВА
$ find $HOME/src -name "*.c" -print | sort | thead

Печатает заголовки (первые несколько строк) всех моих исходных файлов на языке Си.

ТЕКСТ ПРОГРАММЫ
 1 :
 2 # @(#) thead v1.0 Prints head of files in tree Author: Russ Sage
 2а     Печатает заголовки файлов в дереве
 
 4 if [ "`echo $1|cut -c1`" = "-" ]
 5 then echo "$0: arg error"
 6  echo "usage: $0 [dir ...]"
 7  exit 1
 8 fi
 
 10 case $# in
 11 0) while read FILE
 12  do
 13   if file $FILE | fgrep text >/dev/null 2>&1
 14   then echo "
:::::::::::::::::::::"
 15     echo " $FILE"
 16     echo "
:::::::::::::::::::::"
 17     head -15 $FILE
 18   fi
 19  done;;
 20 *) for NAME in $*
 21  do
 22    find $NAME -type f -print | sort | wile read FILE
 23    do
 24     if file $FILE | fgrep text >/dev/null 2>&1
 25      then echo "
:::::::::::::::::::::"
 26       echo " $FILE"
 27       echo "
:::::::::::::::::::::"
 28       head -15 $FILE
 29     fi
 30    done
 31  done;;
 32 esac
ПЕРЕМЕННЫЕ СРЕДЫ ВЫПОЛНЕНИЯ

FILE

Содержит имя каждого файла

NAME

Имя каталога, заданное в командной строке

ОПИСАНИЕ

Зачем нужен командный файл thead?

Как уже объяснялось ранее в этой главе, иерархическая файловая система является очень значительной частью системы UNIX. Однако, только несколько команд в UNIX имеют дело непосредственно с рекурсивным поиском файлов. Единственный способ расширить возможности системы создать новые рекурсивные утилиты, работающие с файлами. В данном случае мы соединим нашу стратегию поиска по дереву с командой head системы UNIX для упрощения идентификации содержимого всех файлов в выделенном сегменте файлового дерева.

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

Что делает thead?

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

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

 $ thead $HOME

Если мы хотим просмотреть только исходные файлы на языке Си (*.c), то представленный выше синтаксис не годится для этого. Он не обладает достаточной гибкостью. Нам необходимо иметь способ указать (или квалифицировать) файлы в $HOME перед тем, как просматривать их. Так как команда thead может воспринимать полные имена файлов, мы можем использовать следующую команду:

 $ find $HOME -name "*.c" -print | sort | thead

Команда find генерирует список файлов с расширением C, который сортируется и подается по каналу на вход команде thead. Как видно из представленных двух примеров, весьма полезной для командного файла является возможность получать входные данные либо из аргументов командной строки (как в первом примере), либо по программному каналу (как во втором примере). Способность использовать программный канал позволяет вам применять какие-либо другие команды системы UNIX, которые могут отбирать входные данные для вашего командного файла. Команда с такой двойной возможностью называется ФИЛЬТРОМ. Среди стандартных команд системы UNIX вы найдете лишь несколько команд-фильтров, таких как wc, awk, sort.

Эти два способа поступления входных данных в программы делают интерфейс с командными файлами очень гибким. Мы можем подстраивать програмные средства под наши нужды, а не подстраивать наши желания под имеющееся программное обеспечение.

Аргументами для команды thead являются каталоги. Никаких опций, начинающихся со знака "-" нет, только каталог или полные имена файлов. Команда thead знает из синтаксиса, какой способ запуска команды будет использоваться. Если командная строка содержит имя файла, thead просмотрит все позиционные параметры. Если никакие имена не указаны, thead читает стандартный ввод (stdin) и останавливается, когда встречает EOF. (Такое бывает в случае, когда команда thead получает входные из программного канала.)

Для каждого файла, с которым работает thead, выполняется контроль - текстовый ли это файл. Применение команды head к исполняемым модулям приводит к выводу "таинственных" символов на экран и иногда может вызвать дамп оперативной памяти.

ПРИМЕРЫ
 1. $ thead /etc

Печатает данные из каждого текстового файла, находящегося в каталоге /etc. Очень полезная команда, так как большинство файлов в /etc являются исполняемыми модулями. Удобно иметь возможность быстро изолировать текстовые файлы.

 2. $ thead /usr/include

Просматривает все подключаемые файлы (*.h), даже в системном подкаталоге sys.

 3. $ find $HOME -ctime 0 -print | thead

Ищет все файлы в вашем регистрационном каталоге, которые были изменены в течении последних 24 часов. Для каждого файла проверяется, текстовый ли он. Если файл текстовый, то он печатается.

ПОЯСНЕНИЯ

Строки 4-8 выполняют проверку ошибок. Так как команда thead не имеет никаких опций, любые позиционные параметры, которые начинаются с дефиса (-) являются неверными. Если первым символом первого позиционного параметра оказывается "-", то печатается сообщение "argument error" (ошибка аргумента) вместе с сообщением о способе запуска и команда thead прекращает работу.

Некоторые приемы программирования для интерпретатора shell, используемые в этих строках, довольно часто встречаются в данной книге, поэтому имеет смысл остановиться на них подробнее. Проанализируем строку 4, работающую изнутри наружу. Команда echo выдает содержимое $1 (текущий параметр командной строки), которое передается по программному каналу команде cut. Команда cut используется для того, чтобы выделить определенные символы или группы символов из строки. В данном случае опция -c1 используется для получения только первого символа.

КОМАНДА cut ДЛЯ BSD

В системе BSD нет команды cut, но следующий командный файл все же "вырезает" первое непустое поле в текущем аргументе. Предположим, мы используем команду для генерации целого набора строк. В данном случае это команда who:

 for NAME in 'who | sed "s/^([^ ]*).*/1/"'
 do
 done

Для каждой обнаруженной строки (аргумента) команда sed должна подставить вторую строку символов вместо первой строки. Первая строка - это строка, которая вырезается. Мы ищем от начала строки (^) символ, отличный от пробела ([^ ]), за которым следует любое число непустых символов (*). Эта операция прерывается по достижении пробела. Набор непустых символов ограничивается обратными косыми чертами ( и ). Впоследствии ссылка на этот набор дается в виде 1. Символы .* означают, что после того, как найден пробел, необходимо считать подходящими все символы до конца строки. Мы находимся фактически сразу после того, что заключено в пару символов ( и ). Группируя первый набор символов, отличных от пробела, мы получаем то, что является результатом работы команды "cut -f1".

В этом месте мы подходим к знакам ударения (`), окаймляющим все выражение. Они берут результат работы всех команд, заключенных в знаки ударения и передают на следующую охватывающую структуру в наших вложенных выражениях. Этот следующий уровень окаймления указан кавычками. Кавычки превращают символ в строку, чтобы его можно было сравнить с символом "-". Следующий слой - квадратные скобки, указывающие условие для оператора if. Это приводит к тому, что генерируется нулевое (истина) или ненулевое (ложь) условие, которое управляет тем, будет ли выполнена часть then оператора if-then.

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

Остальная часть командного файла представляет собой один огромный оператор выбора (case). Аргументом, используемым для ветвления, является число позиционных параметров в командной строке. Если позиционных параметров нет, то в строках 11-19 активируется цикл while. Заметим, что цикл while выполняет оператор чтения, но не указывает, откуда дол- жен быть взят его вход. Это связано с тем, что входом по умолчанию является стандартный ввод (stdin). Для каждого имени файла, которое читается из стандартного ввода, запускается команда file системы UNIX. Выход команды file передается по программному каналу команде fgrep (а не grep, что увеличивает скорость), чтобы посмотреть, является ли файл текстовым.

Фактический выход команды fgrep перенаправляется на нулевое устройство (в бесконечную область памяти), поскольку он нам не нужен. Нас интересует лишь код возврата после выполнения всего конвейера. Если команды file и fgrep отработали успешно, кодом возврата является ноль. Это истинное значение, поэтому выполняется участок цикла после then (строки 14-17). Если файл не существует или не является текстовым, то код возврата ненулевой, и условный оператор завершается. Это приводит нас в конец цикла, выполняется следующая итерация цикла while и мы рассматриваем следующий аргумент из стандартного ввода.

Теперь рассмотрим обработку, выполняемую по then (строки 14-17). Для каждого файла, который является текстовым, печатается строка двоеточий (:) до и после имени файла, а команда head системы UNIX печатает первые 15 строк. Такой сценарий продолжается, пока не закончатся данные в стандартном вводе.

Рассмотрим другую альтернативу, покрываемую данным оператором выбора. Она обрабатывает ситуацию, когда имеется несколько позиционных параметров (что указано символом * в операторе case). Цикл for пробегает все параметры (строка 20). Звездочка (*) в операторе case означает, что подходит любое значение, которое не подошло ранее. Это улавливающая (catchall) опция. Цикл for использует аргумент $* в качестве своего входа. Он представляет значения всех позиционных параметров, что является фактически всей командной строкой, исключая имя утилиты. Команда find используется для поиска всех нормальных файлов в каталоге. "Нормальные" файлы не означает "только текстовые файлы", поэтому мы проверим это позже. Выход команды find передается по каналу команде sort, чтобы сделать его более наглядным. Отсортированный список передается по каналу в цикл while, который помещает имя файла в переменную FILE (строка 27). Проверяется, текстовый ли файл, затем он печатается командой head.

Если мы сравним строки 13-18 и строки 24-29, то мы увидим, что это один и тот же код. В большинстве языков программирования это означало бы, что мы должны оформить эти строки как процедуру и вызывать ее, когда нужно. Язык программирования интерпретатора shell, хотя и довольно мощный, не имеет хорошего способа реализации процедур. Последний интерпретатор shell в System V имеет функции, которые позволяют решить эти проблемы. Отметим, что внутренний цикл while повторяется на каждом файле, который существует в определенном каталоге, а внешний цикл for проходит от каталога к каталогу.

ВОЗМОЖНЫЕ МОДИФИКАЦИИ

Для увеличения гибкости хорошо бы добавить опции, чтобы вы могли переходить на команду find непосредственно из thead. Полезными аргументами были бы -name для изолирования образцов имен файлов и -ctime для обработки изменений, связанных со временем.

Еще одной привлекательной особенностью было бы добавление опции грамматического разбора (основанной на -) и опции -n, указывающей, что из команды head должно быть напечатано n строк.

ВОЗМОЖНЫЕ ИССЛЕДОВАНИЯ

В чем отличие между двумя следующими операторами?

 $ find $HOME -name "*.c" -print | thead
 и
 $ find $HOME -name "*.c" -exec head {} ;

Они выглядят очень похоже, и они действительно похожи. Они обрабатывают одни и те же файлы и печатают одни и те же данные из каждого файла. Основное отличие в том, что строка, которая использует thead, печатает хорошее оформление вокруг имени файла, а чистая команда find печатает непрерывный поток текста так, что очень трудно определить, какой файл вы просматриваете.

2.1.3. tgrep - поиск строк в дереве файловой системы


 ИМЯ: tgrep

tgrep Поиск строки по шаблону в дереве файлов

НАЗНАЧЕНИЕ

Обходит файловое дерево и ищет в каждом файле указанную строку. Если не указан никакой каталог, tgrep действует как фильтр.

ФОРМАТ ВЫЗОВА
 tgrep [-c|-h] string [file ...]
ПРИМЕР ВЫЗОВА
 # tgrep "profanity" /

Поиск слова "profanity" по всей системе (суперпользователь снова на тропе войны!)

ТЕКСТ ПРОГРАММЫ
 1 :
 2 # @(#) tgrep v1.0 Search for string in tree Author: Russ Sage
 2а     Поиск строки в дереве
 
 4 OPT=""
 
 6 for ARG in $@
 7 do
 8   if [ "`echo $ARG|cut -c1`" = "-" ]
 9    then case $ARG in
 10     -c) OPT="-name "*.c""
 11      shift;;
 12     -h) OPT="-name "*.h""
 13      shift;;
 14     *) echo "$O: incorrect argument"    >&2
 15      echo "usage: $O [-c|-h] string [file ...] >&2
 16      exit 1;;
 17     esac
 18   fi
 19 done
 
 21 case $# in
 22 0) echo "$O: argument error"     >&2
 23  echo "usage: $O [-c|-h] string [dir ...]" >&2
 24  exit 2
 25  ;;
 26 1) while read FILE
 27  do
 28    grep -y "$1" $FILE /dev/nul
 29  done
 30  ;;
 31 *) STRING=$1; shift
 32  eval find "$@" -type f $OPT -print | sort | while read FILE
 33  do
 34    grep -y "$STRING" $FILE /dev/null
 35  done
 36  ;;
 37 esac
ПЕРЕМЕННЫЕ СРЕДЫ ВЫПОЛНЕНИЯ

FILE

Содержит имя каждого файла

OPT

Содержит специальные опции команды find

STRING

Временная переменная, в которой содержится строка поиска

ОПИСАНИЕ

Зачем нам нужен командный файл tgrep?

Как мы могли видеть на примере двух предыдущих утилит, рекурсивный просмотр файлов очень полезен. Он сохраняет время, поскольку позволяет избежать поиска файлов вручную, а также создает средства, которые могут быть использованы в более мощных утилитах. Чем больше имеется созданных нами средств, тем больше новых средств мы можем построить с их помощью. Единственная проблема заключается в том, что вы должны позаботиться об их взаимозависимости (каким утилитам или средствам требуются другие утилиты или средства и кто на кого влияет). Еще одна область, где UNIX не имеет "родной" рекурсивной команды - это обработка строк. Семейство команд типа grep очень велико, но все они работают только по одному фиксированному маршрутному имени файла. Нам необходим препроцессор для команды grep. Правда, мы можем дать запрос на все файлы во всей системе или какой-либо ее части по нашему выбору. Если мы попытаемся сделать это вручную, то это означает, что мы должны много раз нажимать на клавиши, что может привести к синтаксической ошибке. Вы также должны точно помнить, как вы создавали командную строку, если вы в следующий раз захотите выполнить такую же задачу. Зачем выполнять грязную работу? Для этого существует компьютер.

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

Что делает tgrep?

Основным предназначением tgrep является обеспечение большей гибкости и легкости использования возможностей команды grep. Ее синтаксис точно такой же, как и у grep, за исключением допустимых типов файлов. В команде grep UNIX в качестве аргумента может указываться практически любой файл, но указание текстового файла имеет наибольший смысл. В команде tgrep также могут использоваться текстовые файлы, но наибольший смысл имеет указание каталогов, поскольку мы ищем имена файлов. Команда find работала бы не очень хорошо, если бы пыталась извлечь множество имен файлов из текстового файла. В командной строке может указываться множество имен каталогов, поскольку все они используются как начальное место поиска оператора find.

По умолчанию tgrep находит все обычные файлы. В этой программе нет никакой проверки на то, текстовый файл или нет, поскольку мы не пытаемся напечатать все на экран. Поэтому мы ищем строки символов во всех файлах, начиная от архивных и заканчивая исполняемыми. Если вы хотите выбрать типы файлов, используйте две опции, -c и -h. Опция -c заставляет команду UNIX find искать только файлы с именами вида *.c. Аналогично, опция -h соответствует файлам *.h. Эти опции могут быть полезны для управления программами на языке Си, с которыми мы более детально познакомимся в главе 4. Эти опции не самое важное, но они показывают, как легко добавить новые опции в программу. Если при вызове была указана какая-то иная опция, кроме упомянутых, выводится сообщение об ошибке и программа останавливается. Подобно thead, tgrep является фильтром.

ПРИМЕРЫ
 1. $ tgrep unix $HOME

Поиск любого вхождения слова unix во всех файлах моего регистрационного каталога.

 2. $ tgrep -c "^sleep()$" $HOME/src

Поиск выражения (начало строки, символьная строка, конец строки) во всех исходных файлах на языке Си в регистрационном каталоге с исходными текстами (опция -c).

 3. # find /usr/src -name "*.c" -print | tgrep "ioctl"

Поиск всех вызовов ioctl в исходных Си-файлах, начиная с каталога /usr/src. (Обратите внимание, что я являюсь суперпользователем. Это видно из того, что я занимаюсь поиском в ограниченной части системы, а именно в исходных дистрибутивах, а также из того, что в качестве символа приглашения используется символ "#".)

 4. $ tgrep "| more" `find . -type f -print`

Поиск символа вертикальной черты (|), после которого следует слово more, в списке имен файлов, генерируемом оператором find. Find печатает имена всех файлов текущего каталога и всех подкаталогов, которые являются обычными файлами.

 5. $ tgrep trap /bin /usr/bin /etc

Поиск команды прерывания (trap) во всех командных файлах интерпретатора shell, которые имеются в трех каталогах.

ПОЯСНЕНИЯ

В строке 4 переменная OPT, в которой хранятся необязательные команды оператора find, инициализируется в нулевое значение.

Строки 6-18 выполняют проверку на наличие ошибок. Проверяется, является ли первым символом каждого позиционного параметра символ "-". Если проверка успешна, то проверяется на корректность сам аргумент. Возможными опциями являются -c и -h. Если указано что-либо другое, выводится сообщение об ошибке и программа завершается. Обратите внимание, что с помощью команды shift допустимые опции удаляются из командной строки. Это сделано для того, чтобы впоследствии выражение $@ могло быть использовано для получения только аргументов строки поиска и маршрута. Выражение $@ является другой формой выражения $ *, в которой оно распространяется на все позиционные параметры. Однако в последующем тексте мы увидим одно большое отличие между ними.

Еще один трюк сделан при присвоении значения переменной OPT в строке 10. Этой переменной нам необходимо присвоить значение, которое включает в себя пару кавычек, поскольку кавычки должны быть частью строки поиска, которая в конце концов используется оператором find. Однако обнаружение второй кавычки обычно ЗАВЕРШАЕТ оператор присваивания, а мы этого в данном случае не хотим. Выходом из ситуации является использование символа , который отменяет специальное значение следующего за ним символа. В данном случае специальное значение было бы концом строки присваивания.

Таким образом, кавычка перед звездочкой (*) в строке 10 рассматривается как часть значения переменной OPT, вместо того, чтобы завершать операцию присваивания. Следующая встреченная кавычка также должна быть сохранена в значении переменной, чтобы указать конец хранимой здесь строки поиска, поэтому она также экранирована символом . Последняя кавычка в этой строке не экранирована обратной косой чертой. Эта кавычка соответствует своей обычной функции в смысле интерпретатора shell и завершает оператор присваивания.

Вам нужно поэкспериментировать с использованием кавычек, чтобы понять его. Когда какой-то командный файл не желает работать правильно, но ошибок не видно, проверьте правильность использования кавычек. Оставшаяся часть командного файла - это оператор case (строки 21-37), который имеет дело с числом аргументов командной строки. Если нет никаких аргументов, печатается сообщение об ошибке и мы выходим из программы. Мы ведь не можем делать никакого поиска командой grep, если мы даже не знаем, какую строку нужно искать.

Если указан только один аргумент, то это должна быть строка поиска. Это также означает, что в командной строке не указаны имена файлов и поэтому мы читаем их со стандартного устройства ввода, т.е. командный файл действует как фильтр. Цикл while (строки 26-29) читает со стандартного ввода имя каждого файла для поиска в нем командой grep нужной строки. Опция -y команды grep означает нечувствительность к регистру символов при поиске. Использование этой опции дает нам хорошие шансы попасть на нужную строку.

Другой интересной особенностью этого цикла являются две команды grep в строках 28 и 34. Мы ищем нужную строку не только в файле, но также в каталоге /dev/null. Это кажется странным? Да. Проблема заключается в самой команде grep. Grep знает, когда в командной строке указано более одного файла. Если имеется более одного файла, то имя файла выводится на экран до вывода найденной строки. Если же в командной строке указан только один файл, то его имя не выводится, поскольку считается, что пользователь должен помнить это имя. Это создает проблемы при написании командных файлов, когда вы имеете много имен файлов, но они обрабатываются по одному. Мы обрабатываем файлы по одному, поскольку если бы мы использовали указание группового имени файлов с помощью метасимволов, то могло бы оказаться слишком много имен файлов в одной командной строке для команды grep. Поэтому вместо использования некоторой замысловатой схемы для сокращения количества аргументов, мы избегаем этого путем обработки в цикле каждого файла по очереди.

Поскольку на самом деле мы ищем большое количество разных строк, то мы хотим видеть имена файлов, в которых они были найдены. При указании в командной строке grep каталога /dev/null, grep всегда печатает имя файла до вывода найденной строки в нашем цикле, поскольку теперь имеется два имени файла в качестве аргументов. Конечно, в /dev/null ничего нельзя найти, поскольку он пуст по определению. Последний образец в операторе case (строки 31-36) - это ловушка. Он соответствует любому количеству позиционных параметров сверх одного, что позволяет нам иметь переменное число каталогов в командной строке.

Даже хотя мы можем указать несколько каталогов в командной строке, первым аргументом по-прежнему является строка поиска. Не так легко сказать: "начиная со второго параметра", поэтому мы сохраняем строку поиска (в переменной STRING - Прим. перев.) и убираем ее командой shift. Теперь мы можем получить доступ к остальной части командной строки с помощью выражения $@.

Давайте посмотрим, что происходит, когда мы ссылаемся на переменную $OPT для получения опций команды find. Допустим, мы вызвали tgrep с опцией -c. Когда мы присваиваем значение переменной OPT, мы ставим строку c в виде "*.c" внутри двойных кавычек, поскольку мы не желаем, чтобы shell раскрывал эту строку (т.е. трактовал ее как имена всех файлов, соответствующих данному образцу) именно сейчас. Теперь, как только к переменной $OPT есть обращение в команде find, значением OPT является *.c, что означает поиск файлов, символьные имена которых соответствуют *.c. Для отмены символьной интерпретации мы должны использовать команду eval. Перед выполнением команды find команда eval заставляет shell повторно анализировать команду в отношении распространения значения переменной. На этом проходе выражение "*.c" превращается в *.c, что разрешает генерацию имен файлов таким образом, чтобы были просмотрены все эти файлы.

Указывая $@ в команде find, мы можем производить поиск во всех каталогах сразу. Таким образом, нам нужно вызвать find только один раз, что позволяет сберечь время центрального процессора. Одной из интересных проблем, возникших при разработке данного командного файла было то, что выражение вида $* не работало. В команде find возникала ошибка сохранения. Даже запуск shell'а в режиме -x (установленный флаг разрешения выполнения) не разрешил проблему. Изменение синтаксиса, кажется, помогло разобраться с ней. Оказывается, причина в том, что выражение $* раскрывается в "$1 $2 ...", в то время как выражение $@ превращается в "$1" "$2" (т.е. в отдельные аргументы). Происходило то, что выражение $* передавало имена нескольких каталогов команде find как одну строку. Команда find не могла обнаружить файл такого типа, поэтому прекращала выполнение. Когда же вместо этого было использовано выражение $@, команда find получила несколько независимых строк с именами. Это вполне подошло команде find, и все заработало. Такие мелкие детали всегда требуют много времени для изучения!

ВОЗМОЖНЫЕ ИССЛЕДОВАНИЯ

В чем разница между двумя следующими операторами?

 grep "$1" `find "$2" -print`
 и
 find "$2" -print | while read F
 do
  grep "$1" $F
 done

Они кажутся совершенно похожими, но существует различие в главном. Первый оператор - это один вызов команды grep. Аргументами являются множество имен файлов, поставляемых командой find. Если find сгенерирует слишком много имен файлов, то выполнение команды завершится. О том, что сгенерировано слишком много имен файлов, никакого предупреждения не выдается, но большое количество файлов фатально для grep. Поэтому мы должны рассматривать такой синтаксис как недопустимый в общем случае.

Второй оператор - это цикл. Он работает медленнее и вызывает grep много раз, что забирает много времени центрального процессора. Однако положительным моментом является то, что цикл получает данные по конвейеру, который фактически не имеет ограничений на число данных, которое он может иметь. Наша программа никогда неожиданно не прекратит выполнение.

Обновлено: 12.03.2015