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ãoagrupe 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 -bforç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()) { /* ... */ }