MyTetra Share
Делитесь знаниями!
Разбираем исходный код GNU Coreutils: утилита yes
Время создания: 11.12.2011 17:10
Текстовые метки: gnu, c language, linux, coreutils
Раздел: Development - Linux
Запись: YellowRaven/myTetra_repo/master/base/1323609037wvnrib9nbn/text.html на raw.github.com

Разбираем исходный код GNU Coreutils: утилита yes

Зачем?


Все вокруг постоянно говорят: «Хочешь научиться писать профессиональные программы? Посмотри, как это делают другие!». Вот и я решил последовать этому совету, тем более что моё обучение в университете как раз подходит к концу. Особенно интересно сравнить то как учили делать и то как делается в реальном мире. В качестве примера для подражания был выбран пакет 
GNU Coreutils . В нём есть всё:

  1. Жёсткие требования к переносимости.
  2. Большой жизненный цикл.
  3. Огромная команда разработчиков.
  4. Код различной сложности: от тривиального 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 и получить версию, которая используется в вашей системе по-умолчанию, либо вытянуть новейшую версию из репозиториев. Мы выберем второй вариант: он более удобен и привычен.

  1. Coreutils: git clone git://git.sv.gnu.org/coreutils
  2. 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 различают два названия программы:

  1. Официальное название, которое пользователь не может изменить.
  2. Реальное название исполняемого файла.


Официальное название используется при выводе информации о версии приложения:

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++)

;

 
MyTetra Share v.0.59
Яндекс индекс цитирования