Программирование в стандарте POSIX

Язык shell

Разбить на страницы
Показывать лекцию целиком

Основные понятия языка shell

В дальнейшем изложении слово shell будет употребляться в двух смыслах - как имя языка программирования и как название командного интерпретатора.

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

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

Выделим основные понятия языка shell на лексическом уровне.

Под пробелом в дальнейшем понимается не только собственно пробел, но также и символ табуляции.

Слово - это лексема, отличная от знака операции.

Имя - последовательность букв, цифр, символов подчеркивания, начинающаяся с буквы или подчеркивания.

Параметр - имя, цифра или любой из символов ?, !.

Комментарий - лексема, начинающаяся с символа #, а также вся последующая часть строки.

На синтаксическом уровне различаются несколько видов команд.

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

Значение простой команды - ее код завершения.

Команда - это либо простая команда, либо одна из управляющих конструкций (см. далее). Кодом завершения команды является код завершения последней выполненной простой команды.

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

Список - последовательность из одного или нескольких разделенных символами ;, , или || конвейеров, она может заканчиваться символами ; или . Из четырех указанных операций ; и имеют равные приоритеты, меньшие, чем у и ||. Приоритеты последних также равны между собой. Символ ; означает, что конвейеры будут выполняться последовательно, а - параллельно (т. е. shell не ожидает завершения конвейера ). Операция ( || ) означает, что список, следующий за ней, будет выполняться лишь в том случае, если код завершения предыдущего конвейера нулевой (ненулевой). В списке в качестве разделителя конвейеров вместо символа ; можно использовать символ перевода строки.

Командная строка - строка текста на языке shell.

Shell-процедура - файл, содержащий программу на языке shell.

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

