Содержание


Обработка команд при помощи getopt()

Простая обработка сложных команд

Comments

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

Этот аспект является одним из самых значительных аспектов UNIX, который делает эту ОС крайне мощной средой для обработки текстовых данных – одно из ее первых применений в корпоративной среде. Если подать конвейеру команд на вход какой-либо текст, то результатом работы этого конвейера команд станет уже обработанный текст.

Параметры и аргументы управляют UNIX-программами и определяют их поведение. В область ответственности разработчика входит определение намерений пользователя по команде, переданной им в метод main() программы. Эта статья показывает, как использовать стандартные функции getopt() и getopt_long() для упрощения обработки команд и одну методику, предназначенную для контроля команд.

Программный код, поставляемый с этой статьей (см. раздел Материалы для скачивания), был создан в среде Eclipse 3.1 на языке C (С Development Tooling, CDT); проекты getopt_demo и getopt_long_demo являются проектами MManaged Make, которые построены с использованием правил создания программ CDT. В этих проектах отсутствует файл Makefile, но если понадобится скомпилировать код вне среды Eclipse, то создать Makefile достаточно легко.

Если у вас еще нет опыта работы со средой Eclipse (см. раздел Ресурсы), то рекомендуем попробовать ее использовать – это отличная интегрированная среда разработки (integrated development environment, IDE), которая с каждой новой версией становится только лучше. Хотя я стойкий приверженец EMACS и Makefile, но Eclipse мне очень нравится.

Команды

При работе над новой программой первым препятствием, которое может возникнуть, является вопрос, что делать с параметрами команды, которые управляют поведением программы. Эти аргументы передаются посредством команды в метод main() как целое число (традиционно называемое argc) и массив указателей на строки (традиционно называемый argv). Стандартный метод main() может быть объявлен двумя способами, которые фактически одинаковы, как показано в листинге 1.

Листинг 1. Два способа объявления метода main()
int main( int argc, char *argv[] );
int main( int argc, char **argv );

Первый способ, с массивом указателей на char, сегодня более распространен и гораздо удобнее для восприятия, чем второй вариант с указателями на указатели на char. По некоторым причинам я предпочитаю все же использовать второй способ объявления более часто, возможно для самоутверждения в моей победе над указателями в языке C, одержанной в средней школе. По большому счету, нет разницы в использовании этих двух способов объявления метода main(), поэтому можно использовать любой из них.

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

Например, если запускается программа foo с параметрами -v bar www.ibm.com, argc в данном случае будет равняться 4, а argv будет указывать на строки, показанные в листинге 2.

Листинг 2. Содержимое argv
argv[0] - foo
argv[1] - -v
argv[2] - bar
argv[3] - www.ibm.com

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

Ниже разобрана программа, которая обрабатывает команду к несуществующей программе doc2html. Программа doc2html конвертирует какой-либо документ в HTML и управляется параметрами командной строки, заданными пользователем. Она поддерживает следующие параметры:

  • -I – не создавать индекс ключевых слов.
  • -l lang – перевести в заданный язык, используя код языка lang.
  • -o outfile.html – записать переведенный документ в outfile.html вместо записи его в стандартный поток вывода.
  • -v – подробный режим вывода информации при переводе; может задаваться несколько раз для увеличения уровня диагностирования.
  • Файлы, чьи имена были дополнительно указаны в команде, будут использоваться в качестве входных файлов.

Также поддерживаются опции -h и -? для вывода вспомогательного сообщения, которое предоставляет пользователю справку по этим параметрам.

Обработка команды: getopt()

Функция getopt(), которая находится в системном заголовочном файле unistd.h, показана в листинге 3.

Листинг 3. Прототип getopt()
int getopt( int argc, char *const argv[], const char *optstring );

Получая на вход число параметров команды (argc), массив указателей на эти параметры (argv) и строку с опциями (option string) (optstring), getopt() возвращает первый параметр и задает некоторые глобальные переменные. При повторном вызове с теми же аргументами функция вернет следующий параметр и задаст глобальную переменную. Если больше не будет найдено опций, которые надо распознать, то функция вернет -1, что означает завершение обработки команды.

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

  • optarg – указатель на текущий аргумент, если таковой имеется.
  • optind – индекс на следующий указатель argv, который будет обработан при следующем вызове getopt().
  • optopt – последний из известных параметров.

Строка с опциями (optstring) состоит из одного символа для каждого аргумента командной строки. После опций, которые имеют параметры, например, -l и -o в примере выше, ставится двоеточие :. optstring для этого примера является Il:o:vh? (напомним, что также нужна поддержка двух последних опций для вывода сообщений о работе программы).

