Текст книги "UNIX — универсальная среда программирования"
Автор книги: Брайан Керниган
Соавторы: Роб Пайк
сообщить о нарушении
Текущая страница: 12 (всего у книги 31 страниц) [доступный отрывок для чтения: 12 страниц]
4.5 Хорошие файлы и хорошие фильтры
Несмотря на то что в качестве примеров использования языка awk
приводились независимые программы, в большинстве случаев его применяют для написания простых программ в одну или две строки, являющихся фильтрами в больших конвейерах. Это справедливо для большинства фильтров: редко поставленная задача может быть решена с помощью одного фильтра, чаще она разбивается на подзадачи, где фигурируют несколько фильтров, объединенных в конвейер. Такую реализацию программных компонентов называют основным принципом организации программного мира UNIX. Фильтры буквально "пронизывают" всю систему, и очень важно понимать причины этого.
Программы UNIX выдают выходной поток в таком формате, что его можно использовать в качестве входного потока для других программ. Файлы, пропускаемые через фильтр, состоят из строк текста, свободных от декоративных заголовков, завершителей или пустых строк. Каждая строка представляет интерес – это имя файла, слово, описатель выполняемого процесса, поэтому программы типа wc
или grep
могут рассчитывать определенные характеристики объектов или искать их по именам. Если о каждом объекте имеется большая информация, файл все равно состоит из строк, разбиваемых на поля пробелами или символами табуляции, как в выводе команды ls -l
. Располагая данными, разбитыми на такие поля, программы типа awk
могут легко выбрать, обработать или переупорядочить информацию.
Фильтры построены по общей схеме. Каждый из них пишет в стандартный выходной поток результат обработки файлов-аргументов или стандартного выходного потока, если аргументов нет. Аргументы задают входной поток и никогда не задают выходной[11] 11
Ранняя версия файловой системы UNIX была уничтожена служебной программой, нарушившей это правило, поскольку команда, которая выглядела безобидной, расписала весь диск.
[Закрыть] , поэтому выходной поток команда всегда может передать в конвейер. Необязательные аргументы (или аргументы, не являющиеся файлами, такие, как шаблон в команде grep
) задаются перед именем файлов. Наконец, сообщения об ошибках пишутся в стандартный поток диагностики, поэтому они не могут исчезнуть в конвейере.
Эти соглашения не оказывают большого влияния на программы пользователя, но единообразное применение их ко всем программам обеспечивает простоту взаимодействия, что подтверждается многочисленными примерами на протяжении всей книги и наиболее наглядно продемонстрировано программой подсчета слов в конце разд. 4.2. Если каким-либо программам потребуется входной или выходной файл с конкретным именем, определенное обращение для спецификации параметров или создание заголовков и завершений, то схема конвейера работать не будет. И конечно, если бы система UNIX не предоставляла программные каналы, кому-то пришлось создать подобное стандартное средство. Однако программные каналы есть, и конвейеры работают. Их даже можно запрограммировать, но для этого вы должны знать возможности системы.
Упражнение 4.15
Команда
ps
выдает поясняющий заголовок, а командаls -l
сообщает общее число блоков файла. Прокомментируйте действие команд.
Историческая и библиографическая справка
Хороший обзор алгоритмов сопоставления шаблонов дается в статье Э. Ахо, создателя команды
egrep
, "Pattern matching in strings" (Proceedings of the Symposium on Formal Language Theory, Santa Barbara, 1979). Редакторsed
разработан и реализован на базе редактораed
Л. Мак-Махоном. Языкawk
был разработан и реализован Э. Ахо, П. Вайнбергером и Б. Керниганом, но это решение не очень элегантно. К тому же выбор названия языка по первым буквам имен создателей представляется не вполне удачным. Проект обсуждался в статье авторов "AWK – а pattern scanning and processing language" (Software-Practice and Experience, July, 1978). Языкawk
имеет несколько источников, но, безусловно, некоторые идеи заимствованы из языка Снобол4, редактораsed
, языка проверки условий, разработанного М. Рочкиндом, языковых средствyacc
иlex
и, конечно, языка Си. В действительности сходство междуawk
и Си порождает ряд проблем. Язык подобен Си, но они не совпадают: одни конструкции вawk
отсутствуют, другие отличаются от соответствующих конструкций Си неочевидным образом.В статье Д. Комера "The flat file system FFG: a database system consisting of primitives". (Software – Practice and Experience, Nov., 1982) обсуждается использование интерпретатора и
awk
для создания системной базы данных.
Глава 5
Программирование на языке shell
Большинство пользователей считают, что shell
представляет собой диалоговый интерпретатор команд. На самом же деле это язык программирования, в котором каждый оператор инициирует запуск команды. Язык shell
может показаться вам несколько странным, поскольку в нем находят отражение и диалоговый, и программный аспекты выполнения команд. Он формировался по плану, хотя и имеет свою историю. Разнообразие применений языка привело к некоторой незавершенности в деталях, но для его эффективного использования вам и не нужно разбираться во всех нюансах. В данной главе мы рассмотрим основы программирования с помощью shell
на примерах разработки ряда полезных программ. При изучении материала желательно иметь под рукой страницу sh(1)
справочного руководства по UNIX.
Для интерпретатора, как и для многих других команд, особенности выполнения наиболее четко проявляются в ходе эксперимента. В справочном руководстве что-то может оказаться неясным, и здесь на помощь вам придет хороший пример. По этой причине материал главы построен на примерах, которые демонстрируют возможности языка. Мы будем обсуждать не только вопросы программирования с помощью интерпретатора, но и проблемы создания программ на языке shell
, уделяя особое внимание тестированию и диалогу.
Если вы написали программу на языке shell
или каком-то ином языке, она может оказаться полезной и другим пользователям вашей системы. Однако требования, которым, по мнению других, должна удовлетворять программа, обычно оказываются более строгими, чем предъявляемые к ней вами. Поэтому важнейший аспект программирования на языке shell
– обеспечение надежности программы, чтобы она могла выполняться даже при неверно заданных входных данных и выдавать полезную информацию об ошибках.
5.1 Совершенствование команды cal
Типичная задача программирования на языке shell
сводится к изменению взаимодействия между пользователем и программой, чтобы сделать это взаимодействие более удобным. В качестве примера рассмотрим команду cal(1)
:
$ cal
usage: cal [month] year
Пока хорошо
$ cal october
Bad argument
Уже не так хорошо
$ cal 10 1983
October 1983
S M Tu W Th F S
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
$
Досадно, что месяц нужно задавать числом, и к тому же, как оказалось, команда cal 10
выдает календарь на весь 10-й год, а не на октябрь текущего года. Поэтому всегда следует указывать год, если вы хотите получить календарь на один месяц.
Указанные выше неудобства связаны с тем, что взаимодействие пользователя с программой было реализовано без привлечения команды cal
. Вы можете изменить характер этого взаимодействия, не меняя самой программы. Если поместить команду в ваш собственный каталог bin
, то возможен более удобный способ перевода аргументов в те, которые нужны настоящей команде cal
. Вы можете даже вызывать свою версию команды, и тогда вам меньше придется запоминать.
Первый шаг разработки – определить функции усовершенствованной команды cal
. В основном мы хотим от нее разумного поведения. Месяц нужно распознавать по названию. При наличии двух аргументов она должна делать то же, что делала прежняя версия, за исключением перевода названия месяца в его номер. В случае одного аргумента следует печатать календарь месяца или года (в зависимости от того, что вам требуется), а при отсутствии аргументов – календарь текущего месяца, так как большей частью именно для этого и обращаются к команде. Поэтому задача сводится к тому, чтобы определить, сколько аргументов задано, и преобразовать их в те параметры, которые требуются стандартной команде cal
.
Язык shell
имеет оператор case
, который успешно применяется в таких ситуациях:
case слово in
шаблон) команды ;;
шаблон) команды ;;
...
esac
В операторе case
слово сравнивается поочередно со всеми шаблонами от начала до конца и выполняются команды, связанные с первым (и только первым) шаблоном, соответствующим слову. Шаблоны составляются по правилам соответствия шаблонов, которые в некоторой степени обобщают правила задания имен файлов. Каждое действие завершается двумя символами ;;
(для последнего варианта можно обойтись без ;;
, но обычно мы ставим их для удобства редактирования).
В нашей версии команды определяется число заданных аргументов, обрабатываются названия месяцев, затем происходит обращение к настоящей команде cal
. В переменной интерпретатора $#
хранится число аргументов, с которыми была вызвана программа; другие специальные переменные интерпретатора перечислены в табл. 5.1.
$# | Число аргументов |
$* | Все аргументы, передаваемые интерпретатору |
$@ | Аналогично $* ; см. разд. 5.7 |
$- | Флаги, передаваемые интерпретатору |
$? | Возвращение значения последней выполненной команды |
$$ | Номер процесса интерпретатора |
$! | Номер процесса последней команды, запущенной с помощью & |
$НOМЕ | Аргумент, принятый по умолчанию для команды cd |
$IFS | Список символов, разделяющих слова в аргументах |
$MAIL | Файл, изменение которого приводит к появлению сообщения "you have a mail" ("У вас есть почта") |
$PATH | Список каталогов, в которых осуществляется поиск команд |
$PS1 | Строка приглашение, по умолчанию принята '$' |
$PS2 | Строка приглашение при продолжении командной строки, по умолчанию принята '>' |
Таблица 5.1: Встроенные переменные интерпретатора
$ cat cal
# cal: nicer interface to /usr/bin/cal
case $# in
0) set `date`; m=$2; y=$6 ;; # no args: use today
1) m=$l; set `date`; y=$6 ;; #1 arg: use this year
*) m=$1; y=$2 ;; #2 args: month and year
esac
case $m in
jan*|Jan*) m=1 ;;
feb*|Feb*) m=2 ;;
mar*|Mar*) m=3 ;;
apr*|Apr*) m=4 ;;
may*|May*) m=5 ;;
jun*|Jun*) m=6 ;;
jul*|Jul*) m=7 ;;
aug*|Aug*) m=8 ;;
sep*|Sep*) m=9 ;;
oct*|Oct*) m=10 ;;
nov*|Nov*) m=11 ;;
dec*|Dec*) m=12 ;;
[1-9]|10|11|12) ;; # numeric month
*) y=$m; m="" ;; # plain year
esac
/usr/bin/cal $m $y # run the real one
$
В первом операторе case проверяется число аргументов $#
и выбирается подходящее действие. Последний шаблон в этом операторе задает вариант, выбираемый по умолчанию; если число аргументов не 0 и не 1, будет выполнен последний вариант. (Поскольку шаблоны просматриваются по порядку, вариант по умолчанию должен быть последним.) При наличии двух аргументов m
и y
принимают значение месяца и года, и наша команда cal
должна выполняться как исходная команда.
Первый оператор case
включает пару нетривиальных строк, содержащих
set `date`
Хотя это сразу и не очевидно, легко установить действие команды, запустив ее:
$ date
Sat Oct 1 06:05:18 EDT 1983
$ set `date`
$ echo $1
Sat
$ echo $4
06:05:20
$
Итак, мы имеем дело с встроенной командой интерпретатора, возможности которой многообразны. При отсутствии аргументов она выдает, как указывалось в гл. 3, значения переменных окружения. В случае обычных аргументов переопределяются значения $1
, $2
и т.д. Поэтому set `date`
присваивает $1
– день недели, $2
– название месяца и т.д. Таким образом, при отсутствии аргументов в первом case
месяц и год устанавливаются из текущей даты. Если был задан один аргумент, он используется в качестве месяца, а год берется из текущей даты.
Команда set
имеет также несколько флагов, из которых наиболее часто используются флаги -v
и -х
– для отключения эха команд при обработке их интерпретатором. Такое отключение может оказаться необходимым в процессе отладки сложных программ на языке shell
.
Теперь осталось только перевести значение месяца, если оно представлено в строковом виде, в число. Это делается с помощью второго оператора case
, который практически очевиден. Единственный нюанс состоит в том, что символ |
в шаблонах оператора case
, как и в команде egrep
, означает альтернативу: малый|большой
соответствует варианту "малый" или "большой". Конечно, эти варианты можно было бы задать с помощью [jJ]an*
и т.д. Программа допускает задание месяца строчными буквами, поскольку большинство команд работает с входным потоком, где данные записаны строчными буквами (иногда первая буква – прописная), поскольку так выглядит вывод команды date
. Правила сопоставления шаблонов приведены в табл. 5.2.
* | Задает любую строку, включая пустую |
? | Задает любой одиночный символ |
[ccc] | Задает любой из символов в ccc [a-d0-3] эквивалентно [abcd0123] |
"..." | Задает в точности ... ; кавычки защищают от специальных символов. Аналогично действует '...' |
c | Задает с буквально |
a|b | Только для выражений выбора; задает а или b |
/ | Для имен файлов; соответствует только символу / в выражении; для выражений выбора сопоставляется, как любой другой символ |
. | Если это первый символ в имени файла, то сопоставляется только с явно заданной точкой в выражении |
Таблица 5.2: Правила сопоставления шаблонов в интерпретаторе
Два последних варианта второго оператора case
относятся к случаю, когда единственный аргумент может быть годом; напомним, что в первом операторе case
предполагалось, что аргументом является месяц. Если это число, которым может быть задан месяц, то ничего не происходит (иначе предполагается, что задан год).
Наконец, в последней строке вызывается /usr/bin/cal
(настоящая команда cal
) с преобразованными аргументами. Наша версия команды cal
работает так, как этого мог бы ожидать начинающий:
$ date
Sat Oct 1 06:09:55 EDT 1983
$ cal
October 1983
S М Tu W Th F S
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
$ cal dec
December 1983
S M Tu W Th F S
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
$
При обращении к cal 1984
будет напечатан календарь на весь 1984 год. Наша обобщенная команда cal
выполняет то же задание, что и исходная, но более простым и легко запоминающимся способом. Поэтому мы предпочитаем называть ее cal
, а не calendar
(что уже является командой), или как-нибудь еще с менее простой мнемоникой, например ncal
. При использовании одного и того же имени пользователю не придется вырабатывать новые рефлексы для печати календаря.
Прежде чем завершить обсуждение оператора case
, следует объяснить, почему правила сопоставления шаблонов в интерпретаторе отличаются от правил для редактора ed
и его производных. Действительно, наличие двух видов шаблонов означает, что нужно изучать два набора правил и иметь два программных фрагмента для их обработки. Некоторые различия вызваны просто неудачным выбором, который никогда не был зафиксирован. В частности, нет никаких причин (кроме того, что так сложилось исторически), по которым ed
использует '.'
а интерпретатор – '?'
для задания единственного символа. Но иногда шаблоны применяются по-разному. Регулярные выражения в редакторе используются для поиска последовательности символов, которая может встретиться в любом месте строки; специальные символы и $ нужны, чтобы направить поиск с начала или конца строки. Но для имен файлов мы хотим, чтобы направление поиска определялось по умолчанию, поскольку это наиболее общий случай. Было бы неудобным задавать нечто вроде
$ ls ^?*.с$
Так не получится
вместо
$ ls *.с
Упражнение 5.1
Если пользователи предпочтут вашу версию команды
cal
, как бы вы сделали ее общедоступной? Что следует предпринять, чтобы поместить ее в/usr/bin
?
Упражнение 5.2
Имеет ли смысл сделать так, чтобы при обращении
cal 83
был напечатан календарь за 1983 г.? Как в этом случае задать вывод календаря?
Упражнение 5.3
Модифицируйте команду
cal
так, чтобы можно было задавать больше одного месяца, например:
$ cal oct nov
и даже диапазон месяцев:
$ cal oct-dec
Если сейчас декабрь, а вы выполняете обращение
cal jan
, то какой должен быть напечатан календарь: на январь этого года или следующего? Когда следует прекратить расширять возможности командыcal
?
5.2 Что представляет собой команда which
?
При обзаведении собственными версиями команд, аналогичных cal
, возникает ряд трудностей. В частности, когда вы работаете как пользователь Мэри и вошли в систему под именем mary
, то, вводя команду cal
, получаете стандартную версию команды вместо новой, если, конечно, не установили в своем каталоге bin
связь с новой командой cal
. Это может привести к путанице: вспомните, что сообщения об ошибках в исходной, команде cal
не очень вразумительны. Мы привели всего лишь один пример возникающих трудностей. Поскольку интерпретатор осуществляет поиск команд среди каталогов, задаваемых переменной PATH
, всегда есть вероятность столкнуться не с той версией команды, которую вы ожидали. Например, если вы задали команду, скажем echo
, имя выполняемого на самом деле файла будет ./echo
, /bin/echo
, /usr/bin/echo
или какое-то другое в зависимости от компонентов вашей переменной PATH
и от того, где находятся файлы. Может случиться, что в вашей последовательности поиска ранее, чем вы ожидали, окажется выполняемый файл с правильным именем, но не с теми результатами. Наиболее типичным примером в такой ситуации является команда test
, которую мы обсудим позднее. Ее название настолько распространено для временной версии программы, что вызовы "не тех" команд test
происходят раздражающе часто[12] 12
Позднее будет показано, как обойти эту трудность в командных файлах, где обычно используется команда test
.
[Закрыть]. Здесь весьма полезным средством явилась бы команда, которая помогла бы выяснить, какая версия программы должна выполняться.
Один из вариантов решения – цикл поиска по каталогам, указанным в PATH
, выполняемого файла с данным именем. В гл. 3 мы использовали цикл for
по именам файлов и аргументам. Здесь же нужен такой цикл:
for i in компонента в PATH
do
если заданное имя в каталоге i
печать полного путевого имени
done
Поскольку любую команду можно запустить внутри символов слабого ударения очевидное решение состоит в том, чтобы запустить sed
по значению PATH
, заменив двоеточия на пробелы. Мы можем проверить это с помощью нашего старого друга echo
:
$ echo $PATH
:/usr/you/bin:/bin:/usr/bin
4 компонента
$ echo $PATH | sed 's/:/ /a'
/usr/you/bin /bin /usr/bin
Только три выдано!
$ echo `echo $PATH | sed 's/:/ /g'`
/usr/you/bin /bin /usr/bin
По-прежнему только три
$
Однако такое решение проблематично. Пустая строка в PATH
служит синонимом '.'
. Поэтому перевод двоеточий в пробелы не слишком удачен – теряется информация о пустых компонентах. Для создания правильного списка каталогов пустую компоненту PATH
нужно перевести в точку. Пустая компонента может быть в середине, начале или конце строки, так что вам придется потрудиться, чтобы учесть все возможные случаи:
$ echo $PATH | sed 's/^:/./
> s/::/:.:/g
> s/:$/:./
> s/:/ /g'
. /usr/you/bin /bin /usr/bin
$
Мы могли бы записать это с помощью четырех отдельных команд sed
, но так как редактор sed
производит замены по порядку, можно выполнить все операции за один вызов.
После задания каталогов в компонентах PATH
упомянутая выше команда test(1)
может вывести сообщение о том, существует ли файл в каждом каталоге. В принципе команда test
– одна из самых "неуклюжих" программ UNIX. Например, команда "test -r файл"
проверяет, существует ли файл и можно ли его читать; "test -w файл"
проверяет, существует ли файл и можно ли в него писать, но в седьмой версии нет команды test -х
(хотя в System V и других версиях есть), а именно она нам и нужна. Мы примем, что обращение "test -f файл"
будет проверять, существует ли файл и не является ли он каталогом, т.е. представляет ли он собой обычный файл. Но вам следует обратиться к соответствующей странице справочного руководства, поскольку имеет хождение несколько версий.
Каждая команда вырабатывает код завершения – значение, передаваемое интерпретатору и показывающее, что произошло. Это небольшое целое число, которое устанавливается по соглашению. Так, нуль может означать "истину" (команда успешно завершена), а ненулевое значение трактуется как "ложь" (выполнение команды было неудачным). Обратите внимание на то, что выбранные здесь значения противоположны значениям истины и лжи в языке Си.
Поскольку ложь может представлять множество различных значений, причина неудачи обозначается кодом завершения по лжи. Например, команда grep возвращает 0, если произошло сопоставление, 1 – если сопоставления не было, и 2 – в случае ошибки в шаблоне или именах файлов. Каждая программа возвращает код завершения, хотя обычно нас не интересует его значение. Команда test
неординарна: ее единственное назначение состоит в передаче кода завершения. Она ничего не выводит и не изменяет файлы.
Интерпретатор хранит код завершения последней программы в переменной $?
:
$ cmp /usr/you/.profile /usr/you/.profile
$
Выдачи нет, файлы совпадают
$ echo $?
0
0 означает успех, файлы идентичны
$ cmp /usr/you/.profile /usr/mary/.profile
/usr/you/.profile /usr/mary/.profile differ: char 6, line 3
$ echo $?
1
He нуль означает, что файлы различны
$
У некоторых команд, таких, как cmp
и grep
, есть флаг -s
, который заставляет их завершить выполнение с определенным кодом, но подавляет вывод. Оператор if
языка shell
запускает команды в зависимости от кода завершения некоторой команды, а именно:
if команда
then
команды, если условие верно
else
команды, если условие ложно
fi
Местоположение символов перевода строк очень важно: fi
, then
и else
распознаются только после символа перевода строки или точки с запятой.
Оператор if
всегда запускает команду (условие), тогда как в операторе case
сопоставление с шаблоном производится самим интерпретатором. В некоторых версиях UNIX, включая System V, test
является встроенной командой интерпретатора, поэтому if
и test
будут выполняться так же быстро, как и case
. Если test
– не встроенная команда, то операторы case
более эффективны, чем операторы if
, и следует использовать именно их для поиска шаблонов;
$ case "$1" in
hello) command
esac
выполняется быстрее, чем
if test "$1"==hello
Медленнее, если test не встроенная
then
command
fi
Это одна из причин, по которой в языке shell
иногда для проверки условий применяются операторы case
, хотя в большинстве языков программирования использовались бы операторы if
. С другой стороны, с помощью оператора case
непросто определить, имеется ли право доступа к файлу на чтение; здесь предпочтение следует отдать команде test
и оператору if
.
Итак, теперь мы готовы воспользоваться первой версией команды which
, которая выведет сообщение о том, какой файл соответствует команде:
$ cat which
# which cmd: which cmd in PATH is executed, version 1
case $# in
0) echo 'Usage: which command' 1>&2; exit 2
esac
for i in `echo $PATH | sed 's/^:/.:/
s/::/:.:/g
s/:$/:./
s/:/ /g'`
do
if test -f $i/$1 # use test -x if you can
then
echo $i/$1
exit 0 # found it
fi
done
exit 1 # not found
$
Проверим ее:
$ cx which
Сделаем ее выполняемой
$ which which
./which
$ which ed
/bin/ed
$ mv which /usr/you/bin
$ which which
/usr/you/bin/which
$
Первый оператор case
осуществляет контроль ошибки. Обратите внимание на переключение 1>&2
в команде echo
, которое выполняется для того, чтобы сообщение об ошибке не пропало в программном канале. Встроенная команда интерпретатора exit
может использоваться для передачи кода завершения. В нашем примере exit 2
передает код завершения в ситуации, когда команда не выполняется, exit 1
– в ситуации, когда файл не удалось найти, и exit 0
– в ситуации, когда файл найден. Если нет явного оператора exit
, кодом завершения командного файла является код завершения последней выполняемой команды.
Что произойдет, если в вашем текущем каталоге есть программа под именем test
? (Мы предполагаем, что test
не является встроенной командой.)
$ echo 'echo hello' >test
Сделаем поддельную команду test
$ cx test
Сделаем ее выполняемой
$ which which
Попробуем which теперь
hello
Неудача!
./which
$
Вывод: требуется больший контроль. Можно запустить команду which
(если нет команды test
в текущем каталоге), чтобы определить полное имя для test
и задать его явно. Но это не лучшее решение, поскольку команда test
может присутствовать в различных каталогах в разных версиях системы, а команда which
зависит от sed
и echo
, так что необходимо указать и их полные имена. Можно поступить проще – установить значение PATH
в командном файле так, чтобы поиск команд осуществлялся только в /bin
и /usr/bin
. Конечно, это возможно только в команде which
, причем прежнее значение PATH
следует сохранить для определения последовательности каталогов при поиске.
$ cat which
# which cmd: which cmd in PATH is executed, final version
opath=$PATH
PATH=/bin:/usr/bin
case $# in
0) echo 'Usage: which command' 1>&2; exit 2
esac
for i in `echo $opath | sed 's/^:/.:/
s/::/:.:/g
s/ :$/:./
s/:/ /g'`
do
if test -f $i/$1 # this is /bin/test
then # or /usr/bin/test only
echo $i/$1
exit 0 # found it
fi
done
exit 1 # not found
$
Теперь команда which
выполняется даже в том случае, если есть "поддельная" команда test
(sed
или echo
) среди каталогов, входящих в PATH
.
$ ls -l test
-rwxrwxrwx 1 you 11 Oct 1 06:55 test
Все еще здесь
$ which which
/usr/you/bin/which
$ which test
./test
$ rm test
$ which test
/bin/test
$
В языке shell
имеются две операции, объединяющие команды ||
и &&
, использование которых часто более компактно и удобно, чем оператора if
. Например, операция ||
может заменить некоторые операторы if
:
test -f имя_файла || echo имя_файла не существует
эквивалентно
if test ! -f имя_файла
! обращает условие
then
echo имя файла не существует
fi
Операция ||
, несмотря на свой вид, не имеет ничего общего с конвейерами – это обычная операция, означающая ИЛИ. Выполняется команда слева от ||
. Если ее код завершения 0 (успех), справа от ||
команда игнорируется. Если команда слева возвращает другое значение (неудача), выполняется команда справа, и значение всего выражения есть код завершения правой команды. Иными словами, ||
представляет собой обычную операцию ИЛИ, которая не выполняет свою правую часть, если левая часть завершилась успешно. Соответственно &&
есть обычная операция И, выполняющая свою правую часть, только если левая часть завершилась успешно.
Упражнение 5.4
Почему в команде
which
перед выходом из нее не восстанавливается значениеPATH
изopath
?
Упражнение 5.5
Если в языке
shell
используетсяesac
для завершения оператораcase
иfi
для завершения оператораif
, почему для завершения оператораdo
применяетсяdone
?
Упражнение 5.6
Введите в команду
which
флаг-а
, чтобы выводились все файлы изPATH
, а не только первый найденный.Подсказка:
match='exit 0'
Упражнение 5.7
Модифицируйте команду
which
так, чтобы она учитывала встроенные в языкshell
команды типаexit
.
Упражнение 5.8
Модифицируйте команду
which
так, чтобы она проверяла права доступа файлов. Как изменить ее для получения диагностического сообщения, если файл не удалось найти?