Open Source →
Разбираем исходный код GNU Coreutils: утилита yes
(Статья доступна для оффлайн чтения: Markdown | PDF | PDF (print) | HTML)
Все вокруг постоянно говорят: «Хочешь научиться писать профессиональные программы? Посмотри, как это делают другие!». Вот и я решил последовать этому совету, тем более что моё обучение в университете как раз подходит к концу. Особенно интересно сравнить то как учили делать и то как делается в реальном мире. В качестве примера для подражания был выбран пакет GNU Coreutils. В нём есть всё:
GNU Core Utilites — это набор утилит для выполнения базовых пользовательских операций: создание директории, вывод файла на экран и так далее. По замыслу разработчиков, эти утилиты должны быть доступны в любой операционной системе, что мы и наблюдаем в настоящее время: для Windows есть Cygwin, ну а про *nix и говорить нечего. Сохранить единообразность работы в разных системах помогает стандарт POSIX, который в Coreutils пытаются соблюдать. Coreutils содержит такие часто используемые утилиты, как cat, tail, echo, wc и многие другие.
Для начала выберем самую тривиальную программу под названием yes. Её простота позволит разобраться с используемыми в Coreutils инструментами и библиотеками.
Как говорится в мане, всё что умеет утилита yes — это бесконечно выводить «yn» в stdout. Если мы передадим yes какие-то аргументы, то вместо «y» yes будет выводить аргументы через пробел. Наверняка похожую программу писал каждым, кто начинал изучать C. А значит у многих есть возможность сравнить свой подход с тем, как это делают суровые бородатые дядьки из GNU. О практическом применении yes немного написано в Википедии.
Переходим к исходному коду. Достать его можно либо с помощью
Исходный код yes умещается в одном файле
Первое, на что обращаешь внимание — непривычное форматирование кода. Почитать о нём можно в соответствующей главе GNU Coding Standarts. Например, при определении функции тип возвращаемого значения должен располагаться на отдельной строке, как и открывающая скобка:
Для отступов и выравнивания используются только пробелы. Между различными уровнями вложенности разница в отступе составляет 2 пробела. Особо извращённую форму имеют фигурные скобки при операторах:
Первое, что делает программа, это вызов
В утилитах Coreutils различают два названия программы:
Официальное название используется при выводе информации о версии приложения:
Причём это название никак не зависит от имени исполняемого файла:
Такое поведение обеспечивается специально определённым в начале файла макросом
Реальное название без всяких хитростей берётся из
Значение
Функция
Coreutils используют по всему миру, поэтому во всех утилитах предусмотрена возможность локализации. Причём эта возможность обеспечивается минимальными усилиями благодаря использованию пакета GNU gettext. Немногих удивит использование именно gettext, ведь этот пакет распространился далеко за пределы проекта GNU. Например, интернационализация в моём любимом web-фреймворке Django построена именно на gettext. Про использование gettext совместно с различными языками и фреймворками уже писали на хабре.
Замечательным свойством gettext является то, что он во всех языках используется примерно одинаково, и C не исключение. Здесь есть стандартная магическая функция
Определение функции
Инициализация механизма интернационализации в Coreutils производится вызовом трёх функций в
Двигаясь дальше по коду
Интуитивно можно подумать, что в функции
Это последний вопрос, который не касается работы самой программы. Здесь, как и в случае с интернационализацией, используется проверенное временем и пролезшее во многие проекты (например, в Python) решение — модуль getopt. Этот модуль очень прост: фактически, от разработчика требуется вызывать в цикле одну из функций
В Gnulib есть специальная функция
Исходный код yes является классным примером работы с getopt. Тут одновременно отсутствует излишняя для обучения сложность с разбором десятков аргументов и присутствует использование всех средств getopt. Сначала, естественно, выполняется вызов
Следующий код можно перевести на русский так: «Если в списке аргументов командой строки ничего кроме ключей --version и --help не было, то мы будем выводить „y“ в stdout»:
Запись в
Ну вот мы и добрались до самого функционала программы. Вот он весь, как есть:
Здесь можно отметить, что все действия выполняются внутри условия
Зачем?
Все вокруг постоянно говорят: «Хочешь научиться писать профессиональные программы? Посмотри, как это делают другие!». Вот и я решил последовать этому совету, тем более что моё обучение в университете как раз подходит к концу. Особенно интересно сравнить то как учили делать и то как делается в реальном мире. В качестве примера для подражания был выбран пакет 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 Standarts. Например, при определении функции тип возвращаемого значения должен располагаться на отдельной строке, как и открывающая скобка:
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++)
;
03.12.2011 20:17+0400