03.12
2020
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.