Manejo de argumentos de línea de comando fácil de usar, potente y expresivo para C++ 14/11/17 contenidos en un único archivo de encabezado .
opciones, opciones+valor(es), valores posicionales, comandos posicionales, alternativas anidadas, árboles de decisión, indicadores unibles, filtros de valores personalizados, ...
generación de documentación (líneas de uso, páginas man); manejo de errores
muchos ejemplos; gran conjunto de pruebas
Considere esta interfaz de línea de comando:
SINOPSIS convertir <archivo de entrada> [-r] [-o <formato de salida>] [-utf16] OPCIONES -r, --recursive convierte archivos de forma recursiva -utf16 usa codificación UTF-16
Aquí está el código que define el input file
de valores posicionales y las tres opciones -r
, -o
y -utf16
. Si el análisis falla, el fragmento de página de manual predeterminado anterior se imprimirá en la salida estándar.
#include <iostream>#include "clipp.h"usando el espacio de nombres 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("archivo de entrada", infile),option("-r", "--recursive").set(rec).doc("convertir archivos recursivamente"),opción("-o") & valor("formato de salida", fmt),opción("-utf16").set(utf16).doc("usar codificación UTF-16") );if(!parse(argc, argv, cli)) cout << make_man_page(cli, argv[0]);// ...}
SINOPSIS buscador make <archivo de palabras> -dict <diccionario> [--progreso] [-v] buscador buscar <archivo de entrada>... -dict <diccionario> [-o <archivo de salida>] [-split|-nosplit] [-v] ayuda del buscador [-v] OPCIONES --progreso, -p mostrar progreso -o, --output <archivo de salida> escribe en el archivo en lugar de la salida estándar -split, -nosplit (no dividir) la salida -v, --version muestra la versión
Esta CLI tiene tres comandos alternativos ( make
, find
, help
), algunos argumentos de valor posicionales ( <wordfile>
, <infile>
) de los cuales uno es repetible, un indicador requerido con valor-argumento ( -dict <dictionary>
), un opción con argumento de valor ( -o <outfile>
), una opción con dos alternativas ( -split
, -nosplit
) y dos opciones convencionales ( -v
, --progress
).
Aquí está el código que define la interfaz, genera el fragmento de la página de manual anterior y maneja el resultado del análisis:
usando clipp de espacio de nombres; usando std::cout; usando std::string;//variables que almacenan el resultado del análisis; inicializados con sus valores predeterminados modo de clase enum {hacer, buscar, ayudar}; modo seleccionado = modo::ayuda; std::vector<cadena> entrada; string dict, out;bool split = false, progr = false;diccionario automático = requerido("-dict") & value("diccionario", dict);auto makeMode = (command("make").set(seleccionado,modo ::hacer), valores("archivo de palabras", entrada), diccionario, option("--progress", "-p").set(progr) % "mostrar progreso" );auto findMode = (command("find").set(selected,mode::find), valores("archivo", entrada), diccionario, (opción("-o", "--output") & valor("outfile", out)) % "escribir en el archivo en lugar de stdout", (opción("-split").set(split,true) | option("-nosplit").set(split,false) ) % "(no) dividir la salida" );auto cli = ( (makeMode | findMode | comando("ayuda").set(selected,mode::help) ),option("-v", "--version").call([]{cout << "versión 1.0nn" ;}).doc("mostrar versión") );if(parse(argc, argv, cli)) {switch(seleccionado) {case mode::make: /* ... */ break;case mode::find : /* ... */ break;case mode::help: cout << make_man_page(cli, "finder"); romper; } } demás { cout << use_lines(cli, "buscador") << 'n'; }
A continuación se muestran algunos ejemplos que deberían darle una idea de cómo funciona clipp. Considere esta configuración básica con algunas variables que queremos configurar usando argumentos de línea de comando:
int main(int argc, char* argv[]) { usando el espacio de nombres clipp;// define algunas variablesbool a = false, b = false;int n = 0, k = 0;double x = 0.0, y = 0.0; std::vector<int> ids;auto cli = ( /* EL CÓDIGO QUE DEFINE LA INTERFAZ DE LÍNEA DE COMANDO VA AQUÍ */ );parse(argc, argv, cli); //excluye argv[0]std::cout << use_lines(cli, "exe") << 'n'; }
Interfaz ( usage_lines ) | Código (contenido de cli paréntesis) |
---|---|
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 la sección de ejemplos para obtener explicaciones detalladas de cada tema.
Los calificadores de espacio de nombres se omiten en todos los ejemplos para mejorar la legibilidad. Todas las entidades están definidas en namespace clipp
.
int main(int argc, char* argv[]) { usando el espacio de nombres clipp;auto cli = ( /* EL CÓDIGO QUE DEFINE LA INTERFAZ DE LÍNEA DE COMANDO VA AQUÍ */ );parse(argc, argv, cli); //excluye argv[0]//si desea incluir argv[0]//parse(argv, argv+argc, cli);}
Hay dos tipos de componentes básicos para las interfaces de línea de comandos: parámetros y grupos. Las funciones de fábrica con nombres convenientes producen parámetros o grupos con la configuración deseada aplicada.
bool a = falso, f = falso; cuerda s; vector<string> vs;auto cli = ( // coincide con el comando repetible posicional requerido("push"), // exactamente sí sí norequired("-f", "--file").set(f), // exactamente sí no norequired("-a", "--all", "-A").set(a), // exactamente no no no value("archivo", s), // cualquier argumento sí sí novalues("archivo", vs), // cualquier argumento sí sí yesopt_value("archivo", s), // cualquier argumento no sí noopt_values("archivo" , vs), // cualquier argumento no sí sí//parámetro "capturar todo": útil para el manejo de erroresany_other(vs), // cualquier argumento no no sí//captura argumentos que cumplen un predicado y no coinciden con otros parámetroscualquier(predicado, vs) // predicado no no sí);
Las funciones anteriores son fábricas de conveniencia:
booleano f = verdadero; cadena s;auto v1 = valores("archivo", s);// es equivalente a:auto v2 = parámetro{match::nonempty}.label("archivo").blocking(true).repeatable(true).set (s);auto r1 = requerido("-f", "--file").set(f);// es equivalente a:auto r2 = parámetro{"-f", "--file"}.required (verdadero).set(f);
un parámetro requerido debe coincidir con al menos un argumento de la línea de comando
un parámetro repetible puede coincidir con cualquier número de argumentos
Los parámetros no posicionales (=sin bloqueo) pueden coincidir con argumentos en cualquier orden.
un parámetro posicional (de bloqueo) define un "punto de parada", es decir, hasta que coincida, todos los parámetros siguientes no pueden coincidir; una vez que coincida, todos los parámetros que lo preceden (dentro del grupo actual) serán inalcanzables
Si desea que los parámetros coincidan en secuencia, puede vincularlos usando el operator &
o la función de agrupación in_sequence
:
int norte = 1; cuerda s; vector<int> ls;auto cli = (//opción con valor requeridoopción("-n", "--repeat") & value("times", n),//marca requerida con valor opcionalrequerido("--file ") & opt_value("name", s), //opción con exactamente dos valoresoption("-p", "--pos") & value("x") & value("y"),//igual que antes de v vin_sequence( opción("-p", "--pos") , valor("x") , valor("y") ), //opción con al menos un valor (y opcionalmente más)opción("-l") & valores("líneas", ls) );
Los parámetros de valor utilizan una función de filtro para probar si se les permite coincidir con una cadena de argumento. El filtro predeterminado match::nonempty
que utilizan value
, values
, opt_value
y opt_values
coincidirá con cualquier cadena de argumento que no esté vacía. Puede proporcionar otras funciones de filtro/objetos de función como primer argumento de value
, values
, etc. o utilizar una de estas funciones de fábrica abreviadas integradas que cubren los casos más comunes:
nombre de cadena; doble r = 0,0; int n = 0;auto cli = (valor("usuario", nombre), // coincide con cualquier palabra de cadena que no esté vacía("usuario", nombre), // coincide con cualquier número de cadena alfanumérico que no esté vacío("ratio", r) , // coincide con representaciones de cadenas de números enteros("times", n) // coincide con representaciones de cadenas de números enteros);
De manera análoga a value
, opt_value
, etc. también hay funciones para words
, opt_word
, etc.
auto is_char = [](const string& arg) { return arg.size() == 1 && std::isalpha(arg[0]); };char c = ' '; // coincide con el valor repetible posicional requerido(is_char, "c", c); // un caracter si si no
agrupe parámetros mutuamente compatibles con paréntesis y comas:
auto cli = (opción("-a"), opción("-b"), opción("-c"));
agrupar parámetros mutuamente excluyentes como alternativas utilizando operator |
o one_of
:
auto cli1 = ( valor("archivo_entrada") | comando("lista") | comando("descarga") );auto cli2 = uno_de( valor("archivo_entrada"), comando("lista"), comando("descarga" ) );
agrupe los parámetros para que deban coincidir en secuencia usando operator &
o in_sequence
:
doble x = 0, y = 0, z = 0;auto cli1 = ( opción("-pos") & valor("X",x) & valor("Y",y) & valor("Z",z ) );auto cli2 = in_sequence( opción("-pos") , valor("X",x), valor("Y",y), valor("Z",z) );
Tenga en cuenta que los grupos circundantes no se ven afectados por esto, por lo que -a
y -b
pueden coincidir en cualquier orden, mientras que -b
y el valor X
deben coincidir en secuencia:
booleano a = falso, b = falso; int x = 0;auto cli = ( opción("-a").set(a), opción("-b").set(b) & valor("X",x));
Los grupos se pueden anidar y combinar para formar interfaces arbitrariamente complejas (ver aquí y aquí):
auto cli = ( comando("push") | ( comando("pull"), opción("-f", "--force") ) );
Los grupos también pueden ser repetibles:
auto cli1 = repetible( comando("flip") | comando("flop") );
forzar prefijos comunes en un grupo de banderas:
int x = 0;auto cli1 = with_prefix("-", opción("a"), opción("b") & valor("x",x), ...); // => -a -b ^no afectado^auto cli2 = with_prefix_short_long("-", "--", opción("a", "todos"), opción("b"), ...); // => -a --todos -b
forzar sufijos comunes en un grupo de banderas:
int x = 0;auto cli1 = with_suffix("=", opción("a") & valor("x",x), ...); // => a= ^no afectado^auto cli2 = with_suffix_short_long(":", ":=", opción("a", "todos"), opción("b"), ...); // => a: todos:= b:
hacer que un grupo de banderas se puedan unir:
auto cli1 = unible( opción("-a"), opción("-b")); //coincidirá con "-a", "-b", "-ab", "-ba"//funciona también con prefijos comunes arbitrarios:auto cli2 = joinable( option("--xA0"), option("- -xB1")); //también coincidirá con "--xA0B1" o "--xB1A0"
La forma más sencilla de conectar la interfaz de línea de comando con el resto de su código es vincular valores de objeto o llamadas de función (objeto) a parámetros (ver también aquí):
booleano b = falso; int yo = 5; int metro = 0; cadena x; ifstream fs;auto cli = ( option("-b").set(b), // "-b" detectado -> establecer b en trueoption("-m").set(m,2), // " -m" detectado -> establece m en 2opción("-x") y valor("X", x), // establece el valor de x a partir de la opción de cadena de argumento("-i") y opt_value("i", i) , // establece el valor de i desde la opción de cadena de argumento("-v").call( []{ cout << "v"; } ), // llama a la función (objeto) / lambdaoption("-v")( []{ cout << "v"; } ), // igual que la opción de línea anterior(" -f") & valor("archivo").call([&](cadena f){ fs.open(f); }) );
En el código de producción probablemente se usaría una clase de configuración:
configuración de estructura { bool x = false; /* ... */ }; configuración cmdline_settings(int argc, char* argv[]) { configuración s;auto cli = (opción("-x").set(sx), /* ... */ );parse(argc, argv, cli);return s; }
Tenga en cuenta que el objetivo debe ser:
un tipo fundamental ( int, long int, float, double, ...
)
un tipo que es convertible desde const char*
una entidad invocable: función, objeto de función/lambda que tiene una lista de parámetros vacía o exactamente un parámetro que se puede convertir desde const char*
Las cadenas de documentación para grupos y parámetros se pueden configurar con la función miembro doc
o con operator %
:
clic automático = ( ( opción("x").set(x).doc("establece X"),opción("y").set(y) % "establece Y" ), "grupo documentado 1:" % (opción("-g").set(g).doc("activa G"), opción("-h").set(h) % "activa H" ), (opción("-i").set(i) % "activa I", opción("-j").set(j) % "activa J" ).doc("grupo documentado 2:") );
Líneas de uso:
cout << use_lines(cli, "progname") << 'n';//con opciones de formatoauto fmt = doc_formatting{} .primera_columna(3) .last_column(79); cout << líneas_uso(cli, "nombre de programa", fmt) << 'n';
Documentación detallada:
cout << documentación(cli) << 'n';//con opciones de formatoauto fmt = doc_formatting{} .primera_columna(7) .doc_column(15) .last_column(99); cout << documentación (cli, fmt) << 'n';
Páginas man:
auto cli = ( /* EL CÓDIGO QUE DEFINE LA INTERFAZ DE LÍNEA DE COMANDO VA AQUÍ */ ); cout << make_man_page(cli, "progname") << 'n';//con opciones de formatoauto fmt = doc_formatting{} .primera_columna(7) .doc_column(15) .last_column(99); cout << make_man_page(cli, "nombre de programa", fmt) << 'n';
Cada parámetro puede tener funciones de controlador de eventos adjuntas. Estos se invocan una vez por cada argumento asignado al parámetro (o una vez por evento faltante):
archivo de cadena = "default.txt"; parámetro automático = requerido("-nof").set(archivo,"") | requerido("-f") y valor("archivo", archivo) // el 2.º, 3.º, 4.º,... coincidencia (sería un error en este caso) .if_repeated( [] { /* ... */ } ) // si falta el parámetro de valor requerido .if_missing( [] { /* ... */ } ) // si es inalcanzable, por ejemplo, no hay indicador "-f" antes del nombre del archivo .if_blocked( [] { /* ... */ } ) // si la coincidencia está en conflicto con otra alternativa "-nof" .if_conflicted( [] { /* ... */ } );
Las funciones del controlador también pueden tomar un int, que se establece en el índice del argumento en el que ocurrió el evento primero:
archivo de cadena = "default.txt"; parámetro automático = requerido("-nof").set(archivo,"") | requerido("-f") y valor("archivo", archivo) .if_repeated ( [] (int argIdx) { /* ... */ } ) .if_missing ( [] (int argIdx) { /* ... */ } ) .if_blocked ( [] (int argIdx) { /* ... */ } ) .if_conflicted( [] (int argIdx) { /* ... */ } );
Si damos -f -b
o -b -f -a
como argumentos de línea de comando para la siguiente CLI, se informará un error, ya que el valor después de -f
no es opcional:
auto cli = (opción("-a"), opción("-f") y valor("nombre de archivo"), opción("-b"));
Este comportamiento está bien para la mayoría de los casos de uso. Pero, ¿qué pasa si queremos que nuestro programa tome cualquier cadena como nombre de archivo, porque nuestros nombres de archivo también podrían colisionar con los nombres de las banderas? ¡Podemos hacer que el parámetro de valor sea codicioso con operator !
. De esta manera, la siguiente cadena después de -f
siempre coincidirá con la prioridad más alta tan pronto como se proporcione -f
:
auto cli = ( opción("-a"), opción("-f") & !valor("nombre de archivo"), opción("-b") );// ^~~~~~
¡Ten mucho cuidado con los parámetros codiciosos!
auto cli = ( /* su interfaz aquí */ );auto res = parse(argc, argv, cli);if(res.any_error()) { /* ... */ }//errores agregadosif(res.unmapped_args_count ()) { /* ... */ }if(res.any_bad_repeat()) { /* ... */ }if(res.any_blocked()) { /* ... */ }if(res.any_conflict()) { /* ... */ }