Текст книги "UNIX — универсальная среда программирования"
Автор книги: Брайан Керниган
Соавторы: Роб Пайк
сообщить о нарушении
Текущая страница: 8 (всего у книги 31 страниц) [доступный отрывок для чтения: 12 страниц]
3.4 Аргументы и параметры команд
Хотя команда nu
, как она задумывалась, удовлетворяет своему назначению, многие программы на языке shell
могут обрабатывать аргументы, так что при их запуске можно задавать имена файлов и флаги.
Допустим, вы хотите создать программу с именем cx
для установки права доступа к файлу на выполнение, так что
$ cx nu
есть сокращенная запись для
$ chmod +x nu
Вы уже знаете почти все, чтобы это сделать. Вам нужен файл cx
, содержимое которого суть
chmod +x filename
Единственное, что требуется выяснить – как сообщить команде cx
имя файла, так как при каждом запуске cx
оно будет иным.
Если интерпретатор выполняет командный файл, то каждое вхождение $1
заменяется первым аргументом, каждое вхождение $2
– вторым и т.д. до $9
. Поэтому если файл cx
содержит строку
chmod +x $1
то при выполнении команды
$ cx nu
порожденный интерпретатор заменит "$1
" на первый аргумент "nu
". Рассмотрим всю последовательность операций:
$ echo 'chmod +x $1' >cx
Вначале создадим cx
$ sh cx сх
Сделать сам файл cx выполняемым
$ echo echo Hi, there! >hello
Приготовим тест
$ hello
Попробуем
hello: cannot execute
$ cx hello
Сделаем файл выполняемым
$ hello
Попробуем снова
Hi, there!
Работает
$ mv cx /usr/you/bin
Установим команду cx
$ rm hello
Уберем ненужное
$
Заметьте, что мы задали
$ sh cx сх
в точности так, как сделал бы автоматически интерпретатор, если бы cx
была выполняемой и можно было бы задать
$ cx сх
А как быть, если нужно работать с несколькими аргументами, например, заставить программу cx
воздействовать сразу на несколько файлов? Прямолинейное решение состоит в том, чтобы включить девять аргументов в командный файл:
chmod +x $1 $2 $3 $4 $5 $6 $7 $8 $9
(Это годится только для девяти аргументов, так как конструкция $10
распознается как "первый аргумент, за которым следует 0"!) Если пользователь такого командного файла задаст меньше девяти аргументов, то недостающие окажутся пустыми строками. Это приведет к тому, что только настоящие аргументы будут переданы chmod
порожденным интерпретатором. Такое решение, конечно, приемлемо, но не вполне корректно и не подходит для случая с числом аргументов более девяти.
С учетом упомянутой выше трудности интерпретатор предоставляет сокращенную запись $*
, означающую "все аргументы". В этом случае правильно определить cx
:
chmod +x $*
что является эффективным при любом числе аргументов.
Используя $*
в своем репертуаре, вы можете создать некоторые полезные командные файлы, такие, как lc
или m
:
$ cd /usr/you/bin
$ cat lc
#lc: подсчет числа строк в файлах
wc -l $*
$ cat m
#m: точный способ послать почту
mail $*
$
Обе команды можно осмысленно использовать и без аргументов. Если нет аргументов, $*
будет пустым, и wc
и mail
вообще не получат никаких аргументов. С аргументами или без них команда вызывается правильно:
$ lc /usr/you/bin/*
1 /usr/you/bin/cx
2 /usr/you/bin/lc
2 /usr/you/bin/m
1 /usr/you/bin/nu
2 /usr/you/bin/what
1 /usr/you/bin/where
9 total
$ ls /usr/you/bin | lc
6
$
Эти и другие команды, описываемые в настоящей главе, являются командами пользователя, т.е. вы создаете их для себя и помещаете в свой каталог /bin
, поэтому вряд ли они должны стать общедоступными. В гл. 5 мы исследуем вопрос создания общедоступных программ на языке shell
.
Аргументами командного файла не обязательно должны быть имена файлов. Рассмотрим в качестве примера поиск в каталоге, где хранится личный телефонный справочник. Если у вас есть файл с именем /usr/you/lib/phone-book
, содержащий строки следующего вида:
dial-a-joke 212-976-3838
dial-a-prayer 212-246-4200
dial santa 212-976-3636
dow jones report 212-976-4141
то для поиска в нем можно воспользоваться командой grep
. (Ваш собственный каталог lib
– хорошее хранилище таких частных баз данных.) Поскольку команда grep
не определяет формат информации, можно искать имена, адреса, индексы или еще какие-нибудь нужные вам сведения. Составим справочную программу для каталога, которой дадим имя 411
по номеру одной из телефонных справочных служб:
$ echo 'grep $* /usr/you/lib/phone-book' > 411
$ cx 411
$ 411 joke
dial-a-joke 212-976-3838
$ 411 dial
dial-a-joke 212-976-3838
dial-a-prayer 212-246-4200
dial santa 212-976-3636
$ 411 'dow jones'
grep: can't open jones
Что-то не так
$
Последний пример вскрывает потенциальную проблему: хотя dow jones
представляет для команды 411
единый аргумент, он содержит пробел и уже не заключен в апострофы, поэтому порожденный интерпретатор, выполняющий команду 411
, преобразует его в два аргумента для grep
, как если бы вы задали
$ grep dow jones /usr/you/lib/phone-book
что, очевидно, неверно.
Один из возможных путей обойти эту проблему основан на том, как интерпретатор трактует кавычки. Хотя все, что заключено в '...'
, не затрагивается, интерпретатор "заглядывает" внутрь "..."
в поиске комбинаций с $
, ,
`...`
. Поэтому если изменить команду 411 следующим образом:
$ grep "$*" /usr/you/lib/phone-book
то $*
заменяется на аргументы, но команде grep
передается как один аргумент, даже при наличии пробелов:
$ 411 dow jones
dow jones report 212-976-4141
$
Кстати, можно сделать с помощью флага -y
команду grep
(а значит, и 411
) независимой от использования строчных или прописных букв:
$ grep -y pattern ...
При наличии флага -y
строчные буквы из шаблона могут сопоставляться с прописными буквами из входного потока. (Такой флаг есть в седьмой версии, но отсутствует в других системах.)
Более подробно аргументы команд мы рассмотрим в гл. 5, но одно важное замечание необходимо сделать здесь. Аргумент $0
– это имя выполняемой программы; в случае cx $0
есть "cx"
. Новое применение $0
находит в реализации программ 2
, 3
, 4
, …, которые печатают свой выходной поток в несколько столбцов:
$ who | 2
drh tty0 Sep 28 21:23 cvw tty5 Sep 28 21:09
dmr tty6 Sep 28 21:10 scj tty7 Sep 28 22:11
you tty9 Sep 28 23:00 jib ttyb Sep 28 19:58
$
Реализация команд 2
, 3
, … идентична. По существу, они являются связями с одним файлом:
$ ln 2 3; ln 2 4; ln 2 5; ln 2 6
$ ls -l [1-9]
167222 -rwxrwxrwx 5 you 51 Sep 28 23:21 2
167222 -rwxrwxrwx 5 you 51 Sep 28 23:21 3
167222 -rwxrwxrwx 5 you 51 Sep 28 23:21 4
167222 -rwxrwxrwx 5 you 51 Sep 28 23:21 5
167222 -rwxrwxrwx 5 you 51 Sep 28 23:21 6
$ ls /usr/you/bin | 5
2 3 4 411 5
6 cx lc m nu
what where
$ cat 5
# 2, 3, ...: печать в n столбцов
pr -$0 -t -11 $*
$
Флаг -t
убирает заголовки в начале страницы, а флаг -ln
устанавливает размер страницы равным n
строк. Имя программы становится числом столбцов, т.е. аргументов для команды pr, так что выходной поток печатается строками по несколько столбцов, число которых определено аргументом $0
.
3.5 Результат выполнения программы в качестве аргумента
Теперь перейдем от аргументов команд для командного файла к порождению аргументов. Конечно, расширение имен файлов с помощью метасимволов, подобных *
, является наиболее типичным способом порождения аргументов (иным, чем их явное задание), но столь же хорошим способом представляется и выполнение программы. Результат выполнения любой программы можно использовать в командной строке, заключив ее вызов в символы слабого ударения `...`
:
$ echo At the tone the time will be `date`.
At the tone the time will be Thu Sep 29 00:02:15 EDT 1983.
$
Небольшое изменение показывает, что `...`
интерпретируется и внутри кавычек "..."
:
$ echo "At the tone
> the time will be `date`."
At the tone
the time will be Thu Sep 29 00:03:07 EDT 1983.
$
В качестве другого примера предположим, что вам необходимо послать почту группе людей, которые зарегистрированы под именем, хранящимся в файле mailinglist
. Можно, конечно, отредактировать файл mailinglist
так, чтобы он стал пригодным для применения команды mail
и передать его интерпретатору, но значительно проще использовать команду
$ mail `cat mailinglist`
Запуск команды cat
порождает список имен пользователей, и эти имена становятся аргументами команды mail
. (При обработке результата выполнения команды, помещенной между знаками слабого ударения и используемой в качестве аргумента, интерпретатор считает символы перевода строки разделителями слов, а не символами завершения командной строки; подробнее данный вопрос обсуждается в гл. 5.) Работать со знаками слабого ударения нетрудно, и поэтому, действительно, нет нужды вводить отдельный флаг команды mail
, задающий список адресатов.
Несколько иной подход требуется для преобразования файла mailinglist
из простого списка имен в программу, выдающую список имен:
$ cat mailinglist
echo don whr ejs mb
Новая версия
$ cx mailinglist
$ mailinglist
don whr ejs mb
$
Теперь посылка писем адресатам из списка реализуется командой:
$ mail `mailinglist`
Добавив еще одну программу, получим возможность даже изменять список пользователей в диалоге. Такая программа называется pick
:
$ pick аргументы...
и выдает свои аргументы по одному, ожидая каждый раз ответа. Результатом действия команды pick
являются те аргументы, на которые был дан ответ y
(yes
– да); при всяком другом ответе аргумент отбрасывается. Например,
$ pr `pick *.с` | lpr
Здесь вначале выдаются имена файлов, оканчивающиеся на .с
. Выбранные имена печатаются с помощью команд pr
и lpr
. (Команда pick
не входит в состав команд седьмой версии, но она столь проста и полезна, что мы включили ее варианты в гл. 5 и 6).
Допустим, вы используете второй вариант команды mailinglist
. Тогда посылка писем адресатам don
и mb
выглядит так:
$ mail `pick `mailinglist``
don? y
whr?
ejs?
mb? y
$
Обратите внимание на вложенные знаки слабого ударения; обратная дробная черта запрещает обработку вложенной конструкции `...`
при разборе внешних знаков слабого ударения.
Упражнение 3.10
Что произойдет, если опустить символы обратной дробной черты в команде
$ echo `echo `date``
Упражнение 3.11
Попробуйте ввести
$`date`
и объясните результат.
Упражнение 3.12
Команда
$ grep -l pattern filenames
перечисляет имена файлов, которые соответствуют шаблону, но больше ничего не выдает. Попытайтесь выполнить разные вариации такого задания:
$ command `grep -l pattern filenames`
3.6 Переменные языка shell
Подобно большинству языков программирования, shell
имеет переменные, которые на программистском жаргоне называются параметрами. Такие строки, как $1
, являются позиционными параметрами-переменными, хранящими аргументы командного файла. Цифра показывает положение параметра в командной строке. Ранее мы имели дело с другими переменными языка shell
: PATH
– список каталогов, в которых происходит поиск команд, НОМЕ
– ваш начальный каталог и т.д. В отличие от переменных в обычном языке переменные, задающие позиционные параметры, не могут быть изменены; хотя PATH
представляет собой переменную со значением $PATH
, нет переменной 1 со значением $1
, т.е. $1
– это не что иное, как компактное обозначение первого аргумента.
Если забыть о позиционных параметрах, переменные языка shell
можно создавать, выбирать и изменять. Например,
$ PATH=:/bin:/usr/bin
означает присваивание, изменяющее список каталогов в процессе поиска. До и после знака равенства не должно быть пробелов. Присваиваемое значение должно выражаться одним словом, и его следует взять в кавычки, если оно содержит метасимволы, которые не нужно обрабатывать. Значение переменной выбирается, если предварить имя знаком доллара:
$ PATH=$PATH:/usr/games
$ echo $PATH
:/usr/you/bin:/bin:/usr/bin:/usr/games
$ PATH=:/usr/you/bin:/bin:/usr/bin
Восстановим значение
$
He все переменные имеют специальное значение для интерпретатора. Можно создавать новые переменные, присваивая им значения. По традиции переменные, имеющие специальное значение, обозначаются прописными буквами, а обычные переменные – строчными. Типичным примером использования переменных является хранение в них длинных строк, таких, как имена файлов:
$ pwd
/usr/you/bin
$ dir=`pwd`
Запомним, где находимся
$ cd /usr/mary/bin
Перейдем в другое место
$ ln $dir/cx .
Используем переменную в имени файла
$ ...
Поработаем некоторое время
$ cd $dir
Вернемся
$ pwd
/usr/you/bin
$
Встроенная в интерпретатор команда set
показывает значения всех определенных вами переменных. Для просмотра одной или двух переменных более подходит команда echo
:
$ set
HOME=/usr/you
IFS=
PATH=:/usr/you/bin:/bin/:/usr/bin
PS1=$
PS2=>
dir=/usr/you/bin
$ echo $dir
/usr/you/bin
$
Значение переменной связано с той копией интерпретатора, который создал ее, и автоматически не передается процессам – потомкам интерпретатора.
$ x=Hello
Создание x
$ sh
Новый shell
$ echo $x
Происходит только перевод строки,
x не определено в порожденном интерпретаторе
$ ctl-d
Возврат в исходный интерпретатор
$ echo $x
Hello
x по-прежнему определено
$
Это означает, что в командном файле нельзя изменить значение переменной, поскольку выполнением командного файла управляет порожденный интерпретатор:
$ echo 'x="Good bye"
Создание shell-файла из двух строк…
> echo $x' >setx
…для определения и печати x
$ cat setx
x="Good Bye"
echo $x
$ echo $x
Hello
x есть Hello в исходном интерпретаторе
$ sh setx
Good Bye
x есть Good Bye в порожденном интерпретаторе…
$ echo $x
Hello
…но по-прежнему есть Hello в текущем интерпретаторе
$
Однако бывают ситуации, когда было бы полезно изменять переменные интерпретатора в командном файле. Очевидным примером является файл, добавляющий новый каталог к вашей переменной PATH
. Поэтому интерпретатор предоставляет команду '.'
(точка), которая выполняет команды из файла в текущем, а не порожденном интерпретаторе. Первоначально это было задумано для удобства пользователей, чтобы они могли повторно выполнять свой файл .profile
, не входя заново в систему, но в принципе это открывает и другие возможности:
$ cat /usr/you/bin/games
PATH=$PATH:/usr/games
Добавим /usr/games к PATH
$ echo $PATH
:/usr/you/bin:/bin:/usr/bin
$ . games
$ echo $PATH
:/usr/you/bin:/bin:/usr/bin:/usr/games
$
Поиск файла для команды '.'
осуществляется с помощью переменной PATH
, так что его можно поместить в ваш каталог bin
.
Когда используется команда '.'
, только условно можно считать, что выполняется командный файл. Файл не "выполняется" в обычном смысле этого слова. Команды из него запускаются, как если бы вы ввели их в диалоговом режиме: стандартный входной поток интерпретатора временно переключается на файл. Поскольку файл читается, не нужно иметь право на его выполнение. Другое отличие состоит в том, что файл не получает аргументов командной строки; $1
, $2
и т.д. являются пустыми строками. Было бы неплохо, если бы аргументы передавались, но этого не происходит.
Есть еще один способ установить значение переменной в порожденном интерпретаторе – присвоить его явно в командной строке перед самой командой:
$ echo 'echo $x' >echox
$ cx echox
$ echo $x
Hello
Как и прежде
x не определено в порожденном интерпретаторе
$ x=Hi echox
Hi
Значение x передается порожденному интерпретатору
$
(Первоначально присваивания всюду в командной строке передавались команде, но это противоречило dd(1)
.)
Операцию '.'
следует использовать, чтобы навсегда изменить значение переменной, тогда как присваивания в командной строке предназначены для временных изменений. В качестве примера рассмотрим еще раз поиск команд в каталоге /usr/games
, не указанном в вашей переменной PATH
:
$ ls /usr/games | grep fort
fortune
Игровая команда fortune
$ fortune
fortune: not found
$ echo $PATH
:/usr/you/bin:/bin:/usr/bin
/usr/games не входит в PATH
$ PATH=/usr/games fortune
Позвони в звонок; закрой книгу; задуй свечу.
$ echo $PATH
:/usr/you/bin:/bin:/usr/bin
PATH не изменилось.
$ cat /usr/you/bin/games
команда games все еще здесь
$ . games
$ fortune
Непродуманная оптимизация – источник всех бед – Кнут
$ echo $PATH
:/usr/you/bin:/bin:/usr/bin:/usr/games
Сейчас PATH изменилось
$
Можно использовать оба средства в одном командном файле. Вы можете несколько видоизменить команду games
для запуска одной игровой программы без изменения переменной PATH
или постоянно переопределять PATH
, чтобы она включала /usr/games
:
$ cat /usr/you/bin/games
PATH=$PATH:/usr/games $*
Обратите внимание на $*
$ cx /usr/you/bin/games
$ echo $PATH
:/usr/you/bin:/bin:/usr/bin
/usr/games не входит
$ games fortune
Готов отдать свою правую руку, чтобы одинаково владеть обеими
$ echo $PATH
:/usr/you/bin:/bin:/usr/bin
Все еще не входит
$ . games
$ echo $PATH
:/usr/you/bin:/bin:/usr/bin:/usr/games
Теперь входит
$ fortune
Тот, кто медлит, иногда спасается
$
При первом обращении к games командный файл выполняется в порожденном интерпретаторе, в котором переменная PATH
временно изменена так, чтобы включать каталог /usr/games
. В то же время во втором примере файл обрабатывается текущим интерпретатором при значении $*
, являющемся пустой строкой, поэтому в строке нет команд и переменная PATH
изменяется. Применение команды games
в обоих случаях достаточно нетривиально, но в результате получаем удобное и естественное для использования средство.
Для того чтобы значение переменной было доступно в порожденном интерпретаторе, следует использовать команду export
языка shell
. (Вы можете поразмышлять о том, почему нет возможности экспортировать значение переменной от порожденного интерпретатора к порождающему его.) Приведем один из рассмотренных выше примеров, но теперь с экспортом переменной:
$ x=Hello
$ export x
$ sh
Новый интерпретатор
$ echo $x
Hello
x доступно в порожденном интерпретаторе
$ x='Good Bye'
Изменим значение x
$ echo $x
Good Bye
$ ctl-d
Выйдем из порожденного интерпретатора
$
Снова в исходном интерпретаторе
$ echo $x
Hello
x по-прежнему Hello
$
Семантика команды export
нетривиальна, но по крайней мере для повседневных нужд достаточно придерживаться основного правила: никогда не экспортируйте временные переменные, служащие для краткосрочных целей, и всегда экспортируйте переменные, необходимые вам во всех порожденных интерпретаторах (включая, например, интерпретаторы, запускаемые командой !
редактора ed
). Поэтому переменные, имеющие специальное значение для интерпретатора, такие, как PATH
и НОМЕ
, следует экспортировать.
Упражнение 3.13
Почему в значение переменной
PATH
всегда включается текущий каталог? Куда его нужно поместить?
3.7 Еще раз о переключении ввода-вывода
Понятие стандартного потока диагностики было введено для того, чтобы сообщения об ошибках всегда появлялись на терминале:
$ diff file1 file2 >diff.out
diff: file2: No such file or directory
$
Без сомнения, сообщения об ошибке должны появляться подобным образом – было бы крайне неприятно, если бы они исчезли в файле diff.out
, оставляя вас в уверенности, что ошибочная команда diff
выполнена правильно.
В начале выполнения каждой программы определены по умолчанию три файла, обозначаемые небольшими целыми числами и называемые дескрипторами файла (мы рассмотрим их в гл. 7). Со стандартными входным (0) и выходным (1) потоками вы уже знакомы: они часто переключаются на файл или программный канал. Последний поток с номером 2 представляет собой стандартный поток диагностики и обычно предназначается для вывода на терминал.
Иногда программы осуществляют вывод в стандартный поток диагностики, даже если они работают правильно. Типичным примером является программа time, которая выполняет команду и выдает в стандартный поток диагностики сообщение о том, сколько времени заняло выполнение:
$ time wc ch3.1
931 4288 22691 ch3.1
real 1.0
user 0.4
sys 0.4
$ time wc ch3.1 >wc.out
real 2.0
user 0.4
sys 0.3
$ time wc ch3.1 >wc.out 2>time.out
$ cat time.out
real 1.0
user 0.4
sys 0.3
$
Конструкция 2> имя_файла
(между 2
и >
не должно быть пробелов) переключает стандартный поток диагностики на файл; синтаксически она непривлекательна, но служит своей цели. (Для такого короткого теста, как приведенный выше, время, выдаваемое командой time
, не совсем правильное, но для последовательности больших тестов она выводит полезную информацию, которой можно доверять в разумных границах. Вы вполне можете сохранить ее для дальнейшего анализа; обратитесь, например, к таблице 8.1.)
Допустимо также слияние двух выходных потоков:
$ time wc ch3.1 >wc.out 2>&1
$ cat wc.out
931 4288 22691 ch3.1
real 1.0
user 0.4
sys 0.3
$
Обозначение 2>&1
является указанием интерпретатору, что стандартный поток диагностики нужно поместить в тот же поток, что и стандартный выходной. Амперсанд не содержит какого-либо мнемонического смысла; это просто идиома, которую следует запомнить. Для добавления стандартного выходного потока к стандартному потоку диагностики можно использовать 1>&2
:
echo ... 1>&2
В командных файлах это позволяет предотвратить исчезновение сообщений в файле или программном канале.
Интерпретатор предоставляет возможность размещать стандартный входной поток вместе с командой, а не в отдельном файле, так что командный файл может хранить всю информацию в себе самом. Наша справочная программа 411
, работающая с каталогом телефонов, могла быть задана так:
$ cat 411
grep "$*" <
dial-a-joke 212-976-3838
dial-a-prayer 212-246-4200
dial santa 212-976-3636
dow jones report 212-976-4141
End
$
Программирующие на языке shell
называют такую конструкцию "документ здесь", т.е. входной поток находится здесь, а не в каком-нибудь файле. Началом конструкции служит <<
; последующее слово (в нашем примере End
) является ограничителем входного потока, включающего все строки до той, которая содержит только данное слово. Интерпретатор выполняет замену конструкций $
, `...`
и в "документе здесь", если только часть слова не экранирована кавычками или обратной дробной чертой, – в этом случае весь документ берется без изменений. В конце главы мы рассмотрим еще более интересный пример с конструкцией "документ здесь".
В табл. 3.2 перечислены различные виды переключения ввода-вывода, допускаемые интерпретатором.
> файл | Переключение стандартного выходного потока в файл |
>> файл | Добавление стандартного выходного потока в файл |
< файл | Получение стандартного выходного потока из файла |
p1 | p2 | Передача стандартного выходного потока программы p1 в качестве входного потока для программы p2 |
^ | Устарелый синоним | |
n> файл | Переключение выходного потока из файла с дескриптором n в файл |
n>> файл | Добавление выходного потока из файла с дескриптором n в файл |
n>&m | Слияние выходных потоков файлов с дескрипторами n и m |
< | "Документ здесь": берется стандартный входной поток до строки, начинающейся с s ; выполняется подстановка для $ , `...` и
|
< | "Документ здесь" без подстановки |
<<'s' | "Документ здесь" без подстановки |
Таблица 3.2: Переключение ввода-вывода интерпретатора
Упражнение 3.14
Сравните версии программы 411: использующую "документ здесь" и первоначальную. Какую легче сопровождать? Какая более подходит в качестве основы общего служебного средства?