Далее при описании синтаксиса конструкций языка shell и способа вызова служебных программ будут использоваться следующие соглашения:

  • в квадратные скобки заключаются необязательные части конструкций;
  • многоточие означает повторение предыдущей части конструкции произвольное число раз.
  • Конвейеры и примеры их использования

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

    Например, утилита ls не подсчитывает число файлов в каталоге, а лишь выдает информацию о них. С другой стороны, служебная программа wc способна подсчитать число строк в файле, но не имеет отношения к распечатке содержимого каталогов. Если же построить конвейер из двух упомянутых команд, количество файлов в каталоге легко вычисляется. Например, результатом работы конвейера (см. листинг 2.1) на нашей установке ОС Linux будет число 92 (утилита wc, вызванная без аргументов, обрабатывает файл стандартного ввода, который в данном случае является результатом работы команды ls ). Значит, в каталоге /bin 91 файл, если считать и элементы, соответствующие текущему и вышележащему каталогам (первая строка выдачи ls содержит суммарное число блоков, занятых файлами каталога).

    ls -al /bin | wc -l

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

    ls -al | grep "Oct "

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

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

    ls -al | grep "Oct " | wc -l

    Здесь утилиту grep с еще большим правом можно назвать фильтром.

    Приведем еще один пример конвейера, полезного, когда нужна подробная информация о большом каталоге (см. листинг 2.4):

    ls -Rl /dev | more

    Связующее звено между последовательными компонентами конвейера называется каналом . Иными словами, для интерпретации конвейера shell создает временный файл типа "канал", с одного конца в него заносят данные, а с другого - читают. С помощью служебной программы tee можно организовать ответвление канала, т. е. помещать информацию не только на стандартный вывод, но и в указанные файлы:

    tee файл ...

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

    ls -al | grep "Oct" | tee /tmp/tmpinf | wc -l

    В результате его выполнения на экране появится сообщение о количестве нужных файлов, а в файле /tmp/tmpinf - информация о них.

    Переменные и аргументы shell-процедур

    Переменные обозначаются именами. Значения могут присваиваться им привычным способом, то есть посредством команд вида:

    имя=значение [имя=значение] ...

    Все значения в языке shell трактуются как текстовые. Подчеркнем, что, в соответствии с этими требованиями, конструкция

    имя=значение

    должна представлять собой одно слово - в ней не может быть пробелов.

    Обычно в языках программирования ясно из контекста, где подразумевается имя переменной, а где значение. Так, в левой части оператора присваивания обычно используется имя, в правой - значение. В shell все не так. Переход от имени переменной к значению помечается посредством явной операции $. Если в команде встречается конструкция

    $имя

    то вместо нее интерпретатор shell подставляет значение переменной с указанным именем. Допускается и запись

    ${имя}

    с тем же смыслом, если нужно отделить имя от последующего текста.

    Рассмотрим пример. После выполнения команд (см. листинг 2.6) - утилита echo (эхо) выдает на стандартный вывод свои аргументы - на экране появится результат вывода значений переменных (см. листинг 2.7).

    a=value_of_variable
    b=1+2
    echo a = $a
    echo b = $b
    a = value_of_variable
    b = 1+2

    Значения формальных аргументов shell-процедур обозначаются как

    $цифра

    $0 - это имя интерпретируемой shell-процедуры. Если заданных при вызове команды фактических аргументов меньше, чем 9, "лишние" формальные аргументы получают пустые значения. О том, как добраться до фактических аргументов с номерами большими, чем 9, будет сказано ниже (см. управляющую конструкцию for и команду shift ).

    В качестве примера рассмотрим shell-процедуру, которая выдает на стандартный вывод свое имя и значения трех первых аргументов (см. листинг 2.8).

    echo Имя команды: $0
    echo Значение первого аргумента: $1
    echo Значение второго аргумента: $2
    echo Значение третьего аргумента: $3

    Пусть приведенный текст помещен в файл с именем three_args. Тогда после выполнения команды (см. листинг 2.9) на экране появится ее результат (см. листинг 2.10).

    three_args arg1 . - arg4
    Имя команды: three_args
    Значение первого аргумента: arg1
    Значение второго аргумента: .
    Значение третьего аргумента: -

    Поскольку в тексте shell-процедуры упомянуты только первые 3 аргумента, значения аргументов с большими номерами (даже если они заданы, как в приведенном примере) не влияют на ее работу.

    Команда

    three_args arg1 --

    выдаст другой результат (см. листинг 2.11):

    Имя команды: three_args
    Значение первого аргумента: arg1
    Значение второго аргумента: --
    Значение третьего аргумента:

    Значение третьего формального аргумента пусто.

    Прежде чем начнется выполнение командной строки, т. е. будет вызвана заданная в ней команда, строку обрабатывает shell, выполняющий в ней некоторые подстановки. С одной из них - значения переменной вместо конструкции $имя - мы уже познакомились. Язык shell содержит ряд аналогичных конструкций. Рассмотрим их.

    Там, где в командной строке встречается запись вида

    ${имя:-слово}

    вместо нее подставляется значение переменной с указанным именем, если это значение непусто, в противном случае подставляется слово. Например, при наличии в shell-процедуре присваивания

    initdir=${1:-/}

    переменная initdir получит значение первого аргумента, если оно непусто. Если же процедуру вызвали без аргументов, значением initdir станет символ /. Подобные конструкции - удобный способ использования подразумеваемых значений.

    Запись вида

    ${имя:=слово}

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

    Конструкция

    ${имя:?слово}

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

    Вместо конструкции

    ${имя:+слово}

    подставляется слово, если значение переменной с заданным именем непусто; в противном случае не подставляется ничего. Такой способ удобен для использования альтернативных значений.

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

    Shell содержит базовые средства для обработки цепочек символов. Так, вместо конструкции

    ${#имя}

    подставляется число символов в значении переменной с заданным именем.

    Предоставляется четыре конструкции для сопоставления с образцом:

    ${имя%слово}
    ${имя%%слово}
    ${имя#слово}
    ${имя##слово}

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

    Приведем несколько примеров. Для их понимания достаточно знать, что шаблон * сопоставляется с произвольной цепочкой символов, в том числе с пустой.

    После присваивания переменной

    HOME=/home/galat

    команда

    echo ${#HOME}

    выдаст 11.

    Заменить в имени файла суффикс .f на .for можно с помощью команд, показанных в листинге 2.12:

    f=file.f
    mv $f ${f%.f}.for

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

    f=маршрутное_имя
    echo ${f%%/*}

    Последний элемент выдается командой, приведенной в листинге 2.14:

    echo ${f##*/}

    Служебные переменные языка shell

    Значения некоторых переменных устанавливаются самим языком shell. Перечислим эти переменные и опишем их предназначение.

    #

    Количество фактических аргументов (десятичное).

    -

    Флаги (однобуквенные опции ), указанные при запуске shell или установленные посредством команды set (см. далее).

    ?

    Десятичное значение, возвращенное предыдущей синхронно выполненной командой.

    $

    Идентификатор процесса, в рамках которого выполняется shell.

    !

    Идентификатор последнего асинхронно запущенного процесса.

    *, @

    Совокупность всех фактических аргументов (начиная с $1 ), разделенных пробелами.

    Напомним: чтобы получить значения этих переменных, перед ними нужно поставить знак $.

    Значения $@ и $* имеют некоторые тонкие различия, на которых мы, однако, останавливаться не будем.

    Несколько усложним процедуру three_args, чтобы продемонстрировать только что описанные возможности (см. листинг 2.15).

    echo Идентификатор текущего процесса: $$
    echo Имя команды: $0
    echo Число фактических аргументов: $#
    echo Совокупность всех аргументов: $@
    echo Значение первого аргумента: $1
    echo Значение второго аргумента: $2
    echo Значение третьего аргумента: $3

    Теперь, вызывая усовершенствованную процедуру three_args, выполним командную строку

    three_args arg1 . - arg4

    Тогда на экране появится примерно следующее (см. листинг 2.16):

    Идентификатор текущего процесса: 3882
    Имя команды: three_args
    Число фактических аргументов: 4
    Совокупность всех аргументов: arg1 . - arg4
    Значение первого аргумента: arg1
    Значение второго аргумента: .
    Значение третьего аргумента: -

    Окружение процессов

    Окружение - это набор пар (имя, значение), который передается выполняемой программе так же, как и обычный список аргументов. Иными словами, порождаемые процессы наследуют окружение процесса-предка. Компонентами окружения являются, помимо прочих, следующие переменные и их значения:

    HOME

    Подразумеваемый аргумент утилиты смены текущего каталога cd - домашний каталог пользователя.

    IFS

    Цепочка символов, где перечислены разделители полей; обычно включает пробел, табуляцию и перевод строки.

    PATH

    Список имен каталогов для поиска команд. В дальнейшем подобные списки называются списками поиска. Элементы списка разделяются двоеточием. Пустой элемент означает текущий каталог.

    PS1

    Основное приглашение интерактивного языка shell (по умолчанию " $ ").

    TERM

    Тип пользовательского терминала.

    TZ

    Информация о часовом поясе.

    Для отсылки информации об окружении на стандартный вывод следует воспользоваться командой

    env

    Поясним подробнее смысл некоторых переменных окружения.

    Прежде чем выполнить команду, shell ищет файл с соответствующим именем в последовательности каталогов, являющейся значением переменной PATH. Назовем подобную последовательность списком поиска. Если, например, значение $PATH суть

    /usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin

    то нужный файл будет сначала разыскиваться в каталоге /usr/local/bin, затем в /bin и т.д. Как только файл отыщется, поиск прекратится. Это важно, если в разных каталогах есть одноименные выполнимые файлы.

    Значение $IFS влияет не только на разбор команд, но и на чтение строк данных с помощью команды read (см. далее). Кроме того, первый символ из значения $IFS вставляется между фактическими аргументами при выполнении подстановки $*.

    Переменная TERM хранит тип терминала пользователя. Интерактивные утилиты (редакторы или другие программы с экранным интерфейсом, например talk ) с помощью значения $TERM настраиваются на конкретный тип терминала.

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

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

    С помощью конструкции

    export  имя[=слово]

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

    Следующая строка иллюстрирует типичное применение команды export:

    export PATH=/local/bin:$PATH

    Переменная PATH изменилась, и новое значение экспортировано в окружение.

    Команда

    export -p

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

    export HISTSIZE="1000"
    export HOME="/home/galat"
    export LANG="C"
    export LESSCHARSET="koi8-r"
    export LOGNAME="galat"
    export MAIL="/var/spool/mail/galat"
    export TTY="/dev/ttyS4"
    export USER="galat"

    Мы видим, что выдача устроена так, чтобы вновь быть поданной на вход командного интерпретатора.

    Подстановка результатов выполнения команд

    Если в командной строке встретилась цепочка символов, заключенная в обратные кавычки ( ` ), она интерпретируется как команда, стандартный вывод которой подставляется вместо упомянутой конструкции. Говорят, что в этом случае производится подстановка результатов выполнения команды, а сами обратные кавычки называют символами подстановки.

    Эквивалентной синтаксической формой для подстановки результата выполнения команды является конструкция вида

    $(команда)

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

    Опишем несколько употребительных способов использования подстановки результатов. Пусть файл filelist содержит список имен других файлов и над совокупностью последних требуется проделать некоторое действие. Если в командную строку поместить конструкцию

    ... `cat filelist` ...

    то это аналогично явному указанию в командной строке на все файлы из списка. (Утилита cat выдает на стандартный вывод содержимое файлов, заданных в качестве аргументов.) Пример подстановки результатов выполнения команды (выдача информации о файлах):

    ls -l `cat filelist`

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

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

    ${x:-$(ls)}

    команда ls выполнится только тогда, когда значение x не установлено или пусто; в противном случае выполнится команда $x, и ее результат займет место в командной строке.

    В языке shell все значения считаются текстовыми. Значит, для выполнения операций с числами нужны особые средства. Вместо конструкции вида

    $((выражение))

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

    i=$(($i+1))

    значение i (если оно было числом) увеличится на единицу.

    Стандарт POSIX обязывает поддерживать арифметику длинных целых со знаком; константы (десятичные, восьмеричные, шестнадцатеричные) должны удовлетворять требованиям языка C.

    Управляющие конструкции

    Среди прочих язык shell содержит следующие управляющие конструкции.

    Оператор цикла for:

    for имя [in слово ...]
     do список
    done

    При каждой итерации переменная имя принимает следующее значение из набора

    in слово  ...

    Если конструкция in слово ... опущена, то список выполняется для каждого формального аргумента.

    Условный оператор:

    if список_1
     then список_2
    [elif список_3
       then список_4]
     ...
    [else список_5]
    fi

    Выполняется список_1. Если код его завершения 0, то выполняется список_2, в противном случае - список_3, и если код его завершения 0, то выполняется список_4 и т.д. Если же коды завершения всех списков, использованных в качестве условий, оказались ненулевыми, выполняется else -часть ( список_5 ). Если else -часть отсутствует и ни одна then -часть не выполнялась, возвращается нулевой код завершения.

    Оператор цикла while ( until ):

    while список_1
     do список_2
    done

    Пока код завершения последней команды списка_1 есть 0, выполняются команды списка_2. При замене служебного слова while на until условие продолжения цикла меняется на противоположное. Если команды из списка_2 не выполнялись вообще, код завершения устанавливается равным нулю.

    Оператор выбора:

    case слово in
     [шаблон [| шаблон] ...) список ;;]
     ...
    esac

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

    Определение функции:

    имя () открывающая_скобка
     список
    закрывающая_скобка

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

    имя  [аргумент ...]

    Если тело функции заключено в фигурные скобки, она выполняется в рамках текущего процесса; в случае круглых скобок порождается отдельный процесс. На время выполнения функции аргументы $1, $2, ..., а также служебная переменная # получают новые значения, определенные аргументами команды вызова. Затем восстанавливаются старые значения.

    Приведем примеры использования управляющих конструкций. Сначала усовершенствуем процедуру three_args, чтобы она выдавала значения всех, а не только первых трех аргументов (см. листинг 2.18):

    echo Идентификатор текущего процесса: $$
    echo Имя команды: $0
    echo Число фактических аргументов: $#
    echo Совокупность всех аргументов: $@
    i=1
    for arg
    do
     echo Значение аргумента номер ${i}: $arg
     i=$(($i+1))
    done

    В управляющих конструкциях if и while часто используется команда test, она проверяет некоторое условие и вырабатывает нулевой или ненулевой код завершения в зависимости от того, оказалось ли условие, соответственно, истинным или ложным. Команда записывается любым из двух способов:

    test  условие

    или употребив пару квадратных скобок:

    [ условие ]

    Условие может включать, помимо прочих, следующие примитивы:

    -r файл

    Истина, если файл существует и доступен для чтения.

    -w файл

    Истина, если файл существует и доступен для записи.

    -x файл

    Истина, если файл существует и является выполнимым.

    -f файл

    Истина, если файл существует и является обычным файлом.

    -d файл

    Истина, если файл существует и является каталогом.

    -c файл

    Истина, если файл существует и является специальным символьным файлом.

    -b файл

    Истина, если файл существует и является специальным блочным файлом.

    -p файл

    Истина, если файл существует и является именованным каналом.

    -s файл

    Истина, если файл существует и имеет ненулевой размер.

    -n s1

    Истина, если цепочка символов s1 имеет ненулевую длину.

    s1 = s2

    Истина, если цепочки символов s1 и s2 равны.

    s1 != s2

    Истина, если цепочки символов s1 и s2 не равны.

    n1 -eq n2

    Истина, если целые числа n1 и n2 алгебраически равны. На месте -eq могут быть также операции сравнения -ne (не равно), -gt (больше), -ge (больше или равно), -lt (меньше), -le (меньше или равно).

    Примитивы комбинируются с помощью круглых скобок, а также следующих операций (в порядке уменьшения приоритета):

    ! Унарная операция отрицания
    -a Логическое И.
    -o Логическое ИЛИ.

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

    Приведем пример использования управляющей конструкции if. В процессе загрузки практически любой разновидности ОС Unix выполняются строки следующего или близкого вида (см. листинг 2.19):

    if [ -s ${f} ]
    then
     /bin/sh ${f} start
    fi

    Если файл, имя которого является значением переменной f, существует и имеет ненулевой размер, он выполняется с аргументом start.

    В качестве примера употребления конструкции case приведем еще один фрагмент, типичный для процесса загрузки системы (см. листинг 2.20):

    case "$1" in
    start)
     start
     ;;
    stop)
     stop
     ;;
    reload | restart)
     restart
     ;;
    condrestart)
     if [ -f /var/lock/subsys/atd ]
     then
       restart
     fi
     ;;
    *)
     echo $"Usage: $0 {start | stop | restart | condrestart}"
     exit 1
    esac

    Известные значения первого аргумента распознаются, в ответ на все прочие (шаблон *) сообщается, как пользоваться данной shell-процедурой.

    Следующий пример иллюстрирует определение и вызов функции (см. листинг 2.21).

    echo $# $1
    f ( ) {
     echo $# $1
    }
    f a b
    f b
    echo $# $1

    Если данный текст хранится в файле g, то по окончании выполнения команды (вызов shell-процедуры, содержащей функцию)

    g c d e

    будет выдан следующий результат:

    3 c
    2 a
    1 b
    3 c

    Генерация маршрутных имен файлов

    После всех подстановок, прежде чем команда начнет выполняться, в каждом составляющем ее поле осуществляется поиск символов ?, и . в начале имени файла или непосредственно после ? и [:

    * - сопоставляется с произвольной цепочкой символов, в том числе с пустой. ? - сопоставляется с произвольным символом. !, шаблону удовлетворяет любой символ, не перечисленный в скобках.

    Рассмотрим несколько примеров. Чтобы подсчитать суммарное число строк во всех C -файлах текущего каталога, достаточно выполнить команду

    wc -l *.c

    В качестве второго примера расширим приведенный выше фрагмент действий при загрузке системы (см. листинг 2.22):

    for f in /etc/rc$runlevel.d/S*
    do
     if [ -s ${f} ]
     then
       /bin/sh ${f} start
     fi
    done

    В цикле будут в алфавитном порядке запускаться все непустые файлы из каталога, соответствующего заданному уровню выполнения (обычно это /etc/rc3.d, поскольку runlevel имеет значение 3, но в данном случае это не суть важно), имена которых начинаются на S.

    Как пример использования шаблона имен файлов приведем команду

    rm -f .*.[Bb]?

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

    Читателю предлагается рассмотреть еще один способ применения шаблона имен файлов и самостоятельно ответить на вопрос, удалит ли команда

    rm -f *.[Bb][AaUu]

    файл с именем .bu.

    Наконец, рассмотрим более сложный пример. Пусть требуется так переименовать фортрановские файлы текущего каталога, чтобы окончание .f заменилось на .for. Служебная программа

    basename  цепочка_символов [суффикс]

    убирает из цепочки_символов любой оканчивающийся на / префикс и суффикс (если он есть) и выдает результат на стандартный вывод. Применим ее для решения сформулированной задачи (см. листинг 2.23).

    for f in *.f
    do
     mv $f `basename $f .f`.for
    done

    Особенностью приведенного примера является использование подстановки результатов команды как части слова (в данном случае - как части нового имени файла).

    Еще одним видом генерации маршрутных имен файлов можно было бы считать обработку символа тильда ' ~ ', однако это действие открывает, а не завершает подстановки в командной строке. Префикс слов, от тильды до ближайшего слэша (или конца слова, если слэш отсутствует), заменяется на имя домашнего каталога пользователя, входное имя которого задано префиксом. Если префикс пустой, вместо него подставляется значение переменной окружения HOME. В присваиваниях тильда распознается и обрабатывается не только в начале слова, но и после знаков равенства и двоеточия, что позволяет, в частности, естественным образом обращаться со значением переменной окружения PATH.

    Так, при подстановке результатом работы команды echo ~ может быть /home/galat.

    Перенаправление ввода/вывода

    Командный интерпретатор shell ассоциирует с каждым открытым файлом так называемый дескриптор. Дескрипторы нумеруются десятичными целыми числами, начиная с нуля. Верхняя граница зависит от реализации, но, согласно стандарту POSIX, должны поддерживаться по крайней мере десять одновременно открытых файлов (с номерами дескрипторов от 0 до 9 включительно).

    Дескрипторы с номерами 0, 1 и 2 имеют специальный смысл. Они соответствуют стандартному вводу, стандартному выводу и стандартному протоколу.

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

    Перенаправление ввода:

    <слово

    Использовать файл слово для стандартного ввода ( дескриптор файла 0).

    Перенаправление вывода с перезаписью:

    >слово
    >|слово

    Использовать файл слово для стандартного вывода (дескриптор файла 1). Имеющийся файл опустошается, а если файла нет, он создается. (При использовании первой разновидности перенаправления вывода в некоторых случаях попытка перезаписи существующего файла является ошибкой; см. далее описание опции -C команды set.)

    Перенаправление вывода с дозаписью:

    >>слово

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

    ...  2>protocol

    перенаправляет стандартный протокол (дескриптор 2) в файл по имени protocol.

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

    <номер_дескриптора
    >номер_дескриптора

    Например, чтобы перенаправить стандартный протокол туда же, куда назначен стандартный вывод, употребить конструкцию

    ...  2>1

    Если в качестве номера_дескриптора указан минус (' - '), соответствующий файл (стандартный ввод, вывод или явно заданный своим дескриптором файл) закрывается.

    Shell позволяет открыть файл одновременно на чтение и запись при помощи конструкции

    <>слово

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

    Приведем два примера. Пусть нужно измерить время выполнения некоторой команды, направив ее результаты в файл cmd.res, а данные о времени - в файл cmd.time. К цели ведет строка, приведенная в листинге 2.24:

    time  команда  >cmd.res  2>cmd.time

    Второй пример. Рассмотрим цикл, описанный в листинге 2.25:

    i=0
    while [ $i -lt 40 ]
    do
     > lost+found/g$i
     i=$(($i+1))
    done
    rm lost+found/g*

    С его помощью создается 40 файлов в каталоге lost+found, которые затем удаляются. Отметим, что перенаправляется стандартный вывод пустой команды, а в результате создается пустой файл.

    (Поясним смысл приведенного фрагмента. При проверке и коррекции файловой системы утилитой fsck в каталог /lost+found помещаются непустые файлы, на которые нет ссылок. Сложность в том, что пока утилита fsck работает, ни один файл не должен расширяться, т. е. в каталоге /lost+found должны быть заранее заготовленные пустые места.)

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

    <<слово
     вставка
    слово

    и

    <<-слово
     вставка
    слово

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

    В качестве примера использования вставок приведем файл ОС Linux /etc/tripwire/twinstall.sh, который содержит следующие фрагменты (см. листинг 2.26 и листинг 2.27):

    cat << END_OF_TEXT
    A clear-text version of the Tripwire 
    configuration file
    $TXT_CFG
    has been preserved for your inspection.  
    It is recommended that you delete this 
    file manually after you have examined it.
    END_OF_TEXT
    cat <<eof1; cat <<eof2
    Вставка 1
    eof1
    Вставка 2
    eof2

    В результате выполнения второго фрагмента будут выданы две строки:

    Вставка 1
    Вставка 2

    Экранирование

    Под экранированием имеется в виду защита символов от интерпретации со стороны языка shell. Следующие символы shell трактует по-особому; не будучи экранированными, они завершают предшествующее им слово:

    |    ;  <  >  (  )  $  `  \  "  '  
    пробел  перевод_строки

    Не являясь разделителями слов, особую роль играют для shell и символы

    *  ?  [  ]  #  ~  =  %

    Каждый из перечисленных выше символов может быть экранирован, т. е. представлять самого себя, если перед ним стоит \ либо он расположен между одиночными или двойными кавычками ( '' или "" ).

    Экранирование с помощью двойных кавычек имеет некоторые особенности. Во-первых, оно не отменяет особой трактовки символов $ и `. Иными словами, внутри двойных кавычек обычным образом выполняется подстановка значений переменных, результатов выполнения команд и результатов вычисления арифметических выражений, только результат подстановки расценивается как одно слово. Во-вторых, особая трактовка символа \ отменяется внутри двойных кавычек лишь частично: символ \ сохраняет экранирующую роль, если после него следуют:

    $  `  "  \  перевод_строки

    Приведем несколько примеров использования двойных кавычек для экранирования. В первом команда

    echo "$(echo *)"

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

    echo "$(echo "*")"

    станет звездочка. Если же аргумент " $(echo *) " предложить усовершенствованной процедуре three_args, то число фактических аргументов окажется равным единице, а список файлов текущего каталога станет значением первого аргумента.

    Рассмотрим третий пример. Пусть нужно выдать информацию о всех файлах текущего каталога, которые модифицировались последний раз 3 октября. Конвейер

    ls -al | grep "Oct  3"

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

    Еще один пример. Запись

    $(($i\*5))

    синтаксически неверна, поскольку в арифметических выражениях shell рассматривает звездочку как операцию умножения, а не как элемент шаблона имен файлов; соответственно, она не нуждается в экранировании, и символ \ становится лишним, нарушая синтаксис выражения. Таким образом, лишнее экранирование может быть вредным.

    Встроенные команды

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

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

  • синтаксическая ошибка в специальной встроенной команде способна привести к аварийному завершению shell;
  • присваивание переменным, заданное посредством специальной встроенной команды, остается в силе и после ее завершения.
  • Перечислим некоторые из специальных встроенных команд.

    .  файл

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

    eval  [аргумент ...]

    Выполнить команду, заданную аргументами eval.

    exec  [аргумент ...]

    Сменить программу процесса: в рамках текущего процесса команда, заданная аргументами exec, заменяет shell. В качестве аргументов могут указываться спецификации ввода/вывода, и, если нет никаких других аргументов, будет лишь перенаправлен ввод/вывод текущего процесса shell.

    exit  [код_завершения]

    Завершить выполнение shell с указанным кодом. При отсутствии аргумента код завершения определяется последней выполненной командой. Чтение символа конца файла также приводит к завершению shell.

    shift  [n]

    Формальные аргументы, начиная с ( n+1 )-го, переименовываются в $1 и т.д. По умолчанию n=1.

    К числу обычных встроенных команд относятся:

    cd  [каталог]

    Сделать текущим заданный каталог. Если каталог не указан, используется значение переменной окружения HOME.

    pwd

    Выводит имя текущего каталога.

    read  [переменная ...]

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

    Некоторые пояснения. С помощью команды exec осуществляется смена программы текущего процесса. Например, если пользователь привык работать в командном интерпретаторе bash, он может перейти туда по команде

    exec bash

    При этом обычный shell бесследно исчезнет.

    Аргументы специальной встроенной команды eval обрабатываются дважды: когда исходную строку интерпретирует shell и когда работает сама eval. Так, после выполнения строк

    a=1
    b=a
    eval c='$'$b
    echo $c

    на стандартный вывод будет выдана единица, поскольку аргументом eval будет оператор присваивания c=$a.

    Если файл data содержит строку

    1 2 3 4 5

    то после выполнения специальной встроенной команды read

    read a b c
    echo $c $b $a

    на стандартный вывод будет выдан следующий результат:

    3 4 5 2 1

    Текст " 3 4 5 " (остаток строки) стал значением переменной c.

    Команду shift используют при последовательной обработке аргументов shell-процедур. Например, если требуется выдать аргументы, которые являются именами существующих обычных файлов, употребляют цикл вида (см. листинг 2.28):

    while [ "$1" ]
    do
     [ -f $1 ]  echo $1
     shift
    done

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

    цифра=слово

    shell трактует как запуск программы с именем цифра=слово, т. е. оно не позволяет изменить значение формального аргумента.

    Команда set записывается в виде

    set  [опция ...] [аргумент ...]

    Из ее опций упомянем следующие:

    -a

    Экспортировать в окружение все переменные, которым производится присваивание.

    -C

    Защищать существующие файлы от перезаписи при перенаправлении вывода посредством конструкции " > " (см. выше раздел "Перенаправление ввода/вывода").

    +o

    Выдать на стандартный вывод текущие опции языка shell в виде команд, которые устанавливают те же значения и которые можно подать ему на вход.

    -x

    Устанавливает режим трассировки после выполнения всех подстановок в командных строках, но до выполнения они будут выдаваться в стандартный протокол. Опция +x выключает трассировку. Стандарт не специфицирует, будет ли трассироваться команда set +x.

    Если в команде set заданы аргументы, они станут новыми значениями $1, $2 и т.д. Типичный пример использования команды set содержится в одном из файлов ОС Linux ( /etc/rc.d/rc ) выполняемых при загрузке системы (см. листинг 2.29):

    argv1="$1"
    set `/sbin/runlevel`
    runlevel=$2
    previous=$1

    Возможностью, не обязательной с точки зрения стандарта POSIX-2001 (отнесенной к расширению "Мобильность пользователей", UP), но весьма полезной на практике, является механизм синонимов для имен простых команд. Синонимы задаются с помощью обычной встроенной команды alias:

    alias  [имя[=синоним] ...]

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

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

    alias ls="ls -a"

    Если подставленный синоним оканчивается пробелом, то анализируется следующее слово командной строки; если и оно оказывается именем синонима, подстановки продолжаются. Например, если синоним для ls задать в виде строки с завершающим пробелом

    alias ls="ls -a "

    и дополнительно ввести еще одно определение синонима

    alias l="-Rl"

    то команда, использующая синоним с завершающим пробелом,

    ls l

    будет раскрыта в строку

    ls -a -Rl

    и выдаст в длинном формате информацию о всех файлах текущего каталога и его подкаталогов.

    Команда alias без аргументов выдает список имеющихся синонимов.

    Вызов командного интерпретатора shell

    Вызов командного интерпретатора shell осуществляется командой

    sh [опция...] [командный_файл [аргумент ...]]

    или

    sh -c [опция...] командная_цепочка 
           [имя_команды [аргумент ...]]

    или

    sh  -s [опция...] [аргумент ...]

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

    Большинство опций команд sh и set (см. выше) совпадают. Им может предшествовать не только знак минус, но и плюс, что означает инвертирование их смысла. Из специфических опций команды sh выделим -i, предписывающую считать shell интерактивным. Shell будет интерактивным и тогда, когда команды читаются со стандартного ввода, направленного, как и стандартный протокол, на терминал.

    Заданные в командной строке аргументы становятся значениями фактических аргументов $1, $2 и т.д. Если при наличии опции -c задано имя_команды, то в результате интерпретации командной цепочки оно становится значением $0.

    Пример. Команда

    sh -c 'echo $0 $1 $2' a b c

    выдаст на стандартный вывод

    a b c

    Читателю предлагается самостоятельно определить, что выдаст на стандартный вывод похожая команда

    sh -c "echo $0 $1 $2" a b c

    (вместо одиночных кавычек для экранирования пробелов использованы двойные), и объяснить полученный результат.

    Командный интерпретатор можно вызвать и из программы на языке C, воспользовавшись функциями system() или popen() (см. листинг 2.30):

    #include <stdlib.h>
    int system (const char *command);
    #include <stdio.h>
    FILE *popen (const char *command, 
                 const char *mode);

    Аргумент command язык shell трактует как командную цепочку в вызове

    sh -c command

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

    code = system 
           ("cd /usr/bin; ./ls > /tmp/lst");

    Функция popen(), как и system(), вызывает выполнение указанной команды. Отличие в том, что при использовании функции popen() создается канал между вызвавшей ее программой и командой. Аргумент mode определяет, что программа будет делать с этим каналом: читать (" r ") или писать (" w ").

    Правила формирования и средства разбора командных строк

    К теме командного языка и его интерпретатора логически примыкают правила формирования и средства разбора командных строк.

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

    Разработчики служебных программ должны руководствоваться следующими правилами.

  • Имя утилиты состоит не менее чем из двух и не более чем из девяти малых латинских букв и/или цифр.
  • Имя опции - это один буквенно-цифровой символ. Опциям предшествует знак минус. После одного минуса могут располагаться несколько опций без аргументов.
  • Опции отделены от своих аргументов.
  • У опций нет необязательных аргументов.
  • Если у опции несколько аргументов, они представляются одним словом и отделяются друг от друга запятыми или экранированными пробелами.
  • Все опции располагаются в командной строке перед операндами.
  • Специальный элемент командной строки --, который не является ни опцией, ни операндом, обозначает конец опций. Все последующие слова трактуются как операнды, даже если они начинаются со знака минус.
  • Порядок разных опций в командной строке не имеет значения. Если повторяется одна опция с аргументами, последние должны интерпретироваться в порядке, указанном в командной строке.
  • Порядок интерпретации операндов может зависеть от утилиты.
  • Если операнд задает читаемый или записываемый файл, то знак минус на его месте используется только для обозначения стандартного ввода (или стандартного вывода, если из контекста ясно, что специфицируется выходной файл).
  • Стандартным средством разбора опций и их аргументов, заданных в командной строке, является служебная программа getopts (чаще всего реализуемая как обычная встроенная команда shell):

    getopts  цепочка_имен_опций переменная 
                              [аргумент ...]

    Как правило, shell-процедура, запущенная разбираемой командной строкой, вызывает утилиту getopts многократно (в цикле). При каждом таком вызове getopts помещает имя очередной выделенной ею опции в качестве значения заданной переменной. Место продолжения разбора (индекс в командной строке) запоминается в shell-переменной OPTIND, начальным значением которой служит единица.

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

    Заданную при вызове getopts переменную, а также OPTIND и OPTARG следует использовать в качестве локальных, они не должны экспортироваться в окружение.

    Если при вызове getopts указаны аргументы, разбираются они, а не элементы упомянутой выше командной строки.

    Когда в командной строке обнаруживается опция, отсутствующая в цепочке имен опций, значением заданной переменной становится знак вопроса. Если первый символ цепочки имен опций - двоеточие, то в OPTARG помещается обнаруженный символ, иначе в стандартный протокол выдается диагностическое сообщение. С точки зрения shell-процедуры, вызвавшей getopts, это должно считаться ошибкой; с точки зрения getopts - нет.

    По достижении конца опций утилита getopts возвращает положительный код завершения, а в OPTIND помещается номер первого элемента командной строки, не являющегося опцией.

    Если переменой OPTIND присваивается единица, осуществляется (новый) разбор текущей командной строки (сформированной, быть может, с помощью специальной встроенной команды set ) или текущих значений заданных при вызове getopts аргументов.

    В качестве примера использования служебной программы getopts рассмотрим фрагмент shell-процедуры cmd, где производится разбор заданных при ее вызове опций (см. листинг 2.31).

    while getopts :abo: c
    do
     case $c in
     a | b) FLAG=$c;;
     o)     OARG=$OPTARG;;
     ?)     printf "Использование: %s: [-a | -b] [-o выходной_файл] [аргумент ...]\n" $0
            exit 1;;
     esac
    done
    shift $(($OPTIND - 1))
    printf "Заданный флаг: %s\n" $FLAG
    printf "Аргумент опции o: %s\n" $OARG
    printf "Остаток командной строки: %s\n" "$*"

    Если вызвать shell-процедуру cmd любым из способов, показанных в листинге 2.32, будет выдан результат, приведенный в листинге 2.33.

    cmd -a -o f1.o,f2.o file1 -
    cmd -ba -o f1.o,f2.o -- file1 -
    cmd -o f1.o,f2.o -b -a file1 -
    Заданный флаг: a
    Аргумент опции o: f1.o,f2.o
    Остаток командной строки: file1 -

    Для разбора опций и их аргументов средствами языка C служит функция getopt() и ассоциированные с ней внешние переменные (см. листинг 2.34).

    #include <unistd.h>
    int getopt (int argc, char *const argv[], 
                    const char *optstring);
    extern char *optarg;
    extern int optind, opterr, optopt;

    В общем и целом логика ее работы та же, что и у служебной программы getopts (с соответствующим переименованием используемых переменных). Аргументы argc и argv задают командную строку в том виде, как она передается функции main(), optstring представляет собой цепочку имен опций. Переменная optind (с начальным значением 1 ) служит индексом в массиве argv []. Стандарт POSIX-2001 не специфицирует, как именно getopt() разбирает несколько расположенных поочередно (после одного знака минус) имен опций и определяет, какие опции уже обработаны.

    В итоге функция getopt() возвращает имя очередной опции из числа перечисленных в цепочке optstring (если таковое удалось выделить). При наличии у опции аргумента указатель на него помещается в переменную optarg с соответствующим увеличением значения optind.

    Если у опции аргумент отсутствует, а на первом месте в optstring задано двоеточие, оно и служит результатом.

    Если встретилось имя опции, не перечисленное в optstring, или у опции нет аргумента, а на первом месте в optstring задано не двоеточие, то результатом станет знак вопроса.

    В любой из перечисленных выше ошибочных ситуаций в переменную optopt помещается имя "проблемной" опции. Кроме того, в стандартный протокол выдается диагностическое сообщение по образу и подобию утилиты getopts. Для подавления выдачи следует присвоить переменной opterr нулевое значение.

    Наконец, если при вызове getopt() указатель argv [optind] не отмечает начало опции (например, он пуст или первый символ указуемой цепочки отличен от знака минус), результат равен -1 как признак того, что разбор опций закончен.

    Следующий пример программы (см. листинг 2.35) возвращает нас к рассмотренной выше shell-процедуре cmd. Он показывает, как можно обработать командную строку вызова утилиты, допускающей взаимоисключающие опции a и b, а также опцию o, которая должна иметь аргумент.

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа разбирает опции вызвавшей ее командной строки */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    #include <unistd.h>
    #include <stdio.h>
    int main (int argc, char *argv []) {
     int c;              /* Имя анализируемой опции                     */
     int aflg = 0;       /* Признак того, что задана опция a            */
     int bflg = 0;       /* Признак того, что задана опция b            */
     int errflg = 0;     /* Флаг наличия ошибки в командной строке      */
     int flg = '?';      /* Флаг (a или b), заданный в командной строке */
     char *ofile = NULL; /* Указатель на аргумент опции o               */
     /* Подавим стандартную диагностику */
     /* независимо от первого символа   */
     /* цепочки имен опций              */
     opterr = 0;
     while ((c = getopt (argc, argv, ":abo:")) != -1) {
       switch (c) {
         case 'a':
        aflg++;
        flg = c;
        if (bflg) {
             fprintf (stderr, "Опции a и b несовместимы\n");
          errflg++;
        }
        break;
         case 'b':
        bflg++;
        flg = c;
        if (aflg) {
             fprintf (stderr, "Опции a и b несовместимы\n");
          errflg++;
        }
        break;
         case 'o':
        ofile = optarg;
        break;
         case ':':
           fprintf (stderr, "Отсутствует аргумент опции -%c\n", optopt);
           errflg++;
           break;
         case '?':
           fprintf (stderr, "Недопустимая опция -%c\n", optopt);
           errflg++;
           break;
       }
     }
      if (errflg) {
       (void) fprintf (stderr, "Использование: %s: [-a | -b] [-o выходной_файл] "
                    "[аргумент ...]\n", argv [0]);
       return (1);
     }
     printf ("Заданный флаг: %c\n", flg);
     printf ("Аргумент опции o: %s\n", ofile);
     printf ("Остаток командной строки:");
     for (; optind < argc; optind++) {
       printf (" %s", argv [optind]);
     }
     printf ("\n");
     return 0;
    }

    Приведенная программа отличается от shell-процедуры cmd более тщательным анализом ошибочных ситуаций и детальной диагностикой.

    Страницы:

    Основные понятия языка shell

    В дальнейшем изложении слово shell будет употребляться в двух смыслах - как имя языка программирования и как название командного интерпретатора.

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

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

    Выделим основные понятия языка shell на лексическом уровне.

    Под пробелом в дальнейшем понимается не только собственно пробел, но также и символ табуляции.

    Слово - это лексема, отличная от знака операции.

    Имя - последовательность букв, цифр, символов подчеркивания, начинающаяся с буквы или подчеркивания.

    Параметр - имя, цифра или любой из символов ?, !.

    Комментарий - лексема, начинающаяся с символа #, а также вся последующая часть строки.

    На синтаксическом уровне различаются несколько видов команд.

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

    Значение простой команды - ее код завершения.

    Команда - это либо простая команда, либо одна из управляющих конструкций (см. далее). Кодом завершения команды является код завершения последней выполненной простой команды.

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

    Список - последовательность из одного или нескольких разделенных символами ;, , или || конвейеров, она может заканчиваться символами ; или . Из четырех указанных операций ; и имеют равные приоритеты, меньшие, чем у и ||. Приоритеты последних также равны между собой. Символ ; означает, что конвейеры будут выполняться последовательно, а - параллельно (т. е. shell не ожидает завершения конвейера ). Операция ( || ) означает, что список, следующий за ней, будет выполняться лишь в том случае, если код завершения предыдущего конвейера нулевой (ненулевой). В списке в качестве разделителя конвейеров вместо символа ; можно использовать символ перевода строки.

    Командная строка - строка текста на языке shell.

    Shell-процедура - файл, содержащий программу на языке shell.

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

    Далее при описании синтаксиса конструкций языка shell и способа вызова служебных программ будут использоваться следующие соглашения:

  • в квадратные скобки заключаются необязательные части конструкций;
  • многоточие означает повторение предыдущей части конструкции произвольное число раз.
  • Конвейеры и примеры их использования

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

    Например, утилита ls не подсчитывает число файлов в каталоге, а лишь выдает информацию о них. С другой стороны, служебная программа wc способна подсчитать число строк в файле, но не имеет отношения к распечатке содержимого каталогов. Если же построить конвейер из двух упомянутых команд, количество файлов в каталоге легко вычисляется. Например, результатом работы конвейера (см. листинг 2.1) на нашей установке ОС Linux будет число 92 (утилита wc, вызванная без аргументов, обрабатывает файл стандартного ввода, который в данном случае является результатом работы команды ls ). Значит, в каталоге /bin 91 файл, если считать и элементы, соответствующие текущему и вышележащему каталогам (первая строка выдачи ls содержит суммарное число блоков, занятых файлами каталога).

    ls -al /bin | wc -l

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

    ls -al | grep "Oct "

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

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

    ls -al | grep "Oct " | wc -l

    Здесь утилиту grep с еще большим правом можно назвать фильтром.

    Приведем еще один пример конвейера, полезного, когда нужна подробная информация о большом каталоге (см. листинг 2.4):

    ls -Rl /dev | more

    Связующее звено между последовательными компонентами конвейера называется каналом . Иными словами, для интерпретации конвейера shell создает временный файл типа "канал", с одного конца в него заносят данные, а с другого - читают. С помощью служебной программы tee можно организовать ответвление канала, т. е. помещать информацию не только на стандартный вывод, но и в указанные файлы:

    tee файл ...

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

    ls -al | grep "Oct" | tee /tmp/tmpinf | wc -l

    В результате его выполнения на экране появится сообщение о количестве нужных файлов, а в файле /tmp/tmpinf - информация о них.

    Переменные и аргументы shell-процедур

    Переменные обозначаются именами. Значения могут присваиваться им привычным способом, то есть посредством команд вида:

    имя=значение [имя=значение] ...

    Все значения в языке shell трактуются как текстовые. Подчеркнем, что, в соответствии с этими требованиями, конструкция

    имя=значение

    должна представлять собой одно слово - в ней не может быть пробелов.

    Обычно в языках программирования ясно из контекста, где подразумевается имя переменной, а где значение. Так, в левой части оператора присваивания обычно используется имя, в правой - значение. В shell все не так. Переход от имени переменной к значению помечается посредством явной операции $. Если в команде встречается конструкция

    $имя

    то вместо нее интерпретатор shell подставляет значение переменной с указанным именем. Допускается и запись

    ${имя}

    с тем же смыслом, если нужно отделить имя от последующего текста.

    Рассмотрим пример. После выполнения команд (см. листинг 2.6) - утилита echo (эхо) выдает на стандартный вывод свои аргументы - на экране появится результат вывода значений переменных (см. листинг 2.7).

    a=value_of_variable
    b=1+2
    echo a = $a
    echo b = $b
    a = value_of_variable
    b = 1+2

    Значения формальных аргументов shell-процедур обозначаются как

    $цифра

    $0 - это имя интерпретируемой shell-процедуры. Если заданных при вызове команды фактических аргументов меньше, чем 9, "лишние" формальные аргументы получают пустые значения. О том, как добраться до фактических аргументов с номерами большими, чем 9, будет сказано ниже (см. управляющую конструкцию for и команду shift ).

    В качестве примера рассмотрим shell-процедуру, которая выдает на стандартный вывод свое имя и значения трех первых аргументов (см. листинг 2.8).

    echo Имя команды: $0
    echo Значение первого аргумента: $1
    echo Значение второго аргумента: $2
    echo Значение третьего аргумента: $3

    Пусть приведенный текст помещен в файл с именем three_args. Тогда после выполнения команды (см. листинг 2.9) на экране появится ее результат (см. листинг 2.10).

    three_args arg1 . - arg4
    Имя команды: three_args
    Значение первого аргумента: arg1
    Значение второго аргумента: .
    Значение третьего аргумента: -

    Поскольку в тексте shell-процедуры упомянуты только первые 3 аргумента, значения аргументов с большими номерами (даже если они заданы, как в приведенном примере) не влияют на ее работу.

    Команда

    three_args arg1 --

    выдаст другой результат (см. листинг 2.11):

    Имя команды: three_args
    Значение первого аргумента: arg1
    Значение второго аргумента: --
    Значение третьего аргумента:

    Значение третьего формального аргумента пусто.

    Прежде чем начнется выполнение командной строки, т. е. будет вызвана заданная в ней команда, строку обрабатывает shell, выполняющий в ней некоторые подстановки. С одной из них - значения переменной вместо конструкции $имя - мы уже познакомились. Язык shell содержит ряд аналогичных конструкций. Рассмотрим их.

    Там, где в командной строке встречается запись вида

    ${имя:-слово}

    вместо нее подставляется значение переменной с указанным именем, если это значение непусто, в противном случае подставляется слово. Например, при наличии в shell-процедуре присваивания

    initdir=${1:-/}

    переменная initdir получит значение первого аргумента, если оно непусто. Если же процедуру вызвали без аргументов, значением initdir станет символ /. Подобные конструкции - удобный способ использования подразумеваемых значений.

    Запись вида

    ${имя:=слово}

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

    Конструкция

    ${имя:?слово}

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

    Вместо конструкции

    ${имя:+слово}

    подставляется слово, если значение переменной с заданным именем непусто; в противном случае не подставляется ничего. Такой способ удобен для использования альтернативных значений.

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

    Shell содержит базовые средства для обработки цепочек символов. Так, вместо конструкции

    ${#имя}

    подставляется число символов в значении переменной с заданным именем.

    Предоставляется четыре конструкции для сопоставления с образцом:

    ${имя%слово}
    ${имя%%слово}
    ${имя#слово}
    ${имя##слово}

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

    Приведем несколько примеров. Для их понимания достаточно знать, что шаблон * сопоставляется с произвольной цепочкой символов, в том числе с пустой.

    После присваивания переменной

    HOME=/home/galat

    команда

    echo ${#HOME}

    выдаст 11.

    Заменить в имени файла суффикс .f на .for можно с помощью команд, показанных в листинге 2.12:

    f=file.f
    mv $f ${f%.f}.for

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

    f=маршрутное_имя
    echo ${f%%/*}

    Последний элемент выдается командой, приведенной в листинге 2.14:

    echo ${f##*/}

    Служебные переменные языка shell

    Значения некоторых переменных устанавливаются самим языком shell. Перечислим эти переменные и опишем их предназначение.

    #

    Количество фактических аргументов (десятичное).

    -

    Флаги (однобуквенные опции ), указанные при запуске shell или установленные посредством команды set (см. далее).

    ?

    Десятичное значение, возвращенное предыдущей синхронно выполненной командой.

    $

    Идентификатор процесса, в рамках которого выполняется shell.

    !

    Идентификатор последнего асинхронно запущенного процесса.

    *, @

    Совокупность всех фактических аргументов (начиная с $1 ), разделенных пробелами.

    Напомним: чтобы получить значения этих переменных, перед ними нужно поставить знак $.

    Значения $@ и $* имеют некоторые тонкие различия, на которых мы, однако, останавливаться не будем.

    Несколько усложним процедуру three_args, чтобы продемонстрировать только что описанные возможности (см. листинг 2.15).

    echo Идентификатор текущего процесса: $$
    echo Имя команды: $0
    echo Число фактических аргументов: $#
    echo Совокупность всех аргументов: $@
    echo Значение первого аргумента: $1
    echo Значение второго аргумента: $2
    echo Значение третьего аргумента: $3

    Теперь, вызывая усовершенствованную процедуру three_args, выполним командную строку

    three_args arg1 . - arg4

    Тогда на экране появится примерно следующее (см. листинг 2.16):

    Идентификатор текущего процесса: 3882
    Имя команды: three_args
    Число фактических аргументов: 4
    Совокупность всех аргументов: arg1 . - arg4
    Значение первого аргумента: arg1
    Значение второго аргумента: .
    Значение третьего аргумента: -

    Окружение процессов

    Окружение - это набор пар (имя, значение), который передается выполняемой программе так же, как и обычный список аргументов. Иными словами, порождаемые процессы наследуют окружение процесса-предка. Компонентами окружения являются, помимо прочих, следующие переменные и их значения:

    HOME

    Подразумеваемый аргумент утилиты смены текущего каталога cd - домашний каталог пользователя.

    IFS

    Цепочка символов, где перечислены разделители полей; обычно включает пробел, табуляцию и перевод строки.

    PATH

    Список имен каталогов для поиска команд. В дальнейшем подобные списки называются списками поиска. Элементы списка разделяются двоеточием. Пустой элемент означает текущий каталог.

    PS1

    Основное приглашение интерактивного языка shell (по умолчанию " $ ").

    TERM

    Тип пользовательского терминала.

    TZ

    Информация о часовом поясе.

    Для отсылки информации об окружении на стандартный вывод следует воспользоваться командой

    env

    Поясним подробнее смысл некоторых переменных окружения.

    Прежде чем выполнить команду, shell ищет файл с соответствующим именем в последовательности каталогов, являющейся значением переменной PATH. Назовем подобную последовательность списком поиска. Если, например, значение $PATH суть

    /usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin

    то нужный файл будет сначала разыскиваться в каталоге /usr/local/bin, затем в /bin и т.д. Как только файл отыщется, поиск прекратится. Это важно, если в разных каталогах есть одноименные выполнимые файлы.

    Значение $IFS влияет не только на разбор команд, но и на чтение строк данных с помощью команды read (см. далее). Кроме того, первый символ из значения $IFS вставляется между фактическими аргументами при выполнении подстановки $*.

    Переменная TERM хранит тип терминала пользователя. Интерактивные утилиты (редакторы или другие программы с экранным интерфейсом, например talk ) с помощью значения $TERM настраиваются на конкретный тип терминала.

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

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

    С помощью конструкции

    export  имя[=слово]

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

    Следующая строка иллюстрирует типичное применение команды export:

    export PATH=/local/bin:$PATH

    Переменная PATH изменилась, и новое значение экспортировано в окружение.

    Команда

    export -p

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

    export HISTSIZE="1000"
    export HOME="/home/galat"
    export LANG="C"
    export LESSCHARSET="koi8-r"
    export LOGNAME="galat"
    export MAIL="/var/spool/mail/galat"
    export TTY="/dev/ttyS4"
    export USER="galat"

    Мы видим, что выдача устроена так, чтобы вновь быть поданной на вход командного интерпретатора.

    Подстановка результатов выполнения команд

    Если в командной строке встретилась цепочка символов, заключенная в обратные кавычки ( ` ), она интерпретируется как команда, стандартный вывод которой подставляется вместо упомянутой конструкции. Говорят, что в этом случае производится подстановка результатов выполнения команды, а сами обратные кавычки называют символами подстановки.

    Эквивалентной синтаксической формой для подстановки результата выполнения команды является конструкция вида

    $(команда)

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

    Опишем несколько употребительных способов использования подстановки результатов. Пусть файл filelist содержит список имен других файлов и над совокупностью последних требуется проделать некоторое действие. Если в командную строку поместить конструкцию

    ... `cat filelist` ...

    то это аналогично явному указанию в командной строке на все файлы из списка. (Утилита cat выдает на стандартный вывод содержимое файлов, заданных в качестве аргументов.) Пример подстановки результатов выполнения команды (выдача информации о файлах):

    ls -l `cat filelist`

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

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

    ${x:-$(ls)}

    команда ls выполнится только тогда, когда значение x не установлено или пусто; в противном случае выполнится команда $x, и ее результат займет место в командной строке.

    В языке shell все значения считаются текстовыми. Значит, для выполнения операций с числами нужны особые средства. Вместо конструкции вида

    $((выражение))

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

    i=$(($i+1))

    значение i (если оно было числом) увеличится на единицу.

    Стандарт POSIX обязывает поддерживать арифметику длинных целых со знаком; константы (десятичные, восьмеричные, шестнадцатеричные) должны удовлетворять требованиям языка C.

    Управляющие конструкции

    Среди прочих язык shell содержит следующие управляющие конструкции.

    Оператор цикла for:

    for имя [in слово ...]
     do список
    done

    При каждой итерации переменная имя принимает следующее значение из набора

    in слово  ...

    Если конструкция in слово ... опущена, то список выполняется для каждого формального аргумента.

    Условный оператор:

    if список_1
     then список_2
    [elif список_3
       then список_4]
     ...
    [else список_5]
    fi

    Выполняется список_1. Если код его завершения 0, то выполняется список_2, в противном случае - список_3, и если код его завершения 0, то выполняется список_4 и т.д. Если же коды завершения всех списков, использованных в качестве условий, оказались ненулевыми, выполняется else -часть ( список_5 ). Если else -часть отсутствует и ни одна then -часть не выполнялась, возвращается нулевой код завершения.

    Оператор цикла while ( until ):

    while список_1
     do список_2
    done

    Пока код завершения последней команды списка_1 есть 0, выполняются команды списка_2. При замене служебного слова while на until условие продолжения цикла меняется на противоположное. Если команды из списка_2 не выполнялись вообще, код завершения устанавливается равным нулю.

    Оператор выбора:

    case слово in
     [шаблон [| шаблон] ...) список ;;]
     ...
    esac

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

    Определение функции:

    имя () открывающая_скобка
     список
    закрывающая_скобка

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

    имя  [аргумент ...]

    Если тело функции заключено в фигурные скобки, она выполняется в рамках текущего процесса; в случае круглых скобок порождается отдельный процесс. На время выполнения функции аргументы $1, $2, ..., а также служебная переменная # получают новые значения, определенные аргументами команды вызова. Затем восстанавливаются старые значения.

    Приведем примеры использования управляющих конструкций. Сначала усовершенствуем процедуру three_args, чтобы она выдавала значения всех, а не только первых трех аргументов (см. листинг 2.18):

    echo Идентификатор текущего процесса: $$
    echo Имя команды: $0
    echo Число фактических аргументов: $#
    echo Совокупность всех аргументов: $@
    i=1
    for arg
    do
     echo Значение аргумента номер ${i}: $arg
     i=$(($i+1))
    done

    В управляющих конструкциях if и while часто используется команда test, она проверяет некоторое условие и вырабатывает нулевой или ненулевой код завершения в зависимости от того, оказалось ли условие, соответственно, истинным или ложным. Команда записывается любым из двух способов:

    test  условие

    или употребив пару квадратных скобок:

    [ условие ]

    Условие может включать, помимо прочих, следующие примитивы:

    -r файл

    Истина, если файл существует и доступен для чтения.

    -w файл

    Истина, если файл существует и доступен для записи.

    -x файл

    Истина, если файл существует и является выполнимым.

    -f файл

    Истина, если файл существует и является обычным файлом.

    -d файл

    Истина, если файл существует и является каталогом.

    -c файл

    Истина, если файл существует и является специальным символьным файлом.

    -b файл

    Истина, если файл существует и является специальным блочным файлом.

    -p файл

    Истина, если файл существует и является именованным каналом.

    -s файл

    Истина, если файл существует и имеет ненулевой размер.

    -n s1

    Истина, если цепочка символов s1 имеет ненулевую длину.

    s1 = s2

    Истина, если цепочки символов s1 и s2 равны.

    s1 != s2

    Истина, если цепочки символов s1 и s2 не равны.

    n1 -eq n2

    Истина, если целые числа n1 и n2 алгебраически равны. На месте -eq могут быть также операции сравнения -ne (не равно), -gt (больше), -ge (больше или равно), -lt (меньше), -le (меньше или равно).

    Примитивы комбинируются с помощью круглых скобок, а также следующих операций (в порядке уменьшения приоритета):

    ! Унарная операция отрицания
    -a Логическое И.
    -o Логическое ИЛИ.

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

    Приведем пример использования управляющей конструкции if. В процессе загрузки практически любой разновидности ОС Unix выполняются строки следующего или близкого вида (см. листинг 2.19):

    if [ -s ${f} ]
    then
     /bin/sh ${f} start
    fi

    Если файл, имя которого является значением переменной f, существует и имеет ненулевой размер, он выполняется с аргументом start.

    В качестве примера употребления конструкции case приведем еще один фрагмент, типичный для процесса загрузки системы (см. листинг 2.20):

    case "$1" in
    start)
     start
     ;;
    stop)
     stop
     ;;
    reload | restart)
     restart
     ;;
    condrestart)
     if [ -f /var/lock/subsys/atd ]
     then
       restart
     fi
     ;;
    *)
     echo $"Usage: $0 {start | stop | restart | condrestart}"
     exit 1
    esac

    Известные значения первого аргумента распознаются, в ответ на все прочие (шаблон *) сообщается, как пользоваться данной shell-процедурой.

    Следующий пример иллюстрирует определение и вызов функции (см. листинг 2.21).

    echo $# $1
    f ( ) {
     echo $# $1
    }
    f a b
    f b
    echo $# $1

    Если данный текст хранится в файле g, то по окончании выполнения команды (вызов shell-процедуры, содержащей функцию)

    g c d e

    будет выдан следующий результат:

    3 c
    2 a
    1 b
    3 c

    Генерация маршрутных имен файлов

    После всех подстановок, прежде чем команда начнет выполняться, в каждом составляющем ее поле осуществляется поиск символов ?, и . в начале имени файла или непосредственно после ? и [:

    * - сопоставляется с произвольной цепочкой символов, в том числе с пустой. ? - сопоставляется с произвольным символом. !, шаблону удовлетворяет любой символ, не перечисленный в скобках.

    Рассмотрим несколько примеров. Чтобы подсчитать суммарное число строк во всех C -файлах текущего каталога, достаточно выполнить команду

    wc -l *.c

    В качестве второго примера расширим приведенный выше фрагмент действий при загрузке системы (см. листинг 2.22):

    for f in /etc/rc$runlevel.d/S*
    do
     if [ -s ${f} ]
     then
       /bin/sh ${f} start
     fi
    done

    В цикле будут в алфавитном порядке запускаться все непустые файлы из каталога, соответствующего заданному уровню выполнения (обычно это /etc/rc3.d, поскольку runlevel имеет значение 3, но в данном случае это не суть важно), имена которых начинаются на S.

    Как пример использования шаблона имен файлов приведем команду

    rm -f .*.[Bb]?

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

    Читателю предлагается рассмотреть еще один способ применения шаблона имен файлов и самостоятельно ответить на вопрос, удалит ли команда

    rm -f *.[Bb][AaUu]

    файл с именем .bu.

    Наконец, рассмотрим более сложный пример. Пусть требуется так переименовать фортрановские файлы текущего каталога, чтобы окончание .f заменилось на .for. Служебная программа

    basename  цепочка_символов [суффикс]

    убирает из цепочки_символов любой оканчивающийся на / префикс и суффикс (если он есть) и выдает результат на стандартный вывод. Применим ее для решения сформулированной задачи (см. листинг 2.23).

    for f in *.f
    do
     mv $f `basename $f .f`.for
    done

    Особенностью приведенного примера является использование подстановки результатов команды как части слова (в данном случае - как части нового имени файла).

    Еще одним видом генерации маршрутных имен файлов можно было бы считать обработку символа тильда ' ~ ', однако это действие открывает, а не завершает подстановки в командной строке. Префикс слов, от тильды до ближайшего слэша (или конца слова, если слэш отсутствует), заменяется на имя домашнего каталога пользователя, входное имя которого задано префиксом. Если префикс пустой, вместо него подставляется значение переменной окружения HOME. В присваиваниях тильда распознается и обрабатывается не только в начале слова, но и после знаков равенства и двоеточия, что позволяет, в частности, естественным образом обращаться со значением переменной окружения PATH.

    Так, при подстановке результатом работы команды echo ~ может быть /home/galat.

    Перенаправление ввода/вывода

    Командный интерпретатор shell ассоциирует с каждым открытым файлом так называемый дескриптор. Дескрипторы нумеруются десятичными целыми числами, начиная с нуля. Верхняя граница зависит от реализации, но, согласно стандарту POSIX, должны поддерживаться по крайней мере десять одновременно открытых файлов (с номерами дескрипторов от 0 до 9 включительно).

    Дескрипторы с номерами 0, 1 и 2 имеют специальный смысл. Они соответствуют стандартному вводу, стандартному выводу и стандартному протоколу.

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

    Перенаправление ввода:

    <слово

    Использовать файл слово для стандартного ввода ( дескриптор файла 0).

    Перенаправление вывода с перезаписью:

    >слово
    >|слово

    Использовать файл слово для стандартного вывода (дескриптор файла 1). Имеющийся файл опустошается, а если файла нет, он создается. (При использовании первой разновидности перенаправления вывода в некоторых случаях попытка перезаписи существующего файла является ошибкой; см. далее описание опции -C команды set.)

    Перенаправление вывода с дозаписью:

    >>слово

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

    ...  2>protocol

    перенаправляет стандартный протокол (дескриптор 2) в файл по имени protocol.

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

    <номер_дескриптора
    >номер_дескриптора

    Например, чтобы перенаправить стандартный протокол туда же, куда назначен стандартный вывод, употребить конструкцию

    ...  2>1

    Если в качестве номера_дескриптора указан минус (' - '), соответствующий файл (стандартный ввод, вывод или явно заданный своим дескриптором файл) закрывается.

    Shell позволяет открыть файл одновременно на чтение и запись при помощи конструкции

    <>слово

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

    Приведем два примера. Пусть нужно измерить время выполнения некоторой команды, направив ее результаты в файл cmd.res, а данные о времени - в файл cmd.time. К цели ведет строка, приведенная в листинге 2.24:

    time  команда  >cmd.res  2>cmd.time

    Второй пример. Рассмотрим цикл, описанный в листинге 2.25:

    i=0
    while [ $i -lt 40 ]
    do
     > lost+found/g$i
     i=$(($i+1))
    done
    rm lost+found/g*

    С его помощью создается 40 файлов в каталоге lost+found, которые затем удаляются. Отметим, что перенаправляется стандартный вывод пустой команды, а в результате создается пустой файл.

    (Поясним смысл приведенного фрагмента. При проверке и коррекции файловой системы утилитой fsck в каталог /lost+found помещаются непустые файлы, на которые нет ссылок. Сложность в том, что пока утилита fsck работает, ни один файл не должен расширяться, т. е. в каталоге /lost+found должны быть заранее заготовленные пустые места.)

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

    <<слово
     вставка
    слово

    и

    <<-слово
     вставка
    слово

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

    В качестве примера использования вставок приведем файл ОС Linux /etc/tripwire/twinstall.sh, который содержит следующие фрагменты (см. листинг 2.26 и листинг 2.27):

    cat << END_OF_TEXT
    A clear-text version of the Tripwire 
    configuration file
    $TXT_CFG
    has been preserved for your inspection.  
    It is recommended that you delete this 
    file manually after you have examined it.
    END_OF_TEXT
    cat <<eof1; cat <<eof2
    Вставка 1
    eof1
    Вставка 2
    eof2

    В результате выполнения второго фрагмента будут выданы две строки:

    Вставка 1
    Вставка 2

    Экранирование

    Под экранированием имеется в виду защита символов от интерпретации со стороны языка shell. Следующие символы shell трактует по-особому; не будучи экранированными, они завершают предшествующее им слово:

    |    ;  <  >  (  )  $  `  \  "  '  
    пробел  перевод_строки

    Не являясь разделителями слов, особую роль играют для shell и символы

    *  ?  [  ]  #  ~  =  %

    Каждый из перечисленных выше символов может быть экранирован, т. е. представлять самого себя, если перед ним стоит \ либо он расположен между одиночными или двойными кавычками ( '' или "" ).

    Экранирование с помощью двойных кавычек имеет некоторые особенности. Во-первых, оно не отменяет особой трактовки символов $ и `. Иными словами, внутри двойных кавычек обычным образом выполняется подстановка значений переменных, результатов выполнения команд и результатов вычисления арифметических выражений, только результат подстановки расценивается как одно слово. Во-вторых, особая трактовка символа \ отменяется внутри двойных кавычек лишь частично: символ \ сохраняет экранирующую роль, если после него следуют:

    $  `  "  \  перевод_строки

    Приведем несколько примеров использования двойных кавычек для экранирования. В первом команда

    echo "$(echo *)"

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

    echo "$(echo "*")"

    станет звездочка. Если же аргумент " $(echo *) " предложить усовершенствованной процедуре three_args, то число фактических аргументов окажется равным единице, а список файлов текущего каталога станет значением первого аргумента.

    Рассмотрим третий пример. Пусть нужно выдать информацию о всех файлах текущего каталога, которые модифицировались последний раз 3 октября. Конвейер

    ls -al | grep "Oct  3"

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

    Еще один пример. Запись

    $(($i\*5))

    синтаксически неверна, поскольку в арифметических выражениях shell рассматривает звездочку как операцию умножения, а не как элемент шаблона имен файлов; соответственно, она не нуждается в экранировании, и символ \ становится лишним, нарушая синтаксис выражения. Таким образом, лишнее экранирование может быть вредным.

    Встроенные команды

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

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

  • синтаксическая ошибка в специальной встроенной команде способна привести к аварийному завершению shell;
  • присваивание переменным, заданное посредством специальной встроенной команды, остается в силе и после ее завершения.
  • Перечислим некоторые из специальных встроенных команд.

    .  файл

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

    eval  [аргумент ...]

    Выполнить команду, заданную аргументами eval.

    exec  [аргумент ...]

    Сменить программу процесса: в рамках текущего процесса команда, заданная аргументами exec, заменяет shell. В качестве аргументов могут указываться спецификации ввода/вывода, и, если нет никаких других аргументов, будет лишь перенаправлен ввод/вывод текущего процесса shell.

    exit  [код_завершения]

    Завершить выполнение shell с указанным кодом. При отсутствии аргумента код завершения определяется последней выполненной командой. Чтение символа конца файла также приводит к завершению shell.

    shift  [n]

    Формальные аргументы, начиная с ( n+1 )-го, переименовываются в $1 и т.д. По умолчанию n=1.

    К числу обычных встроенных команд относятся:

    cd  [каталог]

    Сделать текущим заданный каталог. Если каталог не указан, используется значение переменной окружения HOME.

    pwd

    Выводит имя текущего каталога.

    read  [переменная ...]

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

    Некоторые пояснения. С помощью команды exec осуществляется смена программы текущего процесса. Например, если пользователь привык работать в командном интерпретаторе bash, он может перейти туда по команде

    exec bash

    При этом обычный shell бесследно исчезнет.

    Аргументы специальной встроенной команды eval обрабатываются дважды: когда исходную строку интерпретирует shell и когда работает сама eval. Так, после выполнения строк

    a=1
    b=a
    eval c='$'$b
    echo $c

    на стандартный вывод будет выдана единица, поскольку аргументом eval будет оператор присваивания c=$a.

    Если файл data содержит строку

    1 2 3 4 5

    то после выполнения специальной встроенной команды read

    read a b c
    echo $c $b $a

    на стандартный вывод будет выдан следующий результат:

    3 4 5 2 1

    Текст " 3 4 5 " (остаток строки) стал значением переменной c.

    Команду shift используют при последовательной обработке аргументов shell-процедур. Например, если требуется выдать аргументы, которые являются именами существующих обычных файлов, употребляют цикл вида (см. листинг 2.28):

    while [ "$1" ]
    do
     [ -f $1 ]  echo $1
     shift
    done

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

    цифра=слово

    shell трактует как запуск программы с именем цифра=слово, т. е. оно не позволяет изменить значение формального аргумента.

    Команда set записывается в виде

    set  [опция ...] [аргумент ...]

    Из ее опций упомянем следующие:

    -a

    Экспортировать в окружение все переменные, которым производится присваивание.

    -C

    Защищать существующие файлы от перезаписи при перенаправлении вывода посредством конструкции " > " (см. выше раздел "Перенаправление ввода/вывода").

    +o

    Выдать на стандартный вывод текущие опции языка shell в виде команд, которые устанавливают те же значения и которые можно подать ему на вход.

    -x

    Устанавливает режим трассировки после выполнения всех подстановок в командных строках, но до выполнения они будут выдаваться в стандартный протокол. Опция +x выключает трассировку. Стандарт не специфицирует, будет ли трассироваться команда set +x.

    Если в команде set заданы аргументы, они станут новыми значениями $1, $2 и т.д. Типичный пример использования команды set содержится в одном из файлов ОС Linux ( /etc/rc.d/rc ) выполняемых при загрузке системы (см. листинг 2.29):

    argv1="$1"
    set `/sbin/runlevel`
    runlevel=$2
    previous=$1

    Возможностью, не обязательной с точки зрения стандарта POSIX-2001 (отнесенной к расширению "Мобильность пользователей", UP), но весьма полезной на практике, является механизм синонимов для имен простых команд. Синонимы задаются с помощью обычной встроенной команды alias:

    alias  [имя[=синоним] ...]

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

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

    alias ls="ls -a"

    Если подставленный синоним оканчивается пробелом, то анализируется следующее слово командной строки; если и оно оказывается именем синонима, подстановки продолжаются. Например, если синоним для ls задать в виде строки с завершающим пробелом

    alias ls="ls -a "

    и дополнительно ввести еще одно определение синонима

    alias l="-Rl"

    то команда, использующая синоним с завершающим пробелом,

    ls l

    будет раскрыта в строку

    ls -a -Rl

    и выдаст в длинном формате информацию о всех файлах текущего каталога и его подкаталогов.

    Команда alias без аргументов выдает список имеющихся синонимов.

    Вызов командного интерпретатора shell

    Вызов командного интерпретатора shell осуществляется командой

    sh [опция...] [командный_файл [аргумент ...]]

    или

    sh -c [опция...] командная_цепочка 
           [имя_команды [аргумент ...]]

    или

    sh  -s [опция...] [аргумент ...]

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

    Большинство опций команд sh и set (см. выше) совпадают. Им может предшествовать не только знак минус, но и плюс, что означает инвертирование их смысла. Из специфических опций команды sh выделим -i, предписывающую считать shell интерактивным. Shell будет интерактивным и тогда, когда команды читаются со стандартного ввода, направленного, как и стандартный протокол, на терминал.

    Заданные в командной строке аргументы становятся значениями фактических аргументов $1, $2 и т.д. Если при наличии опции -c задано имя_команды, то в результате интерпретации командной цепочки оно становится значением $0.

    Пример. Команда

    sh -c 'echo $0 $1 $2' a b c

    выдаст на стандартный вывод

    a b c

    Читателю предлагается самостоятельно определить, что выдаст на стандартный вывод похожая команда

    sh -c "echo $0 $1 $2" a b c

    (вместо одиночных кавычек для экранирования пробелов использованы двойные), и объяснить полученный результат.

    Командный интерпретатор можно вызвать и из программы на языке C, воспользовавшись функциями system() или popen() (см. листинг 2.30):

    #include <stdlib.h>
    int system (const char *command);
    #include <stdio.h>
    FILE *popen (const char *command, 
                 const char *mode);

    Аргумент command язык shell трактует как командную цепочку в вызове

    sh -c command

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

    code = system 
           ("cd /usr/bin; ./ls > /tmp/lst");

    Функция popen(), как и system(), вызывает выполнение указанной команды. Отличие в том, что при использовании функции popen() создается канал между вызвавшей ее программой и командой. Аргумент mode определяет, что программа будет делать с этим каналом: читать (" r ") или писать (" w ").

    Правила формирования и средства разбора командных строк

    К теме командного языка и его интерпретатора логически примыкают правила формирования и средства разбора командных строк.

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

    Разработчики служебных программ должны руководствоваться следующими правилами.

  • Имя утилиты состоит не менее чем из двух и не более чем из девяти малых латинских букв и/или цифр.
  • Имя опции - это один буквенно-цифровой символ. Опциям предшествует знак минус. После одного минуса могут располагаться несколько опций без аргументов.
  • Опции отделены от своих аргументов.
  • У опций нет необязательных аргументов.
  • Если у опции несколько аргументов, они представляются одним словом и отделяются друг от друга запятыми или экранированными пробелами.
  • Все опции располагаются в командной строке перед операндами.
  • Специальный элемент командной строки --, который не является ни опцией, ни операндом, обозначает конец опций. Все последующие слова трактуются как операнды, даже если они начинаются со знака минус.
  • Порядок разных опций в командной строке не имеет значения. Если повторяется одна опция с аргументами, последние должны интерпретироваться в порядке, указанном в командной строке.
  • Порядок интерпретации операндов может зависеть от утилиты.
  • Если операнд задает читаемый или записываемый файл, то знак минус на его месте используется только для обозначения стандартного ввода (или стандартного вывода, если из контекста ясно, что специфицируется выходной файл).
  • Стандартным средством разбора опций и их аргументов, заданных в командной строке, является служебная программа getopts (чаще всего реализуемая как обычная встроенная команда shell):

    getopts  цепочка_имен_опций переменная 
                              [аргумент ...]

    Как правило, shell-процедура, запущенная разбираемой командной строкой, вызывает утилиту getopts многократно (в цикле). При каждом таком вызове getopts помещает имя очередной выделенной ею опции в качестве значения заданной переменной. Место продолжения разбора (индекс в командной строке) запоминается в shell-переменной OPTIND, начальным значением которой служит единица.

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

    Заданную при вызове getopts переменную, а также OPTIND и OPTARG следует использовать в качестве локальных, они не должны экспортироваться в окружение.

    Если при вызове getopts указаны аргументы, разбираются они, а не элементы упомянутой выше командной строки.

    Когда в командной строке обнаруживается опция, отсутствующая в цепочке имен опций, значением заданной переменной становится знак вопроса. Если первый символ цепочки имен опций - двоеточие, то в OPTARG помещается обнаруженный символ, иначе в стандартный протокол выдается диагностическое сообщение. С точки зрения shell-процедуры, вызвавшей getopts, это должно считаться ошибкой; с точки зрения getopts - нет.

    По достижении конца опций утилита getopts возвращает положительный код завершения, а в OPTIND помещается номер первого элемента командной строки, не являющегося опцией.

    Если переменой OPTIND присваивается единица, осуществляется (новый) разбор текущей командной строки (сформированной, быть может, с помощью специальной встроенной команды set ) или текущих значений заданных при вызове getopts аргументов.

    В качестве примера использования служебной программы getopts рассмотрим фрагмент shell-процедуры cmd, где производится разбор заданных при ее вызове опций (см. листинг 2.31).

    while getopts :abo: c
    do
     case $c in
     a | b) FLAG=$c;;
     o)     OARG=$OPTARG;;
     ?)     printf "Использование: %s: [-a | -b] [-o выходной_файл] [аргумент ...]\n" $0
            exit 1;;
     esac
    done
    shift $(($OPTIND - 1))
    printf "Заданный флаг: %s\n" $FLAG
    printf "Аргумент опции o: %s\n" $OARG
    printf "Остаток командной строки: %s\n" "$*"

    Если вызвать shell-процедуру cmd любым из способов, показанных в листинге 2.32, будет выдан результат, приведенный в листинге 2.33.

    cmd -a -o f1.o,f2.o file1 -
    cmd -ba -o f1.o,f2.o -- file1 -
    cmd -o f1.o,f2.o -b -a file1 -
    Заданный флаг: a
    Аргумент опции o: f1.o,f2.o
    Остаток командной строки: file1 -

    Для разбора опций и их аргументов средствами языка C служит функция getopt() и ассоциированные с ней внешние переменные (см. листинг 2.34).

    #include <unistd.h>
    int getopt (int argc, char *const argv[], 
                    const char *optstring);
    extern char *optarg;
    extern int optind, opterr, optopt;

    В общем и целом логика ее работы та же, что и у служебной программы getopts (с соответствующим переименованием используемых переменных). Аргументы argc и argv задают командную строку в том виде, как она передается функции main(), optstring представляет собой цепочку имен опций. Переменная optind (с начальным значением 1 ) служит индексом в массиве argv []. Стандарт POSIX-2001 не специфицирует, как именно getopt() разбирает несколько расположенных поочередно (после одного знака минус) имен опций и определяет, какие опции уже обработаны.

    В итоге функция getopt() возвращает имя очередной опции из числа перечисленных в цепочке optstring (если таковое удалось выделить). При наличии у опции аргумента указатель на него помещается в переменную optarg с соответствующим увеличением значения optind.

    Если у опции аргумент отсутствует, а на первом месте в optstring задано двоеточие, оно и служит результатом.

    Если встретилось имя опции, не перечисленное в optstring, или у опции нет аргумента, а на первом месте в optstring задано не двоеточие, то результатом станет знак вопроса.

    В любой из перечисленных выше ошибочных ситуаций в переменную optopt помещается имя "проблемной" опции. Кроме того, в стандартный протокол выдается диагностическое сообщение по образу и подобию утилиты getopts. Для подавления выдачи следует присвоить переменной opterr нулевое значение.

    Наконец, если при вызове getopt() указатель argv [optind] не отмечает начало опции (например, он пуст или первый символ указуемой цепочки отличен от знака минус), результат равен -1 как признак того, что разбор опций закончен.

    Следующий пример программы (см. листинг 2.35) возвращает нас к рассмотренной выше shell-процедуре cmd. Он показывает, как можно обработать командную строку вызова утилиты, допускающей взаимоисключающие опции a и b, а также опцию o, которая должна иметь аргумент.

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа разбирает опции вызвавшей ее командной строки */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    #include <unistd.h>
    #include <stdio.h>
    int main (int argc, char *argv []) {
     int c;              /* Имя анализируемой опции                     */
     int aflg = 0;       /* Признак того, что задана опция a            */
     int bflg = 0;       /* Признак того, что задана опция b            */
     int errflg = 0;     /* Флаг наличия ошибки в командной строке      */
     int flg = '?';      /* Флаг (a или b), заданный в командной строке */
     char *ofile = NULL; /* Указатель на аргумент опции o               */
     /* Подавим стандартную диагностику */
     /* независимо от первого символа   */
     /* цепочки имен опций              */
     opterr = 0;
     while ((c = getopt (argc, argv, ":abo:")) != -1) {
       switch (c) {
         case 'a':
        aflg++;
        flg = c;
        if (bflg) {
             fprintf (stderr, "Опции a и b несовместимы\n");
          errflg++;
        }
        break;
         case 'b':
        bflg++;
        flg = c;
        if (aflg) {
             fprintf (stderr, "Опции a и b несовместимы\n");
          errflg++;
        }
        break;
         case 'o':
        ofile = optarg;
        break;
         case ':':
           fprintf (stderr, "Отсутствует аргумент опции -%c\n", optopt);
           errflg++;
           break;
         case '?':
           fprintf (stderr, "Недопустимая опция -%c\n", optopt);
           errflg++;
           break;
       }
     }
      if (errflg) {
       (void) fprintf (stderr, "Использование: %s: [-a | -b] [-o выходной_файл] "
                    "[аргумент ...]\n", argv [0]);
       return (1);
     }
     printf ("Заданный флаг: %c\n", flg);
     printf ("Аргумент опции o: %s\n", ofile);
     printf ("Остаток командной строки:");
     for (; optind < argc; optind++) {
       printf (" %s", argv [optind]);
     }
     printf ("\n");
     return 0;
    }

    Приведенная программа отличается от shell-процедуры cmd более тщательным анализом ошибочных ситуаций и детальной диагностикой.

    Вернуться к учебному плану