Argumentos de Linha de Comando – Parte 2: getopt

por Fabio A. Mazzarino

Na primeira parte dos posts sobre linha de comando já abordamos o básico, agora vamos usar uma biblioteca padrão POSIX, getopt. E por que getopt? Porque está presente na maioria das variedades de Unix, um grande mercado de desenvolvimento de C/C++ atualmente.

A função getopt é utilizada para fazer parser de argumentos no padrão unix, utilizando – e — para nomear as opções, – para letras, — para palavras, com ou sem argumentos adicionais. Portanto utilizando getopt podemos facilmente fazer parser de linhas de comando como a seguinte:

$ ls -f -T 4 -w 80

A função aceita três parâmetros, os dois primeiros são os parâmetros da função main: argc e argv, conforme na parte 1 do post. O terceiro parâmetro é uma string, com as opções suportadas, seguidas de : caso aceitem parâmetro adicional.

getopt POSIX

Vamos a um exemplo, um programa que vai aceitar duas opções de execução, uma obrigatória a outra opcional, e depois das opções uma lista de arquivos de entrada:

  • -c – arquivo de configuração, obrigatório
  • -v – opcional, exibe trace de andamento do processamento através da saída de erro (stderr)

O parser da linha de comando ficaria assim:



#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    char *configfile = NULL;
    char **files = NULL;
    int verbose = 0;
    int nfiles = 0;
    char opt = -1;

    while ((opt = getopt(argc, argv, "c:v")) != -1) {
        switch (opt) {
        case 'c':
            configfile = (char*)malloc(strlen(optarg) + 1);
            strcpy(configfile, optarg);
            break;

        case 'v':
            verbose = 1;
            break;
        default:
            printf("Invalid option %c", opt);
        }
    }
    if (!configfile) {
        printf("Invalid Syntax. Missing configuration file\n");
        exit(1);
    }

    nfiles = argc - optind;
    if (nfiles > 0) {
        files = (char**)malloc(nfiles * sizeof(char*));
        for (int ctarg = optind, ctfile = 0; ctarg < argc; ctarg++, ctfile++) {
            files[ctfile] = (char*)malloc(strlen(argv[ctarg]) + 1);
            strcpy(files[ctfile], argv[ctarg]);
        }
    }

    printf("configfile: %s\n", configfile);
    printf("verbose: %d\n", verbose);
    for (int ct = 0; ct < nfiles; ct++)
        printf("file: %s\n", files[ct]);

    for (int ct = 0; ct < nfiles; ct++)
        free(files[ct]);
    free(files);
    free(configfile);
}

Que vai gerar a seguinte saída:

$ getoptargs -c test.conf -v input1.txt input2.txt input3.txt
argc: 7
configfile: test.conf
verbose: 1
file: input1.txt
file: input2.txt
file: input3.txt

A chamada da função getopt tem o terceiro parâmetro c:v, indicando que está esperando um argumento -c seguido de um argumento adicional, e um argumento -v. A falta de algum argumento obrigatório é feita após o loop do getopt.

Após a validação dos argumentos, para recuperar os argumentos adicionais, um loop entre optind e argc, armazenando em um array de ponteiros de caracteres.

getopt_long GNU

O GNU estendeu o escopo do getopt, e para evitar problemas de compatibilidade criou o getopt_long, que suporta além das opções com – e um caractere suporta — com nome de argumento. Por exemplo:

$ ls -f --tabsize 4 --width 80

Para exemplificar vamos estender as opções do exemplo anterior para suportar as opções -c ou –config, e -v ou –verbose. Vamos ao exemplo:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>

int main(int argc, char* argv[]) {
    char *configfile = NULL;
    char **files = NULL;
    int verbose = 0;
    int nfiles = 0;
    char opt = -1;
    static struct option opts[] = {
        {"config",  required_argument,  NULL,       'c'},
        {"verbose", no_argument,        NULL,       'v'},
        {NULL,      0,                  NULL,       0}
    };

    while ((opt = getopt_long(argc, argv, "c:v", opts, NULL)) != -1) {
        switch (opt) {
        case 'c':
            configfile = (char*)malloc(strlen(optarg) + 1);
            strcpy(configfile, optarg);
            break;
        case 'v':
            verbose = 1;
            break;
        default:
            printf("Invalid option %c", opt);
        }
    }
    if (!configfile) {
        printf("Invalid Syntax. Missing configuration file\n");
        exit(1);
    }

    nfiles = argc - optind;
    if (nfiles > 0) {
        files = (char**)malloc(nfiles * sizeof(char*));
        for (int ctarg = optind, ctfile = 0; ctarg < argc; ctarg++, ctfile++) {
            files[ctfile] = (char*)malloc(strlen(argv[ctarg]) + 1);
            strcpy(files[ctfile], argv[ctarg]);
        }
    }

    printf("configfile: %s\n", configfile);
    printf("verbose: %d\n", verbose);
    for (int ct = 0; ct < nfiles; ct++)
        printf("file: %s\n", files[ct]);

    for (int ct = 0; ct < nfiles; ct++)
        free(files[ct]);
    free(files);
    free(configfile);
}

Note a diferença na declaração da estrutura struct option, que contém informações sobre os nomes longos dos parâmetros. Isso vai permitir associar a opção longa (–verbose) com a opção curta (-v). Além disso o include é diferente, getopt POSIX está declarado em unistd.h, e getopt GNU está declarado em getopt.h.

Vamos ver como ficaria a saída:

$ getoptlong  -c test.conf -v input1.txt input2.txt input3.txt
argc: 7
configfile: test.conf
verbose: 1
file: input1.txt
file: input2.txt
file: input3.txt

Idêntica ao exemplo anterior, mas podemos também utilizar as opções longas:

$ getoptlong  -c test.conf --verbose input1.txt input2.txt input3.txt

Que gerará exatamente a mesma saída.

Quando Utilizar?

Em alguns ambientes o getopt_long não estará disponível, algumas equipes não homologam, ou não podem homologar, a biblioteca GNU, ficando restritas a POSIX. Nesse caso não há discussão, deve-se utilizar a versão POSIX.

Em ambientes aonde as bibliotecas GNU estão disponíveis é perfeitamente possível utilizar getopt_long, deve-se apenas ter em mente que sua configuração é mais complicada, portanto implica em maior manutenção. Já o getopt POSIX é muito simples e fácil de ser implementado, exigindo o mínimo de manutenção.

No final da história acaba sempre sendo uma definição de projeto.