учебники, программирование, основы, введение в,

 

Shell как язык программирования и интегратор

И швец, и жнец
Любой универсальной ОС приходится много возиться с пользовательскими и своими собственными задачами. Лишь небольшая часть этой деятельности может быть запрограммирована раз и навсегда в ядре. Большая часть логики управления задачами и самой системой должна быть доступна администратору в виде проекта, иначе он просто не сможет ни понять происходящее в системе, ни тем более изменять ее. Стоит повнимательнее взглянуть на инструмент, используемый в UNIX для задания алгоритма работы многих частей системы, - на командный интерпретатор, shell. Оказывается, shell отлично себя показывает не только в диалоге с пользователем, но и как исполнитель сценариев, и как средство организации взаимодействия между задачами в системе.
Начнем с того, что shell - полноценный язык программирования, причем, как многие интерпретаторы, довольно высокого уровня. Если задача - разовая (нет требований по быстродействию, совместимости и переносимости) и достаточно абстрактная (нет привязки к конкретной сложной структуре данных), ее скорее всего можно решить, написав командный сценарий - программу на shell.
С другой стороны, одной алгоритмической полнотой при решении задач в системе ограничиваться нельзя. Скажем, машина Тьюринга чрезвычайно проста и алгоритмически полна, однако мало кому придет в голову организовывать на основе ее модели диалог с пользователем или управление самой ОС. Здесь следует вспомнить, что shell - еще и исполнитель команд: он запросто общается с UNIX и утилитами. Значит, дополнив его механизмом управляемого взаимодействия команд с системой и друг с другом, мы получим неплохой интегратор (или оболочку - что, собственно, и есть перевод слова shell).
Самое приятное, что такая программируемая оболочка не будет слишком выходить за рамки У: если уж, в наследство от диалоговой ипостаси shell, мы можем легко обращаться за решением подзадачи к любой утилите UNIX, дублировать ее в языке совершенно незачем, и там останутся как раз одни только алгоритмические и координационные абстракции.
Сценарий
Прежде чем рассмотреть возможности shell под двумя углами зрения, разрешим вот какое затруднение. Допустим, мы написали программу на языке какого-нибудь интерпретатора, например /bin/sh, и записали ее в некий файл, например /home/george/myscript (если /home/george - текущий каталог, можно использовать более короткий путь: myscript). Как теперь выполнить этот сценарий? Из man sh мы знаем, что для этого можно запустить командный интерпретатор с параметром - именем файла:
$ cat myscript
echo "Hello, George!"
$ /bin/sh myscript
Hello, George!
Нельзя ли обойтись без имени программы, которая интерпретирует сценарий? Вообще говоря, нет: в UNIX немало различных интерпретаторов с разнообразным синтаксисом, например обработчик текстов awk, потоковый текстовый редактор sed, универсальные языки программирования python и perl и много чего еще. Во всех этих языках есть возможность вставлять в текст сценария строчные комментарии, которые начинаются с символа "#" и заканчиваются в конце строки. Поэтому, если сценарий начинается с символов "#!", любой из этих интерпретаторов проигнорирует всю первую строку как комментарий. Система же, увидев "#!" в начале файла, понимает, что это сценарий. С третьего символа и до конца строки она читает имя программы, которой отдает этот файл на выполнение. Значит, если первой строкой в /home/george/myscript будет #!/bin/sh, его смело можно делать исполняемым (установить бит использования) и запускать:
$ chmod +x myscript
$ cat myscript
#!/bin/sh
echo "Hello, $1!"
$ ./myscript George
Hello, George!
Строго говоря, после "#!" может стоять что угодно, например имя написанной нами программы с некоторыми обязательными параметрами; UNIX ее запустит и передаст ей в качестве параметров командной строки обязательные параметры (если они есть), затем имя сценария и все, что идет следом (в нашем примере George). Если же после "#!" будет стоять несуществующий файл, система выдаст сообщение об ошибке:
$ cat myscript
#!/bad/sh
echo "Hello, $1!"
$ ./myscript
./myscript: not found
Обратите, пожалуйста, внимание на то, что из этого сообщения якобы следует, что не найден сам файл сценария. Если не знать подоплеку явления, ситуация кажется подозрительной. Дело в том, что, запуская любую программу, UNIX всегда передает ей один параметр (который имеет индекс 0) - имя этой программы. Но в случае запуска сценария обработчик получит в качестве нулевого параметра не собственное имя, а имя сценария. А когда система этого обработчика не найдет, в сообщении об ошибке он будет упоминаться под новым именем.
Гнезда shell`ов
И еще одно немаловажное замечание. Сначала в UNIX был только один командный интерпретатор, написанный Стивеном Борном (Stephen Bourne), и назывался он просто "оболочка" (т. е. shell, а имя утилиты, для краткости, sh). Это была очень простая маленькая программа, она отлично работала именно как системный интегратор, но во всех остальных ипостасях была довольно слабой. И вот создателям 3BSD пришло в голову, что нужен совершенно новый командный интерпретатор, более удобный при работе в командной строке, с новыми возможностями программирования и с новым синтаксисом, приближенным к языку Си, который и так знаком любому UNIX-программисту. Получившуюся оболочку назвали C shell (за синтаксис команд; имя утилиты - csh), она была намного мощнее старой, там была работа с историей, достраивание имен файлов, управление заданиями; появились массивы и много чего еще.
Однако программировать командные сценарии на csh оказалось... менее удобно, чем на sh. Во-первых, синтаксис языка Си в точности воспроизвести не удалось, возникла путаница; во-вторых, многие системные сценарии уже были к тому времени написаны на shell. Обе эти оболочки дали начало так называемым гнездам - Bourne и C, потому что всякий новый командный интерпретатор проектировался с расчетом на совместимость либо с sh, либо с csh.
Долгое время бытовало мнение, что работать лучше в csh, а сценарии писать - на sh, хотя совмещать два разных языка в одной области деятельности неудобно. С тех пор было создано немало других, еще более мощных sh- и csh-образных оболочек (см. "Сравнительную таблицу командных интерпретаторов"). Они все время дорабатывались, попеременно обгоняя друг друга по охвату возможностей. Пальма первенства принадлежала то bash (Bourne-again shell, детище GNU), то tcsh (международный открытый проект, развивающий csh), то zsh (также международный открытый проект, но с sh-совместимостью). На сегодня нельзя с уверенностью сказать, какой из них мощнее (хотя документации по zsh больше, чем по bash и tcsh, вместе взятых, раза в два), но устаревшее мнение о sh и csh стоит пересмотреть: теперь в какой оболочке работаешь, на языке той и сценарии пиши.

Окружение

Как уже говорилось, многие абстракции языка программирования доработаны в shell так, чтобы служить средством связывания и взаимодействия процессов с системой и друг с другом. Например, абстракция переменная превращается в понятие переменная окружения (environment variable).

Переменная окружения

С одной стороны, переменная окружения - обыкновенная строковая переменная, в которой кто угодно может хранить какие угодно значения; ее даже и описывать не надо:

$ One=U; Three=Zzz
$ echo Example 1: $One $Two $Three
Example 1: U Zzz
$ echo "Example 2: $One $Two $Three"
Example 2:      U Zzz
$ echo 'Example 3: $One $Two $Three'
Example 3:      $One $Two $Three

Из первого примера видно, что сам факт присваивания объявляет переменную, а содержимое необъявленной переменной просто считается пустым (напомним, что команда echo выводит все параметры командной строки, разделяя их пробелом). Как показывают второй и третий примеры, подстановка значения переменной (операция $имя_переменной) продолжает работать, если текст заключен в двойные кавычки, и не работает, если используются одинарные. По договоренности (см. главу 7) закавыченный текст передается как один параметр командной строки. Теперь очевидно, что подстановку shell делает до вызова команды; только этим можно объяснить, почему в первом примере echo выводит один пробел между U и Zzz: она получает четыре параметра ( Example 1: U и Zzz), разделенные цепочками пробелов.
Несмотря на то что переменные в shell - строкового типа, легко организовать арифметические операции над ними: если содержимое переменной нельзя интерпретировать как число, арифметическая операция завершается с ошибкой. Арифметика встроена почти во все виды командных интерпретаторов (см. "Сравнительную таблицу командных интерпретаторов"), однако, к сожалению, в каждом это сделано слегка по-своему. Наиболее распространена арифметическая подстановка (arithmetic expansion), при которой shell вычисляет строки вида $((арифметическое выражение)). Есть надежда, что арифметическая подстановка с целыми числами в версии современного BSD-sh или Linux-ash будет работать и в остальных командных интерпретаторах гнезда Bourne:

$ A=7; b=3; echo $(($A*$b))
21

В примере, описывающем понятие сценария, мы видели, как в shell устроен механизм передачи параметров командной строки. Все просто: первый параметр попадает внутри сценария в переменную с именем 1 (подстановка значения этой переменной выглядит как $1), второй - в переменную с именем 2 и т. д., пока есть параметры в командной строке. Имя самого сценария передается в переменной 0 в точности так, как мы его вызвали. Количество переданных параметров подставится вместо $# (напомним, что по американской традиции этот символ используется вместо нашего No). Все параметры командной строки подставляются вместо $* или $@ (Какая между этими формами разница? RTFM!)
Столь простая схема передачи параметров - явная доработка языка в сторону средств интеграции: чаще всего небольшой сценарий - это некоторая составная пользовательская команда, связывающая несколько утилит, и чтобы она не была одноразовой, ей наверняка придется уметь обрабатывать командную строку.

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

С другой стороны, переменная окружения - удобное и простое средство управления системой и утилитами. Не один только командный интерпретатор, но и любой процесс в системе имеет так называемое окружение (environment), которое состоит из пар "имя=значение". Окружение создает система, копируя окружение процесса-родителя. Потомок волен делать со своей копией что угодно: изменять значения переменных, заводить новые, удалять и т. п., на окружении родителя это никак не отразится. С переменными окружения можно передавать важную информацию. Отличие от параметров командной строки заключается в том, что переменную достаточно изменить или задать один раз, и пока она изменена, по-другому будет работать и чувствительная к ней утилита. С точки зрения shell его собственные переменные и есть переменные окружения.
Например, утилита ls использует переменную окружения COLUMNS, в которой хранится наибольшая допустимая ширина текста. Можно обычным присваиванием изменить значение этой переменной, и ls примет его к сведению:

$ echo $COLUMNS
$ ls 
Makefile    myscript    uzor.c
fhs-2.2-source.tar.gz   o.ps
$ COLUMNS=60
$ ls
Makefile    o.ps
fhs-2.2-source.tar.gz   uzor.c
myscript

Многие программы используют переменную EDITOR, содержащую имя текстового редактора: если понадобится что-нибудь редактировать, они запустят именно его. Переменная HOME содержит путь к вашему домашнему каталогу, в TERM хранится тип терминала, в LOGNAME - входное имя пользователя. Переменная PATH используется самим shell для поиска утилиты, если имя команды не совпадает с именами ни одной из встроенных команд. В PATH перечислены каталоги, в которых shell будет утилиту искать. Кстати сказать, именно потому, что в переменной PATH обычно нет текущего каталога (путь "." не включен в список), в одном из приведенных выше примеров мы были вынуждены явно указывать его в виде ./myscript. Помещать "." в PATH небезопасно: кто может знать, не окажется ли в текущем каталоге исполняемый файл с именем, скажем, ls, и кто может знать, что на самом деле этот файл будет делать при запуске?
Список определяемых переменных окружения велик, его выдает команда set. Желающих узнать, какими переменными окружения пользуется та или иная утилита, мы отсылаем к ее руководству, раздел ENVIRONMENT VARIABLES.
Сам shell пользуется многими переменными окружения. Например, строка-подсказка (то, при виде чего пользователь захочет ввести очередную команду в командной строке) хранится в переменной PS1 (Если есть PS1, то должно быть PS2, а может, и PS3, и PS4. Для чего они? RTFM!). Поначалу в этой переменной хранилась строка "$" для обычного пользователя и "#" для суперпользователя. Потом решили использовать PS1 для вывода кое-какой полезной информации: имени компьютера, имени пользователя, даты входа в систему или прочей относительно статической информации. Это просто, достаточно написать что-то вроде:

$ echo $PS1
$
$ PS1="`logname`@`hostname`> "
george@book.altlinux.ru> 
george@book.altlinux.ru> 

Однако статическая информация - не самое полезное в работе. Гораздо интереснее держать в PS1, скажем, путь к текущему каталогу или точное время. Сменился текущий каталог - изменилась PS1. Сам shell изменять PS1 не будет, значит, надо его доработать. Первый способ доработки - научить его выполнять определенную последовательность команд перед тем, как выводить очередную подсказку или при смене текущего каталога (так поступили разработчики tcsh: там можно задать специальные функции precmd, cwdcmd и некоторые другие). В эту последовательность команд можно вставить команды изменения PS1. Другой вариант - считать некоторые последовательности символов в PS1 специальными и при выводе подсказки заменять их соответствующими значениями - путем, временем и т. п. Вот, например, как это делается в bash:

bash$ pwd
/usr/share/doc
bash$ PS1="[\A]\u@\h:\w> "
[17:30]george@book:/usr/share/doc> 
[17:30]george@book:/usr/share/doc> 

Один сценарий можно запустить из другого точно так же, как и из командной строки, и так же, как любую утилиту UNIX. Выполняться этот сценарий будет тоже стандартно: система подберет и запустит для него соответствующий интерпретатор, передаст ему строку вызова в качестве параметров и т. д. Понятно, что если в нем изменить какую-нибудь переменную окружения, родительский процесс этого не заметит. Больше того, изо всех переменных, определенных в родительском shell в окружение дочернего процесса, попадут только проэкспортированные командой export и те, что достались самому shell с окружением (от его родителя, например, от init). Остальные переменные будут считаться локальными и никуда не перейдут. Если мы хотим вызвать из одного сценария другой так, чтобы этот другой выполнялся той же копией интерпретатора (а значит, сохранились бы все изменения в окружении), надо использовать специальную команду ".":

$ cat changeA 
A="NewA"
$ A=""
$ . changeA 
$ echo $A  
NewA

Порядок выполнения команд
В алгоритмически полном языке программирования должна быть возможность выполнения различных действий в зависимости от условий - проще говоря, оператор if. Он, конечно, есть и в shell:
$ if [ 5 -gt 6 ]; then echo "Bug"; else echo "Good"; fi
Good
Обратите внимание на ключ -gt, означающий greater than, и на fi в конце оператора - так в shell устроены операторные скобки (лексемы if, then, необязательное else и fi ограничивают условие, условную часть и необязательную противоусловную часть; они могут находиться в одной строке, а могут и в разных).
Оказывается, эта простая конструкция очень неплохо доработана для связывания команд. Начать с того, что [ - это совсем не конструкция языка, а утилита:
$ ls -ial /bin/[ /bin/test
35 -r-xr-xr-x 2 root wheel 89992 5 июн 2003 /bin/[
35 -r-xr-xr-x 2 root wheel 89992 5 июн 2003 /bin/test
Как мы видим, [ и test - имена одного и того же файла (с индексным дескриптором 35) откуда, между прочим, следует, что описание содержимого квадратных скобок стоит искать в руководстве по test, а не в руководстве по sh. Команда test принимает такие же точно параметры, что и [ (только ей не нужна завершающая квадратная скобка). Это значит, что [ 5 -gt 6 ] - на самом деле самая обычная команда, и после if может использоваться не только она, но и любая другая команда UNIX, и даже несколько. Все их if запустит и проверит код возврата последней. Если команда выполнилась успешно, код возврата будет нулевым, и if выполнит условную часть. Если неуспешно - ненулевым (тогда он равен номеру ошибки), и if выполнит противоусловную часть.
Это слегка расходится с распространенным (по вине языка Си) мнением, что в условных выражениях "ложь" представляется нулем, а "истина" - не нулем; зато в сценариях приносит немало пользы. Код возврата (называемый также кодом ошибки) только что завершившейся команды shell записывает в переменную ?, и его можно обработать с помощью оператора выбора case $? ... esac, описанного в руководстве.
У команды test, помимо арифметических сравнений, есть немало других ключей для нужд сценариев и связи с системой. Например, test -z "$N" выполняется успешно, если переменная N пуста, test -f имя_файла - если существует файл с таким именем, test -x имя_файла - если файл доступен для чтения, test файл_1 -ot файл_2 - если первый файл старше второго и т. п. С помощью test можно создавать командные сценарии, манипулирующие файлами, решать задачи архивирования, обновления и преобразования каталогов и т. п., не используя ничего, кроме shell и системных утилит.
Если необходимо выполнение одной команды поставить в зависимость от успешного выполнения другой, можно задействовать if. Однако легче воспользоваться конструкциями, перенесенными в shell из языка программирования Си: конъюнкцией (операция &&, логическое "и") и дизъюнкцией (операция ||, логическое "или") команд. В цепочке команда_1 && команда_2 вторая команда выполнится только в случае успешного выполнения первой; в цепочке команда_1 || команда_2 - только в случае неуспешного выполнения. Связывание команд нередко позволяет избавиться от многоэтажных вложенных if.
В shell, как и в каждом процедурном языке программирования, есть операторы цикла: цикл с условием while команды; do команды; done и цикл с перебором списка for переменная in список; do команды; done. Про первый из них можно сказать только, что истиной, как и в операторе if, считается успешное выполнение последней команды в секции условия. В цикле for списком называется последовательность слов и разделителей, которая обрабатывается по тем же правилам, что и командная строка. В результате цикл будет выполняться столько раз, сколько слов оказалось в списке, а переменная на каждом обороте цикла будет содержать очередное слово:
$ for N in 1 2 3 4 5; do echo -n "==$N=="; done
==1====2====3====4====5==$
Заметим, что, поскольку команда echo -n не печатает перевода строки в конце выдачи, результат вывелся в одну строчку; в конце этой же строки shell вывел и свою подсказку - $. Некоторые командные интерпретаторы (например, zsh) очищают строку, в которой собираются вывести подсказку, что может слегка озадачить. В таком случае помогает echo без параметров, добавленная после всех команд, - она выводит дополнительный перевод строки:
zsh$ for N in 1 2 3 4 5; do echo -n "==$N=="; done #
выдачи мы не увидим :(
zsh$ for N in 1 2 3 4 5; do echo -n "==$N=="; done; echo #
вот она :)
==1====2====3====4====5==
Цикл вида for переменная; do команды; done в тексте сценария проходит по всем параметрам командной строки; это то же самое, что и for переменная in "$@".... Мелочь, а приятно: чаще всего приходится разбираться с именами файлов, которые возникают в командной строке в великом множестве на месте шаблонов. Если в таком разборе в начале командной строки были ключи, а потом уже шел список файлов, удобно пользоваться командой shift, которая выбрасывает первый (или с первого по N-ный) параметр, ставит на его место второй (N+1-й) и т. д., то есть "сдвигает" список параметров.

Ввод/вывод
Операции ввода-вывода и работа с файлами, строго говоря, не вытекают из требований алгоритмической полноты, однако любой не совсем уж теоретический язык программирования обязательно их имеет для связи с реальным миром, т. е. с пользователем и системой. Есть все это и в shell. С операцией вывода мы уже знакомы - это echo. Операция ввода имеет формат read список_переменных; она читает со стандартного ввода строку, делит ее на слова по правилам разбора командной строки и помещает первое слово в первую переменную, второе - во вторую и т. д. В последнюю переменную попадает остаток строки, независимо от количества слов в нем. Если переменных, наоборот, больше, чем слов, стоящие в конце остаются пустыми.
Работа с файлами устроена с наименьшим расходом символов. Для того чтобы перенаправить вывод команды в файл, достаточно написать после нее > файл, при этом старое содержимое файла (если он был) пропадет. Операция команда >> файл дописывает вывод команды в конец файла. Для того чтобы команда вводила из файла, а не с терминала, ее надо запустить так: команда < файл. Например, cat < fileIn > fileOut ничего не будет ни вводить с клавиатуры, ни выводить на экран - ничего, кроме диагностических сообщений вроде cannot open fileIn: No such file or directory.
Дело в том, что сообщения об ошибках направляются особым путем, на так называемый стандартный вывод ошибок (дескриптор 2, stderr). Изначально и стандартный вывод (дескриптор 1, stdout), и стандартный вывод ошибок направлены в терминал, а операция > перенаправляет именно стандартный вывод. Это сделано для того, чтобы штатные сообщения, которые, возможно, какая-нибудь другая программа будет автоматически обрабатывать, не смешивались с нештатными, которые могут быть весьма разнообразными и зависят от вида и версии shell, от системы и т. п. Ошибки можно записать в другой файл, написав команда 2> файл_с_ошибками. Если необходимо направить диагностические сообщения туда же, куда и стандартные, используйте явное указание номера дескриптора в правой части ">": cat file1 > file2 2>&1.
Крайняя простота перенаправления ввода/вывода в shell делает удобной связь между программами посредством файлов с данными. Но чаще обмен данными организуют напрямую: вывод одной программы направляется на вход другой. В UNIX есть средство, именуемое каналом (pipe, он же fifo), позволяющее удобно и прозрачно для самих процессов это организовать. В shell каналом пользуется уже известная нам конструкция команда1 | команда2 (например, ls -l | less, направляет выдачу ls на вход less, которая показывает ее постранично). Работает это очень быстро (через буфер обмена в памяти системы), записывается просто, поэтому из команд, связанных по вводу/выводу, можно строить целые конвейеры:
$ ls -1 /bin | grep o | sort | tail -3
domainname
echo
hostname
Простота перенаправления ввода/вывода имеет свои корни. На самом деле в UNIX работа программ с файлами, с терминалом и другими устройствами, с каналом и т. п. организована одинаково, поэтому процесс, мирно выводящий текст на стандартный вывод, может и не подозревать, что это - файл, или это - канал, и с другой его стороны все читает какой-то процесс. Между прочим, в конвейер можно встроить команду | tee файл | , которая раздвоит поток данных и направит одну ветку в файл (tee - английское название буквы "T", отражающей суть работы этой утилиты).
Сгруппировать потоки ввода/вывода нескольких команд (например, перенаправить весь вывод в один файл) можно, окружив их скобками - круглыми или фигурными. Для выполнения команд в круглых скобках запустится отдельная копия командного интерпретатора, а фигурные только дают указание текущему shell группировать ввод/вывод; это, так сказать, дополнительные операторные скобки, которые можно использовать, если shell не предоставил основных (таких, как while ... do ... done).
Мощный механизм связывания - подстановка вывода команды. Обычно подстановка вывода происходит, если заключить команду в обратные апострофы "`", этот символ еще зовется гравис; во многих shell сработает и конструкция $(команда). Подстановка вывода происходит так: сначала выполняется команда, а все, что она выдает на стандартный вывод, передается shell в виде строки:
$ echo `ls -1s`
total 64 56 fhs-2.2.ps.gz 4 myscript 4 o
Заметим, что, хотя ls -1 выдает список файлов в столбик, shell, передавая параметры команде echo, считает переводы строки такими же разделителями, как пробелы или символы табуляции, и echo уже не видит их.
Работа с процессами
В языке программирования общего назначения управление задачами обычно перекладывается на систему, и работают с ними при помощи какой-нибудь системной библиотеки. Но раз уж shell - оболочка, то она-то как раз должна уметь управлять процессами как можно проще. В UNIX нет единого системного вызова "запустить программу как новый процесс", потому что это действие с точки зрения системы не атомарно. Для запуска нового процесса следует, с одной стороны, зарегистрировать еще один процесс в таблице процессов системы, а с другой - запустить новую программу. Нередко новый процесс регистрировать не надо, потому что одна программа запускает другую вместо себя (попросту передает ей управление и исчезает). Для этого в системе предусмотрены вызовы семейства exec(). В shell тоже есть команда exec, запускающая процесс вместо текущей копии командного интерпретатора.
В программе (например, на Си) первую половину неатомарной операции "запустить программу как новый процесс" выполняет системный вызов fork() ("вилка", "развилка"). В результате вызова fork появляется два совершенно одинаковых процесса (окружение, память и даже файловые дескрипторы родительского процесса копируются дочернему), которые отличаются только PID, а еще тем, что дочернему процессу fork возвращает ноль, а родительскому - PID дочернего. Разобравшись в родственных отношениях, процессы могут спокойно заниматься своими делами.
Чаще всего порожденный процесс немедленно выполняет exec, чем, собственно, и завершается упомянутая неатомарная операция. Обычный запуск команды в shell устроен именно так, причем родительский процесс дожидается завершения работы дочернего в помощью системного вызова wait(). Для того чтобы shell запустить фоновый процесс, а самому продолжить работу, нужно всего лишь не вызывать wait, что и происходит, когда в конце командной строки стоит &:
$ ( sleep 5; echo "Hi there!" ) &
$ # прошло пять секунд
$ Hi there!
[1]  Done    (sleep 5; echo Hi there!)
Заметим, что "Hi there!" вывелось прямо поверх командной строки, в которой мы могли в это время набирать другую команду. Получившаяся мешанина кого угодно собьет с толку, поэтому вывод фоновых процессов рекомендуется перенаправлять. Что касается ввода, то его перенаправлять обязательно: фоновый процесс не имеет права читать с терминала. PID последней запущенной в фоне команды хранится в переменной!

