Разбираем исходный код GNU Coreutils: утилита yes
Зачем?
Все вокруг постоянно говорят: «Хочешь научиться писать профессиональные программы? Посмотри, как это делают другие!». Вот и я решил последовать этому совету, тем более что моё обучение в университете как раз подходит к концу. Особенно интересно сравнить то как учили делать и то как делается в реальном мире. В качестве примера для подражания был выбран пакет GNU Coreutils . В нём есть всё:
- Жёсткие требования к переносимости.
- Большой жизненный цикл.
- Огромная команда разработчиков.
- Код различной сложности: от тривиального echo до супер-изощерённого sed, от чисто прикладного wc до более близкого к ОС mkdir.
GNU Coreutils
GNU Core Utilites — это набор утилит для выполнения базовых пользовательских операций: создание директории, вывод файла на экран и так далее. По замыслу разработчиков, эти утилиты должны быть доступны в любой операционной системе, что мы и наблюдаем в настоящее время: для Windows естьCygwin , ну а про *nix и говорить нечего. Сохранить единообразность работы в разных системах помогает стандарт POSIX , который в Coreutils пытаются соблюдать . Coreutils содержит такие часто используемые утилиты, как cat, tail, echo, wc и многие другие.
Для начала выберем самую тривиальную программу под названием yes. Её простота позволит разобраться с используемыми в Coreutils инструментами и библиотеками.
Утилита yes
Как говорится в мане , всё что умеет утилита yes — это бесконечно выводить «yn» в stdout. Если мы передадим yes какие-то аргументы, то вместо «y» yes будет выводить аргументы через пробел. Наверняка похожую программу писал каждый, кто начинал изучать C. А значит у многих есть возможность сравнить свой подход с тем, как это делают суровые бородатые дядьки из GNU. О практическом применении yes немного написано в Википедии .
Исходный код
Переходим к исходному коду. Достать его можно либо с помощью apt-get source и получить версию, которая используется в вашей системе по-умолчанию, либо вытянуть новейшую версию из репозиториев. Мы выберем второй вариант: он более удобен и привычен.
- Coreutils: git clone git://git.sv.gnu.org/coreutils
- Gnulib (заглянем туда пару раз): git clone git://git.savannah.gnu.org/gnulib.git
Исходный код yes умещается в одном файле coreutils/src/yes.c, его и откроем.
Coding style
Первое, на что обращаешь внимание — непривычное форматирование кода. Почитать о нём можно всоответствующей главе GNU Coding Standards. Например, при определении функции тип возвращаемого значения должен располагаться на отдельной строке, как и открывающая скобка:
int
main (int argc, char **argv)
{
foo();
...
}
Для отступов и выравнивания используются только пробелы. Между различными уровнями вложенности разница в отступе составляет 2 пробела. Особо извращённую форму имеют фигурные скобки при операторах:
if (x < foo (y, z))
haha = bar[4] + 5;
else
{
while (z)
{
haha += foo (z, z);
z--;
}
return ++x + bar ();
}
12 строк
yes.c начинается с обязательного для всех GPL-програм комментария. Он уже успел намозолить мне глаза в других программах и необходимость его наличия была для меня загадкой. Оказывается, что текст этого комментария зафиксирован в инструкции по применению GPL. Именно в ней прописано, что все, кто желает выпускать своё ПО под GPL, должны добавлять эти 12 строк заявления о праве копирования в начало каждого файла исходного кода.
initialize_main
Первое, что делает программа, это вызов initialize_main. Эта функция предназначена для того, чтобы программа выполнила свои специфичные действия над аргументами. На практике, в Coreutils нет ни одной утилиты, которая бы использовала эту функцию для чего-то полезного. Везде используется заглушка, представленная в файле coreutils/src/system.h:
#ifndef initialize_main
# define initialize_main(ac, av)
#endif
Название программы
В утилитах Coreutils различают два названия программы:
- Официальное название, которое пользователь не может изменить.
- Реальное название исполняемого файла.
Официальное название используется при выводе информации о версии приложения:
user@laptop:~$ yes --version
yes (GNU coreutils) 8.5
Usage: yes [STRING]...
or: yes OPTION
Причём это название никак не зависит от имени исполняемого файла:
user@laptop:~$ /usr/bin/yes --version
yes (GNU coreutils) 8.5
user@laptop:~$ cp /usr/bin/yes ./foo
user@laptop:~$ ./foo --version
yes (GNU coreutils) 8.5
Такое поведение обеспечивается специально определённым в начале файла макросом PROGRAM_NAME:
/* The official name of this program (e.g., no `g' prefix). */
#define PROGRAM_NAME "yes"
Реальное название без всяких хитростей берётся из argv[0] и используется при выводе ошибок и подсказок:
user@laptop:~$ yes --help
Usage: yes [STRING]...
or: yes OPTION
user@laptop:~$ /usr/bin/yes --help
Usage: /usr/bin/yes [STRING]...
or: /usr/bin/yes OPTION
Значение argv[0] помещается в глобальную переменную program_name с помощью вызова функции set_program_name во второй строке main:
set_program_name (argv[0]);
Функция set_program_name предоставляется библиотекой Gnulib . Соответствующий код находится в каталогеgnulib/lib/, в файлах progname.h и progname.c. Интересно заметить, что set_program_name не просто сохраняет значения argv[0] в глобальную переменную program_name, объявленную в progname.h, но и выполняет дополнительные преобразования, связанные с тонкостями использования GNU Libtool , инструмента для разработки динамических библиотек.
Интернационализация
Coreutils используют по всему миру, поэтому во всех утилитах предусмотрена возможность локализации. Причём эта возможность обеспечивается минимальными усилиями благодаря использованию пакета GNU gettext . Немногих удивит использование именно gettext, ведь этот пакет распространился далеко за пределы проекта GNU. Например, интернационализация в моём любимом web-фреймворке Django построенаименно на gettext . Про использование gettext совместно с различными языками и фреймворками уже писали на хабре .
Замечательным свойством gettext является то, что он во всех языках используется примерно одинаково, и C не исключение. Здесь есть стандартная магическая функция _, использование которой можно найти в функции usage:
void
usage (int status)
{
if (status != EXIT_SUCCESS)
fprintf (stderr, _("Try `%s --help' for more information.\n"),
program_name);
...
}
Определение функции _ находится в уже знакомом нам файле system.h:
#define _(msgid) gettext (msgid)
Инициализация механизма интернационализации в Coreutils производится вызовом трёх функций в main:
setlocale (LC_ALL, "");
bindtextdomain (PACKAGE, LOCALEDIR);
textdomain (PACKAGE);
- setlocale устанавливает стандартную локаль окружения в качестве рабочей для приложения
- bindtextdomain говорит, где искать файл с переводами для конкретного домена сообщений
- textdomain устанавливает текущий домен сообщений
Обработка ошибок
Двигаясь дальше по коду main, мы встречаем такую строку:
atexit (close_stdout);
Интуитивно можно подумать, что в функции close_stdout закрывается стандартный поток вывода, что исключает потерю данных, если мы подменили stdout каким-нибудь файловым дескриптором и используем буферизированный вывод. Но найти исходный код этой функции и понять, что же на самом деле там происходит, выполняются ли какие-нибудь дополнительные действия по подчистке ресурсов, у меня не получилось.
Аргументы командной строки
Это последний вопрос, который не касается работы самой программы. Здесь, как и в случае с интернационализацией, используется проверенное временем и пролезшее во многие проекты (например, в Python ) решение — модуль getopt . Этот модуль очень прост: фактически, от разработчика требуется вызывать в цикле одну из функций getopt или getopt_long. Подробнее о getopt можно почитать в интернете, да и на хабре о нём тоже писали.
В Gnulib есть специальная функция parse_long_options для обработки аргументов --version и --help, которые любое GNU-приложение обязано поддерживать. Находится она в файле gnulib/lib/long-options.c и использует getopt_long в своей работе.
Исходный код yes является классным примером работы с getopt. Тут одновременно отсутствует излишняя для обучения сложность с разбором десятков аргументов и присутствует использование всех средств getopt. Сначала, естественно, выполняется вызов parse_long_options. Затем проверяется, что больше никаких опций-ключей не передано и остальные аргументы, если они есть, являются просто произвольными строками:
parse_long_options (argc, argv, PROGRAM_NAME, PACKAGE_NAME, Version,
usage, AUTHORS, (char const *) NULL);
if (getopt_long (argc, argv, "+", NULL, NULL) != -1)
usage (EXIT_FAILURE);
Следующий код можно перевести на русский так: «Если в списке аргументов командой строки ничего кроме ключей --version и --help не было, то мы будем выводить „y“ в stdout»:
if (argc <= optind)
{
optind = argc;
argv[argc++] = bad_cast ("y");
}
Запись в argv[argc] не является ошибкой: стандарт ANSI C требует, чтобы элемент argv[argc] был нулевым указателем.
Главный цикл
Ну вот мы и добрались до самого функционала программы. Вот он весь, как есть:
while (true)
{
int i;
for (i = optind; i < argc; i++)
if (fputs (argv[i], stdout) == EOF
|| putchar (i == argc - 1 ? '\n' : ' ') == EOF)
error (EXIT_FAILURE, errno, _("standard output"));
}
Здесь можно отметить, что все действия выполняются внутри условия if, а не в его теле. Значит, Кёрниган и Ритчи не врали, когда писали, что опытный C-программист реализует копирование строк так:
while (*dst++ = *src++)
;