Можно вызывать getopt() до тех пор, пока она не вернет -1; любые остальные аргументы команды обычно являются именами файлов или чем-либо другим, необходимым для программы.

getopt() в действии

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

В листинге 4 можно увидеть заголовочные файлы, которые использует демо-программа: stdio.h для прототипов стандартных функций ввода/вывода, stdlib.h для EXIT_SUCCESS и EXIT_FAILURE и unistd.h для getopt().

Листинг 4. Файлы заголовков
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

Листинг 5 показывает структуру globalArgs, созданную для хранения аргументов команды в удобной форме. Поскольку эта структура является глобальной переменной, то из любого места программы можно получить доступ к этим переменным, чтобы посмотреть, надо ли индексировать ключевые слова, на какой язык переводить и прочее. Для кода вне функции main() было бы разумным использовать эту структуру как константу, так как любая часть программы может зависеть от ее содержания.

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

Листинг 5. Глобальная структура с аргументами и строка с опциями
struct globalArgs_t {
    int noIndex;                /* параметр -I */
    char *langCode;             /* параметр -l */
    const char *outFileName;    /* параметр -o */
    FILE *outFile;
    int verbosity;              /* параметр -v */
    char **inputFiles;          /* входные файлы */
    int numInputFiles;          /* число входных файлов */
} globalArgs;

static const char *optString = "Il:o:vh?";

Строка с опциями, optString, указывает getopt(), какие опции должны быть обработаны и каким опциям нужны аргументы. Если при обработке команды неожиданно возникает другая неизвестная опция, то getopt() выводит сообщение об ошибке, и программа завершает свое выполнение после соответствующего служебного сообщения.

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

Листинг 6. Шаблоны
void display_usage( void )
{
    puts( "doc2html - convert documents to HTML" );
    /* ... */
    exit( EXIT_FAILURE );
}

void convert_document( void )
{
    /* ... */
}

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

Листинг 7. Инициализация
int main( int argc, char *argv[] )
{
    int opt = 0;
    
    /* инициализация globalArgs до начала работы с ней. */
	globalArgs.noIndex = 0;     /* false */
    globalArgs.langCode = NULL;
    globalArgs.outFileName = NULL;
    globalArgs.outFile = NULL;
    globalArgs.verbosity = 0;
    globalArgs.inputFiles = NULL;
    globalArgs.numInputFiles = 0;

Цикл while и конструкция switch в листинге 8 реализуют самую важную часть обработки параметров команды в программе. Как только getopt() распознает параметр, конструкция switch определит, какой точно параметр был найден, и отметит соответствующее поле в структуре globalArgs. Когда getopt() вернет -1, то можно считать, что обработка параметров выполнена и оставшиеся аргументы являются файлами ввода.

Листинг 8. Обработка argc/argv при помощи getopt()
opt = getopt( argc, argv, optString );
    while( opt != -1 ) {
        switch( opt ) {
            case 'I':
                globalArgs.noIndex = 1; /* true */
                break;
                
            case 'l':
                globalArgs.langCode = optarg;
                break;
                
            case 'o':
                globalArgs.outFileName = optarg;
                break;
                
            case 'v':
                globalArgs.verbosity++;
                break;
                
            case 'h':   /* намеренный проход в следующий case-блок */
			case '?':
                display_usage();
                break;
                
            default:
                /* сюда на самом деле попасть невозможно. */
				break;
        }
        
        opt = getopt( argc, argv, optString );
    }
    
    globalArgs.inputFiles = argv + optind;
    globalArgs.numInputFiles = argc - optind;

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

Листинг 9. Основная цель работы программы
convert_document();
    
    return EXIT_SUCCESS;
}

Далее будет описано, как сделать так, чтобы программа работала в соответствии со стандартами конца 1990-х годов и поддерживала длинные опции, распространенные в GNU-приложениях.

Сложная обработка команды: getopt_long()

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

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

Функция getopt_long() принимает дополнительные параметры, одним из которых является указатель на массив структур option. Как видно из листинга 10, эта структура достаточно простая.

Листинг 10. Структура option для getopt_long()
struct option {
    char *name;
    int has_arg;
    int *flag;
    int val;
};

Поле name является указателем на длинное имя опции без двойного дефиса. Поле has_arg может принимать значения no_argument, optional_argument или required_argument (все эти значения определены в getopt.h) для указания, имеет ли эта опция аргумент или нет. Если переменная flag не установлена в NULL, то, как только в ходе процесса обработки команды появится указанная опция, числу типа int, на которое указывает переменная flag, будет присвоено значение переменной val. Если flag установлен в NULL, значение val будет возвращено функцией getopt_long() при обнаружении ею рассматриваемой опции; если настроить val на работу с короткими аргументами, функцию getopt_long() можно использовать без дополнительного кода – функция getopt(), которая содержит цикл while и switch блок, автоматически обработает эту опцию.

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