Профили
Допустим, мы написали некоторый командный сценарий, настраивающий сообразно нашему вкусу окружение shell. Этот сценарий можно "втянуть" при помощи команды "." (см. выше). Было бы неплохо, если бы каждый раз при входе в систему shell обрабатывал этот сценарий, не дожидаясь команды. Поскольку задача эта, во-первых, хорошо автоматизируется, а во-вторых, упрощает взаимодействие с системой, она, скорее всего, уже решена.
Когда при входе в систему login запускает командный интерпретатор, тому в качестве нулевого параметра (имя самой программы) передается, скажем, не sh, а -sh. В результате shell начинает вести себя особенным образом, как стартовый командный интерпретатор (т. н. login shell). В частности, он считывает и выполняет специальный сценарий, называемый общий профиль (для командных интерпретаторов гнезда Bourne - это файл /etc/profile) и собственный профиль пользователя (для sh это файл $HOME/.profile), в который и стоит вписывать свои изменения в настройке окружения (командные интерпретаторы семейства C пользуются /etc/csh.login и .login соответственно).
Современные оболочки идут в этом плане гораздо дальше стандартного sh. Вот, например, описание того, как устроены входные и выходные сценарии zsh. По аналогии со стандартным профилем, каждый из этих файлов бывает общим (обычно находится в /etc) или личным (находится в домашнем каталоге пользователя, имя файла начинается на "."). Сначала любой запущенный zsh выполняет общий, а затем - личный файл zshenv. Потом, если командный интерпретатор - стартовый, выполняется общий и личный профили (zprofile, а за неимением такового - profile). Далее, если командный интерпретатор - интерактивный (запущен для диалога с пользователем, а не для выполнения сценария), выполняется общий и личный zshrc. Наконец, стартовый zsh выполняет общий и личный файлы zlogin (в нем можно пользоваться определенными в .zshrc функциями и сокращениями). По окончании сеанса работы пользователя, при завершении стартового zsh, выполнится сначала личный, а затем - общий zlogout.
И на дуде игрец
В чем еще современные оболочки опережают старый добрый /bin/sh?
Многие полезные и часто используемые команды (test, echo, kill, wait и т. п.) в современных shell делают встроенными (built-in): это весьма увеличивает быстродействие и разгружает систему, потому что встроенные команды - подпрограммы shell - не требуют ресурсоемких системных операций с процессами и переключения контекста процесса.
Во многих shell есть массивы, а в zsh предусмотрена поддержка ассоциативных массивов (по сути дела, индексированных списков). Использование ассоциативных массивов очень удобно при обработке текстов, но требует изрядного усложнения синтаксиса языка, в частности в плане подстановки значений.
С подстановкой значений переменных (variable expansion) в zsh творится вообще непонятно что. Включая в язык разнообразные модификаторы, позволяющие по ходу подстановки отрезать части содержимого переменной, заменять некоторые символы другими, превращать подстановку в несколько слов, выравнивать по ширине и т. д., разработчики zsh напрочь забывают об У! Мы насчитали более 50 (!) таких модификаторов, так что всякий раз, когда хочется ими воспользоваться, нужно заглядывать в руководство. Единственное, чего авторы zsh этим достигли, - вместо вызова специализированной внешней программы, вроде sed или awk, можно воспользоваться вложенными подстановками. Сценарии и вправду работают быстрее, но следует помнить, что командный интерпретатор, в отличие от названных утилит, не предназначен для обработки текстов!
Многие shell толкуют содержимое PS1 на свой лад. Впереди опять-таки zsh, в котором определено даже нечто вроде условного оператора внутри PS1 (используется для урезания длины подсказки).
Перенаправление ввода/вывода тоже со временем обогащается удобными сокращениями. Например, часто вместо > файл 2>&1 вводится операция >& файл, а в zsh есть еще и |&, заталкивающая оба потока вывода (стандартный и ошибки) в канал.
И наконец, по мере усложнения и распространения командного интерпретатора нарастает неоднозначность толкования некоторых его конструкций. Точнее, ширится и круг желающих, чтобы нечто работало "так", и круг желающих, чтобы оно работало "эдак". Например, что делать с шаблоном 'a*', если файлов, начинающихся на "a", вообще нет? Выдавать ошибку или передавать строку 'a*' команде? В tcsh и bash подобные вопросы решались с помощью ключей самого shell, и в zsh тоже, но в нем-то таких неоднозначностей более сотни! Так что от ключей пришлось отказаться. Вместо этого для каждого модификатора поведения придумали осмысленное собственное имя и ввели команду setopt.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика