Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 4 (всего у книги 55 страниц)
• «Файлы и процессы» суммируют мировоззрение Linux/Unix. Трактовка файлов как потоков байтов, а устройств как файлов, и использование стандартных ввода, вывода и ошибки упрощают построение программ и унифицируют модель доступа к данным. Модель прав доступа проста, но гибка, и приложима как к файлам, так и каталогам.
• Процессы являются работающими программами, у которых есть связанные с ними идентификаторы пользователя и группы для проверки прав доступа, а также другие атрибуты, такие, как открытые файлы и текущий рабочий каталог.
• Наиболее заметным различием между стандартным С и оригинальным С является использование прототипов функций для более строгой проверки типов. Хороший программист на С должен быть способен прочесть код, написанный в оригинальном стиле, поскольку его используют многие существующие программы. Новый код должен быть написан с использованием прототипов.
• GNU Coding Standards описывает написание программ GNU. Она предусматривает многочисленные ценные методики и руководящие принципы для создания надежного, практичного программного обеспечения. Принцип «никаких произвольных ограничений» является, возможно, единственным наиболее важным из них. Этот документ является обязательным для прочтения серьезными программистами.
• Переносимость программ является сложной проблемой. Руководящие указания и инструментарий помогают, но в конечном счете нужен также и опыт.
Упражнения1. Прочтите и прокомментируйте статью Ричарда М. Столмена «Проект GNU» (Richard M. Stallman, «The GNU Project»)[26]26
http://www.gnu.org/gnu/thegnuproject.html
– Примеч. автора.
[Закрыть], первоначально написанную в августе 1998 г.
Глава 2
Аргументы, опции и переменные окружения
Первой задачей любой программы обычно является интерпретация опций и аргументов командной строки. Данная глава рассматривает, как программы С (и С++) получают аргументы своей командной строки, описывает стандартные процедуры для разбора опций и бросает взгляд на переменные окружения.
2.1. Соглашения по опциям и аргументамУ слова аргументы есть два значения. Более техническим определением является «все 'слова' в командной строке». Например:
$ ls main.с opts.с process.с
Здесь пользователь напечатал четыре «слова». Все четыре слова сделаны доступными программе в качестве ее аргументов[27]27
Имя команды – ls
в данном примере, так же доступно программе в качестве аргумента – Примеч. науч. ред.
[Закрыть].
Второе определение более неформальное: аргументами являются все слова командной строки, за исключением имени команды. По умолчанию, оболочки Unix отделяют аргументы друг от друга разделителями (пробелами или символами TAB). Кавычки позволяют включать в аргументы разделитель:
$ echo here are lots of spaces
here are lots of spaces /* Оболочка «съедает» пробелы */
$ echo "here are lots of spaces"
here are lots of spaces /* Пробелы остались */
Кавычки прозрачны для запущенной программы; echo
никогда не видит символов двойной кавычки. (В оболочке двойные и одинарные кавычки различаются; обсуждение этих правил выходит за рамки данной книги, которая фокусируется на программировании на С.)
Аргументы можно подразделить далее на опции и операнды. В предыдущих двух примерах все аргументы были операндами: файлы для ls
и простой текст для echo
.
Опции являются специальными аргументами, которые каждая программа интерпретирует. Опции изменяют поведение программы или предоставляют программе информацию. По старому соглашению, которого (почти) всегда придерживаются, опции начинаются с черточки (т.е. дефиса, значка минус), и состоят из единственной буквы. Аргументы опции являются информацией, необходимой для опции, в отличие от обычных аргументов-операндов. Например, опция -f
программы fgrep
означает «использовать содержимое следующего файла в качестве списка строк для поиска». См. рис 2.1.
Рис. 2.1. Компоненты командной строки
Таким образом, patfile
является не файлом данных для поиска, а предназначен для использования fgrep
в определении списка строк, которые нужно искать.
Стандарт POSIX описывает ряд соглашений, которых придерживаются удовлетворяющие стандарту программы. Никто от вас не требует, чтобы ваши программы удовлетворяли этим стандартам, но это хорошая мысль сделать так: пользователи Linux и Unix по всему миру понимают и используют эти соглашения, и если вы не будете им следовать, ваши пользователи будут несчастны. (Или у вас вообще не будет пользователей!) Более того, функции, которые мы обсуждаем далее в этой главе, освобождают вас от бремени ручной реализации этих соглашений для каждой программы, которую вы пишете. Вот эти правила, перефразированные из стандарта:
1. В имени программы должно быть не менее двух и не более девяти символов.
2. Имена программ должны содержать лишь строчные символы и цифры.
3. Имя опции должно быть простым буквенно-цифровым символом. Опции с множеством цифр не должны допускаться. Для производителей, реализующих утилиты POSIX, опция -W
зарезервирована для специфичных для производителя опций.
4. Все опции должны начинаться с символа '-
'.
5. Для опций, не требующих аргументов, должно быть возможно объединение нескольких опций после единственного символа '-
'. (Например, 'foo -a -b -c
' и 'foo -abc
' должны интерпретироваться одинаково.)
6. Когда опции все же требуется аргумент, он должен быть отделен от опции пробелом (например, 'fgrep -f patfile
').
Однако, стандарт допускает историческую практику, при которой иногда опция и ее операнд могут находиться в одной строке: 'fgrep -fpatfile
'. На практике функции getopt()
и getopt_long()
интерпретируют '-fpatfile
' как '-f patfile
', а не как '-f -p -a -t ...
'.
7. Аргументы опций не должны быть необязательными.
Это означает, что если в документации программы указано, что опции требуется аргумент, этот аргумент должен присутствовать всегда, иначе программа потерпит неудачу GNU getopt()
все же предусматривает необязательные аргументы опций, поскольку иногда они полезны
8. Если опция принимает аргумент, который может иметь несколько значений, программа должна получать этот аргумент в виде одной строки со значениями, разделенными запятыми или разделителем.
Например, предположим, что гипотетической программе myprog
требуется список пользователей для опции -u
. Далее она может быть вызвана одним из двух способов:
myprog -u "arnold,joe,jane" /* Разделение запятыми */
myprog -u "arnold joe jane" /* Разделение пробелами */
В таком случае вы должны самостоятельно отделить и обработать каждое значение (т.е. здесь нет стандартной процедуры), но ручная реализация обычно проста.
9. Опции должны находиться в командной строке первыми, перед операндами. Версии getopt()
Unix проводят в жизнь это соглашение. GNU getopt()
по умолчанию этого не делает, хотя вы можете настроить его на это.
10. Специальный аргумент '–
' указывает на окончание всех опций. Все последующие аргументы командной строки рассматриваются как операнды, даже если они начинаются с черточки.
11. Порядок, в котором приведены опции, не должен играть роли. Однако, для взаимно исключающих опций, когда одна опция перекрывает установки другой, тогда (так сказать) последняя побеждает. Если опция, имеющая аргумент, повторяется, программа должна обработать аргументы по порядку. Например, 'myprog -u arnold -u jane
' то же самое, что и 'myprog -u "arnold, jane"
'. (Вам придется осуществить это самостоятельно; getopt()
вам не поможет.)
12. Нормально, когда порядок аргументов имеет для программы значение. Каждая программа должна документировать такие вещи.
13. Программы, читающие или записывающие именованные файлы, должны трактовать единственный аргумент '-
' как означающий стандартный ввод или стандартный вывод, в зависимости от того, что подходит программе.
Отметим, что многие стандартные программы не следуют всем указанным соглашениям. Главной причиной является историческая совместимость; многие такие программы предшествовали систематизации этих соглашений.
Как мы видели в разделе 1.4.2 «Поведение программ», программам GNU рекомендуется использовать длинные опции в форме –help
, –verbose
и т.д. Такие опции, поскольку они начинаются с '–
', не конфликтуют с соглашениями POSIX. Их также легче запомнить, и они предоставляют возможность последовательности среди всех утилит GNU. (Например, –help
является везде одним и тем же, в отличие от -h
для «help», -i
для «information» и т.д.) Длинные опции GNU имеют свои собственные соглашения, реализованные в функции getopt_long()
:
1. У программ, реализующих инструменты POSIX, каждая короткая опция (один символ) должна иметь также свой вариант в виде длинной опции.
2. Дополнительные специфические для GNU опции не нуждаются в соответствующей короткой опции, но мы рекомендуем это сделать.
3. Длинную опцию можно сократить до кратчайшей строки, которая остается уникальной. Например, если есть две опции –verbose
и –verbatim
, самыми короткими сокращениями будут –verbo
и –verba
.
4. Аргументы опции отделяются от длинных опций либо разделителем, либо символом =
. Например, –sourcefile=/some/file
или –sourcefile /some/file
.
5. Опции и аргументы могут быть заинтересованы в операндах командной строки, getopt_long()
переставляет аргументы таким образом, что сначала обрабатываются все опции, а затем все операнды доступны последовательно. (Такое поведение можно запретить.)
6. Аргументы опций могут быть необязательными. Для таких опций считается, что аргумент присутствует, если он находится в одной строке с опцией. Это работает лишь для коротких опций. Например, если -х такая опция и дана строка 'foo -хYANKEES -y
', аргументом -х
является 'YANKEES
'. Для 'foo -х -y
' у -х
нет аргументов.
7. Программы могут разрешить длинным опциям начинаться с одной черточки (Это типично для многих программ X Window.)
Многое из этого станет яснее, когда позже в этой главе мы рассмотрим getopt_long()
.
GNU Coding Standards уделяет значительное место перечислению всех длинных и коротких опций, используемых программами GNU. Если вы пишете программу, использующую длинные опции, посмотрите, нет ли уже использующихся имен опций, которые имело бы смысл использовать и вам.
2.2. Базовая обработка командной строкиПрограмма на С получает доступ к своим аргументам командной строки через параметры argc
и argv
. Параметр argc
является целым, указывающим число имеющихся аргументов, включая имя команды. Есть два обычных способа определения main()
, отличающихся способом объявления argc
:
int main(int argc, char *argv[]) int main(int argc, char **argv)
{ {
... ...
} }
Практически между двумя этими объявлениями нет разницы, хотя первое концептуально более понятно: argc
является массивом указателей на символы. А второе определение технически более корректно, это то, что мы используем. На рис. 2.2 изображена эта ситуация.
Рис. 2.2. Память для argc
По соглашению, argv[0]
является именем программы. (Детали см. в разделе 9.1.4.3. «Имена программ и argv[0]
».) Последующие элементы являются аргументами командной строки. Последним элементом массива argv
является указатель NULL
.
argc
указывает, сколько имеется аргументов; поскольку в С индексы отсчитываются с нуля, выражение 'argv[argc] == NULL
' всегда верно. Из-за этого, особенно в коде для Unix, вы увидите различные способы проверки окончания списка аргументов, такие, как цикл с проверкой, что счетчик превысил argc
, или 'argv[i] == 0
', или '*argv != NULL
' и т.д. Они все эквивалентны.
echo
V7Возможно, простейшим примером обработки командной строки является программа V7 echo,
печатающая свои аргументы в стандартный вывод, разделяя их пробелами и завершая символом конца строки. Если первым аргументом является -n
, завершающий символ новой строки опускается. (Это используется для приглашений из сценариев оболочки.) Вот код[28]28
См. /usr/src/cmd/echo.c
в дистрибутиве V7 – Примеч. автора.
[Закрыть]:
1 #include
2
3 main(argc, argv) /*int main(int argc, char **argv)*/
4 int argc;
5 char *argv[];
6 {
7 register int i, nflg;
8
9 nflg = 0;
10 if (argc > 1 && argv[1][0] == && argv[1][1] == 'n') {
11 nflg++;
12 argc–;
13 argv++;
14 }
15 for (i=1; i
16 fputs(argv[i], stdout);
17 if (i < argc-1)
18 putchar(' ');
19 }
20 if (nflg == 0)
21 putchar('n');
22 exit(0);
23 }
Всего 23 строки! Здесь есть два интересных момента. Во-первых, уменьшение argc
и одновременное увеличение argv
(строки 12 и 13) являются обычным способом пропуска начальных аргументов. Во-вторых, проверка наличия -n
(строка 10) является упрощением. -no-newline-at-the-end
также работает. (Откомпилируйте и проверьте это!)
Ручной разбор опций обычен для кода V7, поскольку функция getopt()
не была еще придумана.
Наконец, здесь и в других местах по всей книге, мы видим использование ключевого слова register. Одно время это ключевое слово давало компилятору подсказку, что данная переменная должна по возможности размещаться в регистре процессора. Теперь это ключевое слово устарело; современные компиляторы все основывают размещение переменных в регистрах на анализе исходного кода, игнорируя ключевое слово register
. Мы решили оставить использующий это слово код, как есть, но вы должны знать, что оно больше не имеет реального применения.[29]29
Когда мы спросили Джима Мейеринга (Jim Meyering), сопроводителя Coreulils, о наличии register
в GNU Coreutils, он дал нам интересный ответ. Он удаляет эти слова при изменении кода, но в остальных случаях оставляет их на месте, чтобы облегчить интеграцию сделанных изменений с существующими версиями – Примеч. автора.
[Закрыть]
getopt()
и getopt_long()
Примерно в 1980-х группа поддержки Unix для System III в AT&T заметила, что каждая программа Unix использовала для разбора аргументов свои собственные методики. Чтобы облегчить работу пользователей и программистов, они разработали большинство из перечисленных ранее соглашений. (Хотя изложение в System III справки для intro(1) значительно менее формально, чем в стандарте POSIX.)
Группа поддержки Unix разработала также функцию getopt()
, вместе с несколькими внешними переменными, чтобы упростить написание кода, придерживающегося стандартных соглашений. Функция GNU getopt_long()
предоставляет совместимую с getopt()
версию, а также упрощает разбор длинных опций в описанной ранее форме.
Функция getopt()
объявлена следующим образом:
#include
int getopt(int argc, char *const argv[], const char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;
Аргументы argc
и argv
обычно передаются непосредственно от main()
. optstring
является строкой символов опций. Если за какой-либо буквой в строке следует двоеточие, эта опция ожидает наличия аргумента.
Для использования getopt()
вызывайте ее повторно из цикла while
до тех пор, пока она не вернет -1
. Каждый раз, обнаружив действительный символ опции, функция возвращает этот символ. Если опция принимает аргумент, указатель на него помещается в переменную optarg
. Рассмотрим программу, принимающую опцию -а
без аргумента и опцию -b
с аргументом:
int ос; /* символ опции */
char *b_opt_arg;
while ((ос = getopt(argc, argv, "ab:")) != -1) {
switch (oc) {
case 'a':
/* обработка -а, установить соответствующий флаг */
break;
case 'b':
/* обработка -b, получить значение аргумента из optarg */
b_opt_arg = optarg;
break;
case ':':
... /* обработка ошибок, см. текст */
case '?':
default:
... /* обработка ошибок, см. текст */
}
}
В ходе работы getopt()
устанавливает несколько переменных, контролирующих обработку ошибок:
char *optarg
Аргумент для опции, если она принимает аргумент.
int optind
Текущий индекс в argv
. Когда цикл loop
завершается, оставшиеся операнды находятся с argv[optind]
по argv[argc-1]
. (Помните, что 'argv [argc] ==NULL
'.)
int opterr
Когда эта переменная не равна нулю (значение по умолчанию), getopt()
печатает свои собственные сообщения для недействительных опций или отсутствующих аргументов опций.
int optopt
Когда находится недействительный символ опции, getopt()
возвращает либо '?
', либо ':
' (см ниже), a optopt
содержит обнаруженный недействительный символ.
Люди есть люди, программы неизбежно будут иногда вызываться неправильно либо с недействительной опцией, либо с отсутствующим аргументом опции. Обычно в таких случаях getopt()
выводит свои собственные сообщения и возвращает символ '?
'. Однако, вы можете изменить такое поведение двумя способами.
Во-первых, записав 0 в opterr
перед вызовом getopt()
, можно заставить getopt()
не предпринимать при обнаружении проблем никаких действий.
Во-вторых, если первый символ в optstring
является двоеточием, getopt()
не предпринимает никаких действий и возвращает другой символ в зависимости от ошибки следующим образом:
Неверная опция
getopt()
возвращает '?
', a optopt
содержит неверный символ опции (Это обычное поведение).
Отсутствует аргумент опции
getopt()
возвращает ':
'. Если первый символ optstring
не является двоеточием, getopt()
возвращает '?
', делая этот случай неотличимым от случая неверной опции.
Таким образом, помещение в качестве первого символа optstring
двоеточия является хорошей мыслью, поскольку это позволяет различать «неверную опцию» и «отсутствующий аргумент опции». Расплатой за это является то, что getopt()
в этом случае также не предпринимает никаких действий, заставляя вас выводить собственные сообщения об ошибках. Вот предыдущий пример, на этот раз с обработкой ошибок:
int ос; /* символ опции */
char *b_opt_arg;
while ((ос = getopt(argc, argv, ":ab:")) != -1) {
switch (oc) {
case 'a':
/* обработка -a, установка соответствующего флага */
break;
case 'b':
/* обработка -b, получение значения аргумента из optarg */
b_opt_arg = optarg;
break;
case ':':
/* отсутствует аргумент опции */
fprintf(stderr, "%s: option '-%c' requires an argumentn",
argv[0], optopt);
break;
case '?':
default:
/* недействительная опция */
fprintf(stderr, "%s: option '-%c' is invalid: ignoredn",
argv[0], optopt);
break;
}
}
Замечание о соглашениях по именованию флагов или опций: в большом количестве кода для Unix используются имена в виде xflg
для любого данного символа опции x (например, nflg
в echo
V7; обычным является также xflag
). Это может быть замечательным для авторе программы, который без проверки документации знает, что означает опция x. Но это не подходит для кого-то еще, кто пытается прочесть код и не знает наизусть значений всех символов опций. Гораздо лучше использовать имена, передающие смысл опции, как no_newline
для опции -n
echo.
getopt()
и порядок опцийСтандартная функция getopt()
прекращает поиск опций, как только встречает аргумент командной строки, который не начинается с GNU getopt()
отличается: она просматривает в поисках опций всю командную строку. По мере продвижения она переставляет элементы argv
, так что после ее завершения все опции оказываются переставленными в начало, и код, продолжающий разбирать аргументы с argv[optind]
до argv[argc-1]
, работает правильно. Во всех случаях специальный аргумент '–
' завершает сканирование опций.
Вы можете изменить поведение по умолчанию, использовав в optstring
специальный первый символ следующим образом:
optstring[0] == '+'
GNU getopt()
ведет себя, как стандартная getopt()
; она возвращает опции по мере их обнаружения, останавливаясь на первом аргументе, не являющемся опцией. Это работает также в том случае, если в окружении присутствует строка POSIXLY_CORRECT
.
optstring[0] == '-'
GNU getopt()
возвращает каждый аргумент командной строки независимо от того, представляет он аргумент или нет. В этом случае для каждого такого аргумента функция возвращает целое 1, а указатель на соответствующую строку помещает в optarg
.
Как и для стандартной getopt()
, если первым символом optstring
является ':
', GNU getopt()
различает «неверную опцию» и «отсутствующий аргумент опции», возвращая соответственно '?
' или ':
'. Символ ':
' в optstring
может быть вторым символом, если первым символом является '+
' или '-
'.
Наконец, если за символом опции в optstring
следуют два двоеточия, эта опция может иметь необязательный аргумент. (Быстро повторите это три раза!) Такой аргумент считается присутствующим, если он находится в том же элементе argv
, что и сама опция, и отсутствующим в противном случае. В случае отсутствия аргумента GNU getopt()
возвращает символ опции, а в optarg
записывает NULL. Например, пусть имеем:
while ((с = getopt(argc, argv, "ab::")) != -1)
...
для -bYANKEES
, возвращаемое значение будет 'b
', a optarg
указывает на «YANKEES
», тогда как для -b
или '-b YANKEES
' возвращаемое значение будет все то же 'b
', но в optarg
будет помещен NULL. В последнем случае «YANKEES
» представляет отдельный аргумент командной строки.