Давайте посмотрим, как внедрение getopt_long() в тестовую программу изменит ее (проект getopt_long_demo можно найти в разделе Материалы для скачивания).

Применение getopt_long()

Поскольку исходный код проекта getopt_long_demo практически такой же, как у проекта getopt_demo, рассмотренного ранее, будет рассмотрено только то, что изменилось в getopt_long_demo. Ввиду того что программа сейчас обладает большей гибкостью в работе, добавим поддержку опции --randomize без соответствующей короткой опции.

Функция getopt_long() находится в заголовочном файле getopt.h, а не в unistd.h, поэтому его необходимо подключить (листинг 11). Также подключается файл string.h, поскольку далее планируется использовать strcmp() чтобы узнать, обработка какого аргумента сейчас проводится.

Листинг 11. Подключение дополнительных заголовочных файлов
#include <getopt.h>
#include <string.h>

Затем добавляется флаг (листинг 12) к структуре globalArgs для опции --randomize и создается массив longOpts для хранения информации о длинных опциях, поддерживаемых программой. Кроме --randomize все длинные аргументы имеют соответствующие короткие версии (--no-index то же самое, что -I, например). Добавив в конец структуры соответствующие короткие опции, можно обрабатывать длинные опции без добавочного программного кода.

Листинг 12. Расширенные аргументы
struct globalArgs_t {
    int noIndex;                /* параметр -I */
    char *langCode;             /* параметр -l */
    const char *outFileName;    /* параметр -o */
    FILE *outFile;
    int verbosity;              /* параметр -v */
    char **inputFiles;          /* входные файлы */
    int numInputFiles;          /* число входных файлов */
    int randomized;             /* параметр --randomize */
} globalArgs;

static const char *optString = "Il:o:vh?";

static const struct option longOpts[] = {
    { "no-index", no_argument, NULL, 'I' },
    { "language", required_argument, NULL, 'l' },
    { "output", required_argument, NULL, 'o' },
    { "verbose", no_argument, NULL, 'v' },
    { "randomize", no_argument, NULL, 0 },
    { "help", no_argument, NULL, 'h' },
    { NULL, no_argument, NULL, 0 }
};

Листинг 13 меняет вызовы getop() на getopt_long(), которые принимают массив longOpts и указатель на int (longIndex) в дополнение к стандартным параметрам getopt(). Целое число, на которое указывает longIndex, будет равняться индексу только что обнаруженной длинной опции, когда getopt_long() вернет 0.

Листинг 13. Новая и улучшенная обработка опций
opt = getopt_long( argc, argv, optString, longOpts, &longIndex );
    while( opt != -1 ) {
        switch( opt ) {
            case 'I':
                globalArgs.noIndex = 1; /* true */
                break;
                
            case 'l':
                globalArgs.langCode = optarg;
                break;
                
            case 'o':
                globalArgs.outFileName = optarg;
                break;
                
            case 'v':
                globalArgs.verbosity++;
                break;
                
            case 'h':   /* намеренный проход в следующий case-блок */
			case '?':
                display_usage();
                break;

             case 0:     /* длинная опция без короткого эквивалента */
			 if( strcmp( "randomize", longOpts[longIndex].name ) == 0 ) {
                    globalArgs.randomized = 1;
                }
                break;
                
            default:
                /* сюда попасть невозможно. */
				break;
        }
        
        opt = getopt_long( argc, argv, optString, longOpts, amp;longIndex );
    }

Также добавлен case-блок для 0, в котором можно обработать длинную опцию, которая не имеет короткого аналога. В этом случае имеется только длинная опция, однако в коде все еще будет использоваться strcmp() для проверки, является ли эта опция ожидаемой.

Это все, что необходимо было сделать; теперь программа поддерживает более подробные и более дружелюбные для пользователя длинные опции.

Итого

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

getopt() является стандартной библиотечной функцией, которая позволяет "пройти" через параметры командной строки и распознать опции (с сопутствующими аргументами или без них), используя простые конструкции while/switch. Похожая функция getopt_long() позволяет обрабатывать сложные опции почти без дополнительных действий, направленных на улучшение функции, что очень нравится разработчикам.

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

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=AIX и UNIX
ArticleID=398505
ArticleTitle=Обработка команд при помощи getopt()
publish-date=06192009