Manipulação de argumentos de linha de comando fácil de usar, poderosa e expressiva para C++ 11/14/17 contida em um único arquivo de cabeçalho .
opções, opções+valor(es), valores posicionais, comandos posicionais, alternativas aninhadas, árvores de decisão, sinalizadores juntáveis, filtros de valor personalizados, ...
geração de documentação (linhas de uso, páginas man); tratamento de erros
muitos exemplos; grande conjunto de testes
Considere esta interface de linha de comando:
SINOPSE converter <arquivo de entrada> [-r] [-o <formato de saída>] [-utf16] OPÇÕES -r, --recursive converte arquivos recursivamente -utf16 usa codificação UTF-16
Aqui está o código que define o input file
do valor posicional e as três opções -r
, -o
e -utf16
. Se a análise falhar, o snippet de página de manual padrão acima será impresso em stdout.
#include <iostream>#include "clipp.h"usando o namespace clipp; usando std::cout; usando std::string;int main(int argc, char* argv[]) { bool rec = false, utf16 = false; string infile = "", fmt = "csv";auto cli = (value("arquivo de entrada", infile),option("-r", "--recursive").set(rec).doc("converter arquivos recursivamente"),option("-o") & value("formato de saída", fmt),option("-utf16").set(utf16).doc("usar codificação UTF-16") );if(!parse(argc, argv, cli)) cout << make_man_page(cli, argv[0]);// ...}
SINOPSE localizador make <arquivodepalavras> -dict <dicionário> [--progresso] [-v] localizador encontrar <arquivo de entrada>... -dict <dicionário> [-o <arquivo de saída>] [-split|-nosplit] [-v] ajuda do localizador [-v] OPÇÕES --progresso, -p mostra o progresso -o, --output <outfile> grava no arquivo em vez de stdout -split, -nosplit (não) divide a saída -v, --version mostra a versão
Esta CLI possui três comandos alternativos ( make
, find
, help
), alguns argumentos de valor posicionais ( <wordfile>
, <infile>
) dos quais um é repetível, um sinalizador obrigatório com argumento de valor ( -dict <dictionary>
), um opção com argumento de valor ( -o <outfile>
), uma opção com duas alternativas ( -split
, -nosplit
) e duas opções convencionais ( -v
, --progress
).
Aqui está o código que define a interface, gera o trecho da página de manual acima e trata o resultado da análise:
usando clipp de namespace; usando std::cout; using std::string;//variáveis que armazenam o resultado da análise; inicializado com seu modo de classe de valor padrão {make, find, help}; modo selecionado = modo::ajuda; std::vector<string> entrada; string dict, out;bool split = false, progr = false;auto dicionário = obrigatório("-dict") & value("dicionário", dict);auto makeMode = (command("make").set(selected,mode ::fazer), valores("arquivo de palavras", entrada), dicionário, option("--progress", "-p").set(progr) % "show progress" );auto findMode = (command("find").set(selected,mode::find), valores("arquivo", entrada), dicionário, (option("-o", "--output") & value("outfile", out)) % "gravar no arquivo em vez de stdout", (opção("-dividir").set(dividir,verdadeiro) | option("-nosplit").set(split,false) ) % "(não) dividir a saída" );auto cli = ( (makeMode | findMode | command("help").set(selected,mode::help) ),option("-v", "--version").call([]{cout << "version 1.0nn" ;}).doc("mostrar versão") );if(parse(argc, argv, cli)) {switch(selected) {case mode::make: /* ... */ break;case mode::find : /* ... */ break;case mode::help: cout << make_man_page(cli, "finder"); quebrar; } } outro { cout << linhas_de_uso(cli, "finder") << 'n'; }
Abaixo estão alguns exemplos que devem dar uma ideia de como o clipp funciona. Considere esta configuração básica com algumas variáveis que queremos definir usando argumentos de linha de comando:
int main(int argc, char* argv[]) { usando namespace clipp;// define algumas variáveisbool a = false, b = false;int n = 0, k = 0;double x = 0,0, y = 0,0; std::vector<int> ids;auto cli = ( /* INTERFACE DE LINHA DE COMANDO DE DEFINIÇÃO DE CÓDIGO VAI AQUI */ );parse(argc, argv, cli); //exclui argv[0]std::cout << usage_lines(cli, "exe") << 'n'; }
Interface ( usage_lines ) | Código (conteúdo dos parênteses cli ) |
---|---|
exe [-a] | option("-a", "--all").set(a) |
exe [--all] | option("--all", "-a", "--ALL").set(a) |
exe [-a] [-b] | option("-a").set(a), option("-b").set(b) |
exe -a | required("-a").set(a) |
exe [-a] -b | option("-a").set(a), required("-b").set(b) |
exe [-n <times>] | option("-n", "--iter") & value("times", n) |
exe [-n [<times>]] | option("-n", "--iter") & opt_value("times", n) |
exe -n <times> | required("-n", "--iter") & value("times", n) |
exe -n [<times>] | required("-n", "--iter") & opt_value("times", n) |
exe [-c <x> <y>] | option("-c") & value("x", x) & value("y", y) |
exe -c <x> <y> | required("-c") & value("x", x) & value("y", y) |
exe -c <x> [<y>] | required("-c") & value("x", x) & opt_value("y", y) |
exe [-l <lines>...] | option("-l") & values("lines", ids) |
exe [-l [<lines>...]] | option("-l") & opt_values("lines", ids) |
exe [-l <lines>]... | repeatable( option("-l") & value("lines", ids) ) |
exe -l <lines>... | required("-l") & values("lines", ids) |
exe -l [<lines>...] | required("-l") & opt_values("lines", ids) |
exe (-l <lines>)... | repeatable( required("-l") & value("lines", ids) ) |
exe fetch [-a] | command("fetch").set(k,1), option("-a").set(a) |
exe init | fetch [-a] | command("init").set(k,0) | (command("fetch").set(k,1), option("-a").set(a)) |
exe [-a|-b] | option("-a").set(a) | option("-b").set(b) |
exe [-ma|b] | option("-m") & (required("a").set(a) | required("b").set(b)) |
Consulte a seção de exemplos para explicações detalhadas de cada tópico.
Os qualificadores de namespace são omitidos de todos os exemplos para melhor legibilidade. Todas as entidades são definidas no namespace clipp
.
int main(int argc, char* argv[]) { using namespace clipp;auto cli = ( /* CODE DEFINING COMMAND LINE INTERFACE VAI AQUI */ );parse(argc, argv, cli); //exclui argv[0]//se você deseja incluir argv[0]//parse(argv, argv+argc, cli);}
Existem dois tipos de blocos de construção para interfaces de linha de comando: parâmetros e grupos. Funções de fábrica com nomes convenientes produzem parâmetros ou grupos com as configurações desejadas aplicadas.
bool a = falso, f = falso; cadeia de caracteres; vector<string> vs;auto cli = ( // corresponde ao posicional requerido repeatablecommand("push"), // exatamente sim sim norequired("-f", "--file").set(f), // exatamente sim não norequired("-a", "--all", "-A").set(a), // exatamente não não não value("file", s), // qualquer argumento sim sim novalues("file", vs), // qualquer argumento sim sim yesopt_value("file", s), // qualquer argumento não sim noopt_values("file" , vs), // qualquer argumento não sim sim // parâmetro "catch all" - útil para tratamento de errosany_other(vs), // qualquer argumento não não sim // captura argumentos que atendem a um predicado e não são correspondidos por outros parâmetrosany(predicado, vs) // predicado não não sim);
As funções acima são fábricas de conveniência:
bool f = verdadeiro; string s;auto v1 = valores("arquivo", s);// é equivalente a:auto v2 = parâmetro{match::nonempty}.label("file").blocking(true).repeatable(true).set (s);auto r1 = obrigatório("-f", "--file").set(f);// é equivalente a:auto r2 = parâmetro{"-f", "--file"}.required (verdadeiro).set(f);
um parâmetro obrigatório deve corresponder a pelo menos um argumento de linha de comando
um parâmetro repetível pode corresponder a qualquer número de argumentos
parâmetros não posicionais (= sem bloqueio) podem corresponder a argumentos em qualquer ordem
um parâmetro posicional (bloqueio) define um "ponto de parada", ou seja, até que ele corresponda, todos os parâmetros seguintes não poderão corresponder; uma vez correspondido, todos os parâmetros anteriores (dentro do grupo atual) se tornarão inacessíveis
Se quiser que os parâmetros sejam correspondidos em sequência, você pode uni-los usando o operator &
ou a função de agrupamento in_sequence
:
interno n = 1; cadeia de caracteres; vector<int> ls;auto cli = (//opção com valor obrigatóriooption("-n", "--repeat") & value("times", n),//sinalizador obrigatório com valor opcionalrequired("--file ") & opt_value("name", s), //opção com exatamente dois valoresoption("-p", "--pos") & value("x") & value("y"),//igual a antes de v vin_sequence( option("-p", "--pos") , value("x") , value("y") ), //opção com pelo menos um valor (e opcionalmente mais)option("-l") & valores("linhas", ls) );
Os parâmetros de valor usam uma função de filtro para testar se eles podem corresponder a uma sequência de argumentos. O filtro padrão match::nonempty
usado por value
, values
, opt_value
e opt_values
corresponderá a qualquer string de argumento não vazia. Você pode fornecer outras funções de filtro/objetos de função como primeiro argumento de value
, values
, etc. ou usar uma dessas funções abreviadas de fábrica integradas que cobrem os casos mais comuns:
nome da sequência; duplo r = 0,0; int n = 0;auto cli = (value("user", name), // corresponde a qualquer stringword não vazia("user", name), // corresponde a qualquer stringnumber alfanumérico não vazio("ratio", r) , // corresponde a representações de strings de números inteiros("times", n) // corresponde a representações de strings de números inteiros);
Análogo a value
, opt_value
, etc. também existem funções para words
, opt_word
, etc.
auto is_char = [](const string& arg) { return arg.size() == 1 && std::isalpha(arg[0]); };char c = ' '; // corresponde ao valor repetitivo posicional necessário(is_char, "c", c); // um caractere sim sim não
agrupe parâmetros mutuamente compatíveis com parênteses e vírgulas:
auto cli = (opção("-a"), opção("-b"), opção("-c") );
agrupar parâmetros mutuamente exclusivos como alternativas usando operator |
ou one_of
:
auto cli1 = ( valor("arquivo_de_entrada") | comando("lista") | comando("flush") );auto cli2 = one_of( valor("arquivo_de_entrada") , comando("lista") , comando("flush" ) );
agrupe parâmetros para que eles sejam correspondidos em sequência usando operator &
ou in_sequence
:
duplo x = 0, y = 0, z = 0;auto cli1 = (opção("-pos") & valor("X",x) & valor("Y",y) & valor("Z",z ) );auto cli2 = in_sequence( opção("-pos") , valor("X",x) , valor("Y",y) , valor("Z",z) );
Observe que os grupos vizinhos não são afetados por isso, de modo que -a
e -b
podem ser correspondidos em qualquer ordem, enquanto -b
e o valor X
devem corresponder em sequência:
bool a = falso, b = falso; int x = 0;auto cli = (opção("-a").set(a), opção("-b").set(b) & valor("X",x) );
grupos podem ser aninhados e combinados para formar interfaces arbitrariamente complexas (veja aqui e aqui):
auto cli = (comando("push") | (comando("pull"), opção("-f", "--force") ));
grupos também podem ser repetíveis:
auto cli1 = repetível(comando("flip") | comando("flop") );
forçar prefixos comuns em um grupo de sinalizadores:
int x = 0;auto cli1 = with_prefix("-", option("a"), option("b") & value("x",x), ... ); // => -a -b ^não afetado^auto cli2 = with_prefix_short_long("-", "--", option("a", "all"), option("b"), ... ); // => -a --all -b
forçar sufixos comuns em um grupo de sinalizadores:
int x = 0;auto cli1 = with_suffix("=", option("a") & value("x",x), ... ); // => a= ^não afetado^auto cli2 = with_suffix_short_long(":", ":=", option("a", "all"), option("b"), ... ); // => a: todos:= b:
faça um grupo de bandeiras juntáveis:
auto cli1 = joinable(opção("-a"), opção("-b")); //corresponderá a "-a", "-b", "-ab", "-ba"//funciona também com prefixos comuns arbitrários:auto cli2 = joinable( option("--xA0"), option("- -xB1")); //também corresponderá a "--xA0B1" ou "--xB1A0"
A maneira mais fácil de conectar a interface de linha de comando ao resto do seu código é vincular valores de objeto ou chamadas de função (objeto) a parâmetros (veja também aqui):
bool b = falso; int eu = 5; interno m = 0; cadeia x; ifstream fs;auto cli = ( option("-b").set(b), // "-b" detectado -> definir b como trueoption("-m").set(m,2), // " -m" detectado -> definir m para 2option("-x") & value("X", x), // definir o valor de x a partir do argumento string option("-i") & opt_value("i", i) , // define o valor de i a partir do argumento string option("-v").call( []{ cout << "v" } ), // chama a função (objeto) / lambdaoption("-v")( []{ cout << "v"; } ), // igual ao anterior lineoption("-f" ) & value("arquivo").call([&](string f){ fs.open(f); }) );
No código de produção provavelmente usaríamos uma classe de configurações:
configurações de estrutura {bool x = false; /* ... */ }; configurações cmdline_settings(int argc, char* argv[]) { configurações s;auto cli = ( option("-x").set(sx), /* ... */ );parse(argc, argv, cli);return s; }
Observe que o destino deve ser:
um tipo fundamental ( int, long int, float, double, ...
)
um tipo que é conversível de const char*
uma entidade que pode ser chamada: função, objeto de função/lambda que possui uma lista de parâmetros vazia ou exatamente um parâmetro que é conversível de const char*
Docstrings para grupos e parâmetros podem ser definidos com a função de membro doc
ou com operator %
:
cli automático = ( (opção("x").set(x).doc("configura X"),option("y").set(y) % "configura Y" ), "grupo documentado 1:"% (opção("-g").set(g).doc("ativa G"), option("-h").set(h) % "ativa H" ), (opção("-i").set(i) % "ativa I", opção("-j").set(j) % "ativa J" ).doc("grupo documentado 2:") );
Linhas de uso:
cout << usage_lines(cli, "progname") << 'n';//com opções de formataçãoauto fmt = doc_formatting{} .primeira_coluna(3) .última_coluna(79); cout << linhas_de_uso(cli, "nome do programa", fmt) << 'n';
Documentação detalhada:
cout << documentação(cli) << 'n';//com opções de formataçãoauto fmt = doc_formatting{} .primeira_coluna(7) .doc_column(15) .última_coluna(99); cout << documentação(cli, fmt) << 'n';
Páginas de manual:
auto cli = ( /*CODE DEFINING INTERFACE DE LINHA DE COMANDO VAI AQUI*/ ); cout << make_man_page(cli, "progname") << 'n';//com opções de formataçãoauto fmt = doc_formatting{} .primeira_coluna(7) .doc_column(15) .última_coluna(99); cout << make_man_page(cli, "nome do programa", fmt) << 'n';
Cada parâmetro pode ter funções de manipulador de eventos anexadas a ele. Eles são invocados uma vez para cada argumento mapeado para o parâmetro (ou uma vez por evento ausente):
string arquivo = "default.txt";auto parâmetro = obrigatório("-nof").set(arquivo,"") | obrigatório("-f") & valor("arquivo", arquivo) // na 2ª, 3ª, 4ª,... correspondência (seria um erro neste caso) .if_repeated( [] { /* ... */ } ) // se o valor-param necessário estava faltando .if_missing( [] { /* ... */ } ) // se inacessível, por exemplo, nenhum sinalizador "-f" antes do nome do arquivo .if_blocked( [] { /* ... */ } ) // se a correspondência estiver em conflito com outra alternativa "-nof" .if_conflicted( [] { /* ... */ } );
As funções do manipulador também podem receber um int, que é definido como o índice do argumento no qual o evento ocorreu primeiro:
string arquivo = "default.txt";auto parâmetro = obrigatório("-nof").set(arquivo,"") | obrigatório("-f") & valor("arquivo", arquivo) .if_repeated ( [] (int argIdx) { /* ... */ } ) .if_missing ( [] (int argIdx) { /* ... */ } ) .if_blocked ( [] (int argIdx) { /* ... */ } ) .if_conflicted( [] (int argIdx) { /* ... */ } );
Se fornecermos -f -b
ou -b -f -a
como argumentos de linha de comando para a CLI a seguir, um erro será relatado, pois o valor após -f
não é opcional:
auto cli = (opção("-a"), opção("-f") & valor("nome do arquivo"), opção("-b") );
Esse comportamento é adequado para a maioria dos casos de uso. Mas e se quisermos que nosso programa use qualquer string como nome de arquivo, porque nossos nomes de arquivos também podem colidir com nomes de sinalizadores? Podemos tornar o parâmetro de valor ganancioso com operator !
. Dessa forma, a próxima string após -f
sempre corresponderá à prioridade mais alta assim que -f
for fornecido:
auto cli = (opção("-a"), opção("-f") & !value("nome do arquivo"), opção("-b") );// ^~~~~~
Tenha muito cuidado com parâmetros gananciosos!
auto cli = ( /* sua interface aqui */ );auto res = parse(argc, argv, cli);if(res.any_error()) { /* ... */ }//erros agregadosif(res.unmapped_args_count ()) { /* ... */ }if(res.any_bad_repeat()) { /* ... */ }if(res.any_blocked()) { /* ... */ }if(res.any_conflict()) { /* ... */ }