Usando getopts para procesar opciones de línea de comando largas y cortas

410

Deseo que se invoquen formas largas y cortas de opciones de línea de comando utilizando mi script de shell.

Sé que getoptsse puede usar, pero como en Perl, no he podido hacer lo mismo con Shell.

Alguna idea sobre cómo se puede hacer esto, para que pueda usar opciones como:

./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/

En lo anterior, ambos comandos significan lo mismo para mi shell, pero usando getopts, ¿no he podido implementarlos?

gagneet
fuente
2
En mi humilde opinión, la respuesta aceptada no es la mejor. No muestra cómo usar getopts para manejar los argumentos "-" y "-", lo que se puede hacer, como lo demostró @Arvid Requate. Estoy insertando otra respuesta usando un concepto similar, pero también trata con el error del usuario de "olvidar" insertar valores para argumentos que son necesarios. Punto clave: se puede hacer que los getopts funcionen. El usuario debe evitar usar "getopt" en su lugar si se necesita portabilidad multiplataforma. Además, getopts es parte del estándar POSIX para shells, por lo que es probable que sea portátil.
pauljohn32

Respuestas:

304

Hay tres implementaciones que pueden considerarse:

  • Bash incorporado getopts. Esto no admite nombres largos de opciones con el prefijo de doble guión. Solo admite opciones de un solo carácter.

  • Implementación BSD UNIX del getoptcomando independiente (que es lo que usa MacOS). Esto tampoco admite opciones largas.

  • Implementación GNU de autónomo getopt. GNU getopt(3)(utilizado por la línea de comandos getopt(1)en Linux) admite el análisis de opciones largas.


Algunas otras respuestas muestran una solución para usar bash builtin getoptspara imitar opciones largas. Esa solución en realidad hace una opción corta cuyo carácter es "-". Entonces obtienes "-" como la bandera. Luego, todo lo que sigue se convierte en OPTARG, y prueba el OPTARG con un anidado case.

Esto es inteligente, pero viene con advertencias:

  • getoptsno se puede hacer cumplir la especificación opt. No puede devolver errores si el usuario proporciona una opción no válida. Debe realizar su propia comprobación de errores mientras analiza OPTARG.
  • OPTARG se usa para el nombre de la opción larga, lo que complica el uso cuando la opción larga tiene un argumento. Termina teniendo que codificarlo usted mismo como un caso adicional.

Por lo tanto, si bien es posible escribir más código para evitar la falta de soporte para opciones largas, esto es mucho más trabajo y anula parcialmente el propósito de usar un analizador getopt para simplificar su código.

Bill Karwin
fuente
18
Entonces. ¿Qué es la solución multiplataforma portátil?
troelskn
66
GNU Getopt parece ser la única opción. En Mac, instale GNU getopt desde macports. En Windows, instalaría GNU getopt con Cygwin.
Bill Karwin el
2
Aparentemente , ksh getopts puede manejar opciones largas.
Tgr
1
@Bill +1, aunque también es bastante sencillo construir getopt desde la fuente ( software.frodo.looijaard.name/getopt ) en Mac. También puede verificar la versión de getopt instalada en su sistema desde scripts con "getopt -T; echo $?".
Chinasaur
8
@Bill Karwin: "El bash getopts incorporado no admite nombres largos de opciones con el prefijo de doble guión". Pero se pueden hacer getopts para admitir opciones largas: consulte stackoverflow.com/a/7680682/915044 a continuación.
TomRoche
307

getopty getoptsson diferentes bestias, y las personas parecen tener un poco de incomprensión de lo que hacen. getoptses un comando incorporado bashpara procesar las opciones de la línea de comandos en un bucle y asignar cada opción y valor encontrados a su vez a las variables incorporadas, para que pueda procesarlas aún más. getopt, sin embargo, es un programa de utilidad externo, y en realidad no procesa sus opciones para usted de la manera en que lo hacen, por ejemplo getopts, bash , el Getoptmódulo Perl o Python optparse/ argparsemodules. Todo lo que getopthace es canonizar las opciones que se pasan, es decir, convertirlas a una forma más estándar, para que sea más fácil para un script de shell procesarlas. Por ejemplo, una aplicación de getoptpodría convertir lo siguiente:

myscript -ab infile.txt -ooutfile.txt

dentro de esto:

myscript -a -b -o outfile.txt infile.txt

Tienes que hacer el procesamiento real tú mismo. No tiene que usarlo getopten absoluto si establece varias restricciones en la forma en que puede especificar opciones:

  • solo pon una opción por argumento;
  • todas las opciones van antes que cualquier parámetro posicional (es decir, argumentos sin opciones);
  • para opciones con valores (por ejemplo, -oarriba), el valor debe ir como un argumento separado (después de un espacio).

¿Por qué usar en getoptlugar de getopts? La razón básica es que solo GNU getoptle brinda soporte para las opciones de línea de comandos de nombre largo. 1 (GNU getoptes el valor predeterminado en Linux. Mac OS X y FreeBSD vienen con una getoptversión básica y no muy útil , pero la versión GNU se puede instalar; ver más abajo).

Por ejemplo, aquí hay un ejemplo del uso de GNU getopt, de un script mío llamado javawrap:

# NOTE: This requires GNU getopt.  On Mac OS X and FreeBSD, you have to install this
# separately; see below.
TEMP=`getopt -o vdm: --long verbose,debug,memory:,debugfile:,minheap:,maxheap: \
             -n 'javawrap' -- "$@"`

if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi

# Note the quotes around `$TEMP': they are essential!
eval set -- "$TEMP"

VERBOSE=false
DEBUG=false
MEMORY=
DEBUGFILE=
JAVA_MISC_OPT=
while true; do
  case "$1" in
    -v | --verbose ) VERBOSE=true; shift ;;
    -d | --debug ) DEBUG=true; shift ;;
    -m | --memory ) MEMORY="$2"; shift 2 ;;
    --debugfile ) DEBUGFILE="$2"; shift 2 ;;
    --minheap )
      JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MinHeapFreeRatio=$2"; shift 2 ;;
    --maxheap )
      JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MaxHeapFreeRatio=$2"; shift 2 ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

Esto le permite especificar opciones como --verbose -dm4096 --minh=20 --maxhe 40 --debugfi="/Users/John Johnson/debug.txt"o similar. El efecto de la llamada a getoptes canonizar las opciones para --verbose -d -m 4096 --minheap 20 --maxheap 40 --debugfile "/Users/John Johnson/debug.txt"que pueda procesarlas más fácilmente. La cita alrededor "$1"y"$2" es importante ya que garantiza que los argumentos con espacios en ellos se manejen correctamente.

Si elimina las primeras 9 líneas (todo a través de la eval setlínea), ¡el código seguirá funcionando ! Sin embargo, su código será mucho más exigente en el tipo de opciones que acepta: en particular, tendrá que especificar todas las opciones en el formulario "canónico" descrito anteriormente. Sin getoptembargo, con el uso de , puede agrupar opciones de una sola letra, usar formas más cortas y no ambiguas de opciones largas, usar el estilo --file foo.txto --file=foo.txt, usar el estilo -m 4096o -m4096, mezclar opciones y no opciones en cualquier orden, etc. getopttambién genera un mensaje de error si se encuentran opciones no reconocidas o ambiguas.

NOTA : En realidad, hay dos versiones totalmente diferentes de getopt, basic getopty GNU getopt, con diferentes características y diferentes convenciones de llamadas. 2 Basic getoptestá bastante roto: no solo no maneja opciones largas, sino que ni siquiera puede manejar espacios incrustados dentro de argumentos o argumentos vacíos, mientras getoptsque lo hace bien. El código anterior no funcionará en básico getopt. GNU getoptse instala por defecto en Linux, pero en Mac OS X y FreeBSD necesita instalarse por separado. En Mac OS X, instale MacPorts ( http://www.macports.org ) y luego sudo port install getoptinstale GNU getopt(generalmente en /opt/local/bin), y asegúrese de que /opt/local/binesté en su ruta de shell antes de/usr/bin . En FreeBSD, instalemisc/getopt.

Una guía rápida para modificar el código de ejemplo para su propio programa: de las primeras líneas, todas son "repetitivas" que deben permanecer igual, excepto la línea que llama getopt. Debe cambiar el nombre del programa después -n, especificar opciones cortas después -oy opciones largas después --long. Poner dos puntos después de las opciones que toman un valor.

Finalmente, si ve un código que tiene solo en setlugar de eval set, fue escrito para BSD getopt. Debe cambiarlo para usar el eval setestilo, que funciona bien con ambas versiones de getopt, mientras que el plano setno funciona bien con GNU getopt.

1 En realidad, getoptsen ksh93soportes opciones nombrado largo, pero esta capa no se utiliza tan a menudo como bash. En zsh, use zparseoptspara obtener esta funcionalidad.

2 Técnicamente, "GNU getopt" es un nombre inapropiado; Esta versión fue escrita para Linux en lugar del proyecto GNU. Sin embargo, sigue todas las convenciones de GNU, y el término "GNU getopt" se usa comúnmente (por ejemplo, en FreeBSD).

Vagabundo urbano
fuente
3
Esto fue muy útil, la idea de usar getopt para verificar las opciones y luego procesar esas opciones en un ciclo muy simple funcionó realmente bien cuando quería agregar opciones de estilo largas a un script bash. Gracias.
ianmjones
2
getopten Linux no es una utilidad GNU y lo tradicional getoptno proviene inicialmente de BSD sino de AT&T Unix. ksh93's getopts(también de AT&T) admite opciones largas de estilo GNU.
Stephane Chazelas
@StephaneChazelas: editado para reflejar sus comentarios. Todavía prefiero el término "GNU getopt" a pesar de que es un nombre inapropiado, porque esta versión sigue las convenciones de GNU y generalmente actúa como un programa GNU (por ejemplo, haciendo uso de POSIXLY_CORRECT), mientras que "getopt mejorado por Linux" sugiere erróneamente que esta versión solo existe en Linux
Urban Vagabond
1
Proviene del paquete util-linux, por lo que es Linux solo porque ese paquete de software está destinado solo para Linux (que getoptfácilmente podría ser portado a otros Unices, pero muchos otros programas util-linuxson específicos de Linux). Todos los programas que no son GNU que utilizan GNU getopt (3) entienden $POSIX_CORRECT. Por ejemplo, no diría que aplayes GNU solo por esos motivos. Sospecho que cuando FreeBSD menciona GNU getopt, se refieren a GNU getopt (3) C API.
Stephane Chazelas
@StephaneChazelas - FreeBSD tiene un mensaje de error "Generar dependencia: instale GNU getopt" que se refiere inequívocamente a la getoptutilidad, no a getopt (3).
Urban Vagabond
202

La función getopts incorporada de Bash se puede usar para analizar opciones largas poniendo un carácter de guión seguido de dos puntos en la especificación de operación:

#!/usr/bin/env bash 
optspec=":hv-:"
while getopts "$optspec" optchar; do
    case "${optchar}" in
        -)
            case "${OPTARG}" in
                loglevel)
                    val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
                    echo "Parsing option: '--${OPTARG}', value: '${val}'" >&2;
                    ;;
                loglevel=*)
                    val=${OPTARG#*=}
                    opt=${OPTARG%=$val}
                    echo "Parsing option: '--${opt}', value: '${val}'" >&2
                    ;;
                *)
                    if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                        echo "Unknown option --${OPTARG}" >&2
                    fi
                    ;;
            esac;;
        h)
            echo "usage: $0 [-v] [--loglevel[=]<value>]" >&2
            exit 2
            ;;
        v)
            echo "Parsing option: '-${optchar}'" >&2
            ;;
        *)
            if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                echo "Non-option argument: '-${OPTARG}'" >&2
            fi
            ;;
    esac
done

Después de copiar al archivo ejecutable name = getopts_test.shen el directorio de trabajo actual , uno puede producir resultados como

$ ./getopts_test.sh
$ ./getopts_test.sh -f
Non-option argument: '-f'
$ ./getopts_test.sh -h
usage: code/getopts_test.sh [-v] [--loglevel[=]<value>]
$ ./getopts_test.sh --help
$ ./getopts_test.sh -v
Parsing option: '-v'
$ ./getopts_test.sh --very-bad
$ ./getopts_test.sh --loglevel
Parsing option: '--loglevel', value: ''
$ ./getopts_test.sh --loglevel 11
Parsing option: '--loglevel', value: '11'
$ ./getopts_test.sh --loglevel=11
Parsing option: '--loglevel', value: '11'

Obviamente, getopts no realiza OPTERRcomprobaciones ni analiza argumentos de opciones para las opciones largas. El fragmento de script anterior muestra cómo se puede hacer esto manualmente. El principio básico también funciona en el shell Debian Almquist ("guión"). Tenga en cuenta el caso especial:

getopts -- "-:"  ## without the option terminator "-- " bash complains about "-:"
getopts "-:"     ## this works in the Debian Almquist shell ("dash")

Tenga en cuenta que, como GreyCat de http://mywiki.wooledge.org/BashFAQ señala, este truco explota un comportamiento no estándar del shell que permite el argumento de opción (es decir, el nombre de archivo en "-f filename") ser concatenado a la opción (como en "-ffilename"). El estándar POSIX dice que debe haber un espacio entre ellos, que en el caso de "- longoption" terminaría el análisis de opciones y convertiría todas las longopciones en argumentos sin opciones.

Arvid Requate
fuente
2
Una pregunta: ¿cuál es la semántica de !in val="${!OPTIND}?
TomRoche
2
@TomRoche es una sustitución indirecta: unix.stackexchange.com/a/41293/84316
ecbrodie
2
@ecbrodie: Esto se debe a que se han procesado dos argumentos, en lugar de uno solo. El primer argumento es la palabra "loglevel", y el siguiente es el argumento de ese argumento. Mientras tanto, getoptsautomáticamente solo se incrementa OPTINDcon 1, pero en nuestro caso necesitamos que se incremente en 2, por lo que lo incrementamos en 1 manualmente, luego dejamos getoptsaumentarlo en 1 nuevamente para nosotros automáticamente.
Victor Zamanian
3
Ya que estamos en equilibrio bash aquí: los nombres de variables desnudos están permitidos dentro de las expresiones aritméticas, no es $necesario. OPTIND=$(( $OPTIND + 1 ))puede ser justo OPTIND=$(( OPTIND + 1 )). Aún más interesante, incluso puede asignar y aumentar variables dentro de una expresión aritmética, por lo que es posible abreviarla aún más : $(( ++OPTIND )), o incluso (( ++OPTIND ))tener en cuenta que ++OPTINDsiempre será positiva, por lo que no se disparará una ejecución de shell con la -eopción. :-) gnu.org/software/bash/manual/html_node/Shell-Arithmetic.html
clacke
3
¿Por qué no --very-badda una advertencia?
Tom Hale
148

El getoptscomando incorporado sigue siendo, AFAIK, limitado a las opciones de un solo carácter.

Hay (o solía haber) un programa externo getoptque reorganizaría un conjunto de opciones para que fuera más fácil de analizar. También puede adaptar ese diseño para manejar opciones largas. Ejemplo de uso:

aflag=no
bflag=no
flist=""
set -- $(getopt abf: "$@")
while [ $# -gt 0 ]
do
    case "$1" in
    (-a) aflag=yes;;
    (-b) bflag=yes;;
    (-f) flist="$flist $2"; shift;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*)  break;;
    esac
    shift
done

# Process remaining non-option arguments
...

Podría usar un esquema similar con un getoptlongcomando.

Tenga en cuenta que la debilidad fundamental con el getoptprograma externo es la dificultad de manejar argumentos con espacios en ellos y de preservar esos espacios con precisión. Esta es la razón por la cual el incorporado getoptses superior, aunque limitado por el hecho de que solo maneja opciones de una letra.

Jonathan Leffler
fuente
11
getopt, a excepción de la versión GNU (que tiene una convención de llamada diferente), está fundamentalmente roto. No lo uses. Utilice ** getopts en su lugar bash-hackers.org/wiki/doku.php/howto/getopts_tutorial
hendry
99
@hendry: desde su propio enlace: "Tenga en cuenta que getopts no puede analizar las opciones largas de estilo GNU (--myoption) ni las opciones largas de estilo XF86 (-myoption)".
Tom Auger
1
Jonathan: debes reescribir el ejemplo para usarlo eval setcon comillas (ver mi respuesta a continuación) para que también funcione correctamente con GNU getopt (el valor predeterminado en Linux) y maneje los espacios correctamente.
Urban Vagabond
@UrbanVagabond: No estoy seguro de por qué debería hacer eso. La pregunta está etiquetada con Unix, no con Linux. Estoy mostrando el mecanismo tradicional, deliberadamente, y tiene problemas con espacios en blanco en los argumentos, etc. Puede demostrar la versión moderna específica de Linux si lo desea, y su respuesta lo hace. (Noto, passim, que su uso de ${1+"$@"}es pintoresco y está en desacuerdo con lo que es necesario en los shells modernos y específicamente con cualquier shell que encuentre en Linux. Consulte Uso de $ 1: + "$ @"} en / bin / sh para obtener un discusión de esa notación.)
Jonathan Leffler
Debería hacerlo porque eval sethace lo correcto tanto con GNU como con BSD getopt, mientras que simplemente setsolo hace lo correcto con BSD getopt. Por lo tanto, puede utilizar eval setpara alentar a las personas a adquirir el hábito de hacer esto. Por cierto, gracias, no me di cuenta de que ${1+"$@"}ya no era necesario. Tengo que escribir cosas que funcionen tanto en Mac OS X como en Linux: entre los dos fuerzan mucha portabilidad. Acabo de comprobar y "$@"hace de hecho lo correcto en todos sh, bash, ksh, y zshen Mac OS X; seguramente bajo Linux también.
Urban Vagabond
78

Aquí hay un ejemplo que realmente usa getopt con opciones largas:

aflag=no
bflag=no
cargument=none

# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc: -l along,blong,clong: -- "$@")
then
    # something went wrong, getopt will put out an error message for us
    exit 1
fi

set -- $options

while [ $# -gt 0 ]
do
    case $1 in
    -a|--along) aflag="yes" ;;
    -b|--blong) bflag="yes" ;;
    # for options with required arguments, an additional shift is required
    -c|--clong) cargument="$2" ; shift;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*) break;;
    esac
    shift
done
sme
fuente
1
Debería reescribir el ejemplo para usarlo eval setcon comillas (vea mi respuesta a continuación) para que también funcione correctamente con GNU getopt (el valor predeterminado en Linux) y maneje los espacios correctamente.
Urban Vagabond
2
Sin embargo, esto se usa getoptmientras se trata la pregunta getopts.
Niklas Berglund
1
Son (--, (-*y (*patrones válidos? ¿En qué son diferentes de --, -*y *?
Maëlan
1
@ Maëlan: el paréntesis abierto principal es opcional, por lo que (--)es idéntico al --)de una caseestrofa. Es extraño ver la sangría desigual y el uso inconsistente de esos padres principales opcionales, pero el código actual de la respuesta me parece válido.
Adam Katz
59

Las opciones largas pueden ser analizadas por el estándar getoptsincorporado como "argumentos" a la -"opción"

Este es un shell POSIX portátil y nativo: no se necesitan programas externos ni bashismos.

Esta guía implementa opciones largas como argumentos de la -opción, por lo que --alphase ve getoptscomo -con argumento alphay --bravo=foose ve como -con argumento bravo=foo. El verdadero argumento puede ser cosechada con una simple sustitución: ${OPTARG#*=}.

En este ejemplo, -by -c(y sus formas largas, --bravoy --charlie) tienen argumentos obligatorios. Los argumentos sobre las opciones largas aparecen después de signos iguales, por ejemplo --bravo=foo(los delimitadores de espacio para opciones largas serían difíciles de implementar, ver más abajo).

Debido a que este utiliza el getoptsincorporado , este uso soportes como solución cmd --bravo=foo -ac FILE(que ha combinado opciones -ay -ce intercala largas opciones con opciones estándar), mientras que la mayoría de las otras respuestas aquí tampoco lucha o dejar de hacer eso.

die() { echo "$*" >&2; exit 2; }  # complain to STDERR and exit with error
needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }

while getopts ab:c:-: OPT; do
  # support long options: https://stackoverflow.com/a/28466267/519360
  if [ "$OPT" = "-" ]; then   # long option: reformulate OPT and OPTARG
    OPT="${OPTARG%%=*}"       # extract long option name
    OPTARG="${OPTARG#$OPT}"   # extract long option argument (may be empty)
    OPTARG="${OPTARG#=}"      # if long option argument, remove assigning `=`
  fi
  case "$OPT" in
    a | alpha )    alpha=true ;;
    b | bravo )    needs_arg; bravo="$OPTARG" ;;
    c | charlie )  needs_arg; charlie="$OPTARG" ;;
    ??* )          die "Illegal option --$OPT" ;;  # bad long option
    \? )           exit 2 ;;  # bad short option (error reported via getopts)
  esac
done
shift $((OPTIND-1)) # remove parsed options and args from $@ list

Cuando la opción es un guión ( -), es una opción larga. getoptshabrá analizado la opción larga real en $OPTARG, por ejemplo, --bravo=fooestablece originalmente OPT='-'y OPTARG='bravo=foo'. La ifestrofa se ajusta $OPTal contenido de $OPTARGantes del primer signo igual ( bravoen nuestro ejemplo) y luego elimina eso desde el principio de $OPTARG(ceder =fooen este paso, o una cadena vacía si no hay =). Finalmente, despojamos el argumento principal =. En este punto, $OPTes una opción corta (un carácter) o una opción larga (más de 2 caracteres).

La casecontinuación partidos ya sea a corto o largo opciones. Para las opciones cortas, getoptsse queja automáticamente sobre las opciones y los argumentos faltantes, por lo que tenemos que replicarlos manualmente usando la needs_argfunción, que se cierra fatalmente cuando $OPTARGestá vacía. La ??*condición coincidirá con cualquier opción larga restante ( ?coincide con un solo carácter y *coincide con cero o más, por lo que ??*coincide con más de 2 caracteres), lo que nos permite emitir el error "Opción ilegal" antes de salir.

(Una nota sobre nombres de variables en mayúsculas: en general, el consejo es reservar las variables en mayúsculas para el uso del sistema. Me mantengo $OPTcomo todo en mayúsculas para mantenerlo en línea $OPTARG, pero esto rompe esa convención. Creo que es encaja porque esto es algo que el sistema debería haber hecho, y debería ser seguro porque no hay estándares (afaik) que usen dicha variable).


Para quejarse de argumentos inesperados a opciones largas, imite lo que hicimos para argumentos obligatorios: use una función auxiliar. Simplemente voltee la prueba para quejarse de un argumento cuando no se espera uno:

no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }

Una versión anterior de esta respuesta tenía un intento de aceptar opciones largas con argumentos delimitados por espacios, pero no era confiable; getoptspodría terminar prematuramente asumiendo que el argumento estaba más allá de su alcance y que el incremento manual $OPTINDno funciona en todos los shells.

Esto se lograría utilizando una de estas técnicas:

y luego concluí con algo como [ $# -gt $OPTIND ] && OPTIND=$((OPTIND+1))

Adam Katz
fuente
Muy buena solución autónoma. Una pregunta: dado que letter-cno necesita argumentos, ¿no sería suficiente usarlo letter-c)? El *parece redundante.
Philip Kearns
1
@Arne Los argumentos posicionales son malos UX; son difíciles de entender y los argumentos opcionales son bastante desordenados. getoptsse detiene en el primer argumento posicional ya que no está diseñado para tratar con ellos. Esto permite subcomandos con sus propios argumentos, por ejemplo git diff --color, por lo que interpretaría command --foo=moo bar --baz wazque tienen --foocomo argumento commandy --baz wazcomo argumento (con opción) al barsubcomando. Esto se puede hacer con el código anterior. Rechazo --bravo -blahporque --bravorequiere un argumento y no está claro que esa -blahno sea otra opción.
Adam Katz
1
No estoy de acuerdo con el UX: los argumentos posicionales son útiles y fáciles, siempre y cuando limites su número (a lo sumo 2 o 1 más N del mismo tipo). Debería ser posible intercalarlos con argumentos de palabras clave, porque los usuarios pueden construir un comando paso a paso (es decir, ls abc -la).
Arne Babenhauserheide
1
@AdamKatz: Escribí un pequeño artículo con esto: draketo.de/english/free-software/shell-argument-parsing : incluye la lectura repetida de los argumentos restantes para captar las opciones finales.
Arne Babenhauserheide
1
@ArneBabenhauserheide: He actualizado esta respuesta para admitir argumentos delimitados por espacios. Debido a que requiere evalen el shell POSIX, aparece debajo del resto de la respuesta.
Adam Katz
33

Eche un vistazo a shFlags, que es una biblioteca de shell portátil (es decir: sh, bash, dash, ksh, zsh en Linux, Solaris, etc.).

Hace que agregar nuevas marcas sea tan simple como agregar una línea a su script, y proporciona una función de uso generada automáticamente.

Aquí hay un simple Hello, world!uso de shFlag :

#!/bin/sh

# source shflags from current directory
. ./shflags

# define a 'name' command-line string flag
DEFINE_string 'name' 'world' 'name to say hello to' 'n'

# parse the command-line
FLAGS "$@" || exit 1
eval set -- "${FLAGS_ARGV}"

# say hello
echo "Hello, ${FLAGS_name}!"

Para los sistemas operativos que tienen el getopt mejorado que admite opciones largas (por ejemplo, Linux), puede hacer lo siguiente:

$ ./hello_world.sh --name Kate
Hello, Kate!

Por lo demás, debe usar la opción corta:

$ ./hello_world.sh -n Kate
Hello, Kate!

Agregar una nueva bandera es tan simple como agregar una nueva DEFINE_ call.

k0pernikus
fuente
2
Esto es fantástico, pero desafortunadamente mi getopt (OS X) no admite espacios en argumentos: / me pregunto si hay una alternativa.
Alastair Stuart
@AlastairStuart: de hecho, existe una alternativa en OS X. Utilice MacPorts para instalar GNU getopt (generalmente se instalará en / opt / local / bin / getopt).
Urban Vagabond
3
@UrbanVagabond: desafortunadamente, la instalación de herramientas no predeterminadas del sistema no es un requisito aceptable para una herramienta suficientemente portátil.
Alastair Stuart
@AlastairStuart: vea mi respuesta para una solución portátil que utiliza getopts incorporado en lugar de GNU getopt. Es lo mismo que el uso básico de getopts, pero con una iteración adicional para opciones largas.
Adam Katz
31

Usar getoptscon opciones cortas / largas y argumentos


Funciona con todas las combinaciones, por ejemplo:

  • foobar -f --bar
  • foobar --foo -b
  • foobar -bf --bar --foobar
  • foobar -fbFBAshorty --bar -FB --arguments = longhorn
  • foobar -fA "texto shorty" -B --arguments = "texto longhorn"
  • bash foobar -F --barfoo
  • sh foobar -B --foobar - ...
  • bash ./foobar -F --bar

Algunas declaraciones para este ejemplo

Options=$@
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty

Cómo se vería la función de uso

function _usage() 
{
  ###### U S A G E : Help and ERROR ######
  cat <<EOF
   foobar $Options
  $*
          Usage: foobar <[options]>
          Options:
                  -b   --bar            Set bar to yes    ($foo)
                  -f   --foo            Set foo to yes    ($bart)
                  -h   --help           Show this message
                  -A   --arguments=...  Set arguments to yes ($arguments) AND get ARGUMENT ($ARG)
                  -B   --barfoo         Set barfoo to yes ($barfoo)
                  -F   --foobar         Set foobar to yes ($foobar)
EOF
}

[ $# = 0 ] && _usage "  >>>>>>>> no options given "

getops con banderas largas / cortas, así como argumentos largos

while getopts ':bfh-A:BF' OPTION ; do
  case "$OPTION" in
    b  ) sbar=yes                       ;;
    f  ) sfoo=yes                       ;;
    h  ) _usage                         ;;   
    A  ) sarguments=yes;sARG="$OPTARG"  ;;
    B  ) sbarfoo=yes                    ;;
    F  ) sfoobar=yes                    ;;
    -  ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
         eval OPTION="\$$optind"
         OPTARG=$(echo $OPTION | cut -d'=' -f2)
         OPTION=$(echo $OPTION | cut -d'=' -f1)
         case $OPTION in
             --foo       ) lfoo=yes                       ;;
             --bar       ) lbar=yes                       ;;
             --foobar    ) lfoobar=yes                    ;;
             --barfoo    ) lbarfoo=yes                    ;;
             --help      ) _usage                         ;;
             --arguments ) larguments=yes;lARG="$OPTARG"  ;; 
             * )  _usage " Long: >>>>>>>> invalid options (long) " ;;
         esac
       OPTIND=1
       shift
      ;;
    ? )  _usage "Short: >>>>>>>> invalid options (short) "  ;;
  esac
done

Salida

##################################################################
echo "----------------------------------------------------------"
echo "RESULT short-foo      : $sfoo                                    long-foo      : $lfoo"
echo "RESULT short-bar      : $sbar                                    long-bar      : $lbar"
echo "RESULT short-foobar   : $sfoobar                                 long-foobar   : $lfoobar"
echo "RESULT short-barfoo   : $sbarfoo                                 long-barfoo   : $lbarfoo"
echo "RESULT short-arguments: $sarguments  with Argument = \"$sARG\"   long-arguments: $larguments and $lARG"

Combinando lo anterior en un guión coherente

#!/bin/bash
# foobar: getopts with short and long options AND arguments

function _cleanup ()
{
  unset -f _usage _cleanup ; return 0
}

## Clear out nested functions on exit
trap _cleanup INT EXIT RETURN

###### some declarations for this example ######
Options=$@
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty

function _usage() 
{
  ###### U S A G E : Help and ERROR ######
  cat <<EOF
   foobar $Options
   $*
          Usage: foobar <[options]>
          Options:
                  -b   --bar            Set bar to yes    ($foo)
                    -f   --foo            Set foo to yes    ($bart)
                      -h   --help           Show this message
                  -A   --arguments=...  Set arguments to yes ($arguments) AND get ARGUMENT ($ARG)
                  -B   --barfoo         Set barfoo to yes ($barfoo)
                  -F   --foobar         Set foobar to yes ($foobar)
EOF
}

[ $# = 0 ] && _usage "  >>>>>>>> no options given "

##################################################################    
#######  "getopts" with: short options  AND  long options  #######
#######            AND  short/long arguments               #######
while getopts ':bfh-A:BF' OPTION ; do
  case "$OPTION" in
    b  ) sbar=yes                       ;;
    f  ) sfoo=yes                       ;;
    h  ) _usage                         ;;   
    A  ) sarguments=yes;sARG="$OPTARG"  ;;
    B  ) sbarfoo=yes                    ;;
    F  ) sfoobar=yes                    ;;
    -  ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
         eval OPTION="\$$optind"
         OPTARG=$(echo $OPTION | cut -d'=' -f2)
         OPTION=$(echo $OPTION | cut -d'=' -f1)
         case $OPTION in
             --foo       ) lfoo=yes                       ;;
             --bar       ) lbar=yes                       ;;
             --foobar    ) lfoobar=yes                    ;;
             --barfoo    ) lbarfoo=yes                    ;;
             --help      ) _usage                         ;;
               --arguments ) larguments=yes;lARG="$OPTARG"  ;; 
             * )  _usage " Long: >>>>>>>> invalid options (long) " ;;
         esac
       OPTIND=1
       shift
      ;;
    ? )  _usage "Short: >>>>>>>> invalid options (short) "  ;;
  esac
done
Rapa Nui
fuente
¿Esto no funciona con más de un argumento largo (-). Parece que solo leí el primero para mí.
Sinaesthetic
@Sinaesthetic: sí, estaba jugando con el evalenfoque de argumentos espaciados en opciones largas y me pareció poco confiable con ciertos shells (aunque espero que funcione con bash, en cuyo caso no tiene que usarlo eval). Vea mi respuesta sobre cómo aceptar argumentos de opciones largas =y mis intentos notables de usar el espacio. Mi solución no hace llamadas externas, mientras que esta se usa cutvarias veces.
Adam Katz
24

De otra manera...

# translate long options to short
for arg
do
    delim=""
    case "$arg" in
       --help) args="${args}-h ";;
       --verbose) args="${args}-v ";;
       --config) args="${args}-c ";;
       # pass through anything else
       *) [[ "${arg:0:1}" == "-" ]] || delim="\""
           args="${args}${delim}${arg}${delim} ";;
    esac
done
# reset the translated args
eval set -- $args
# now we can process with getopt
while getopts ":hvc:" opt; do
    case $opt in
        h)  usage ;;
        v)  VERBOSE=true ;;
        c)  source $OPTARG ;;
        \?) usage ;;
        :)
        echo "option -$OPTARG requires an argument"
        usage
        ;;
    esac
done
mtvee
fuente
1
¿No necesita esto un espacio en cada $argsreasignación? Esto incluso podría hacerse sin bashisms, pero este código perderá espacios en opciones y argumentos (no creo que el $delimtruco funcione). En su lugar, puede ejecutar set dentro del forbucle si tiene cuidado de vaciarlo solo en la primera iteración. Aquí hay una versión más segura sin bashismos.
Adam Katz
18

Lo resolví de esta manera:

# A string with command options
options=$@

# An array with all the arguments
arguments=($options)

# Loop index
index=0

for argument in $options
  do
    # Incrementing index
    index=`expr $index + 1`

    # The conditions
    case $argument in
      -a) echo "key $argument value ${arguments[index]}" ;;
      -abc) echo "key $argument value ${arguments[index]}" ;;
    esac
  done

exit;

¿Estoy siendo tonto o algo así? getopty getoptsson muy confusos

Benjamín
fuente
1
Esto parece funcionar para mí, no sé cuál es el problema con este método, pero parece simple, por lo que debe haber una razón por la que todos los demás no lo están usando.
Billy Moon
1
@Billy Sí, esto es simple porque no uso ningún script para administrar mis parámetros, etc. Básicamente convierto la cadena de argumentos ($ @) en una matriz y la recorro. En el bucle, el valor actual será la clave y el siguiente será el valor. Simple como eso.
1
@Theodore ¡Me alegro de que esto te haya sido útil! También fue un dolor para mí. Si está interesado, puede ver un ejemplo en acción aquí: raw.github.com/rafaelrinaldi/swf-to-html/master/swf-to-html.sh
2
Definitivamente la forma más fácil que he visto. Lo cambié un poco, como usar i = $ (($ i + 1)) en lugar de expr, pero el concepto es hermético.
Thomas Dignan
66
No eres tonto en absoluto, pero es posible que te falte una característica: getopt (s) puede reconocer opciones que están mezcladas (por ejemplo, -ltro -lt -rtambién -l -t -r). Y también proporciona cierto manejo de errores, y una manera fácil de cambiar los parámetros tratados una vez que finaliza el tratamiento de opciones.
Olivier Dulac
14

En caso de que no desee la getoptdependencia, puede hacer esto:

while test $# -gt 0
do
  case $1 in

  # Normal option processing
    -h | --help)
      # usage and help
      ;;
    -v | --version)
      # version info
      ;;
  # ...

  # Special cases
    --)
      break
      ;;
    --*)
      # error unknown (long) option $1
      ;;
    -?)
      # error unknown (short) option $1
      ;;

  # FUN STUFF HERE:
  # Split apart combined short options
    -*)
      split=$1
      shift
      set -- $(echo "$split" | cut -c 2- | sed 's/./-& /g') "$@"
      continue
      ;;

  # Done with options
    *)
      break
      ;;
  esac

  # for testing purposes:
  echo "$1"

  shift
done

Por supuesto, entonces no puedes usar opciones de estilo largas con un guión. Y si desea agregar versiones abreviadas (por ejemplo, --verbos en lugar de --verbose), debe agregarlas manualmente.

Pero si está buscando obtener getoptsfuncionalidad junto con opciones largas, esta es una manera simple de hacerlo.

También puse este fragmento en una esencia .

jakesandlund
fuente
Esto parece funcionar solo con una opción larga a la vez, pero satisfizo mis necesidades. ¡Gracias!
kingjeffrey
En el caso especial --)parece haber una shift ;falta. Por el momento --, permanecerá como primer argumento sin opción.
dgw
Creo que esta es realmente la mejor respuesta, aunque como señala dgw, la --opción necesita una shiftallí. Yo digo que esto es mejor porque las alternativas son o bien plataforma de versiones dependientes de getopto getopts_longo tiene que forzar cortos opciones para ser utilizado sólo en el inicio de la orden (es decir, - se utiliza getoptsluego procesar las opciones largas después), mientras que esto da cualquier orden y control completo.
Haravikk
Esta respuesta me hace preguntarme por qué tenemos un hilo de docenas de respuestas para hacer el trabajo que se puede hacer con nada más que esta solución absolutamente clara y directa , y si hay alguna razón para los mil millones de casos de uso de getopt (s) además de probar uno mismo.
Florian Heigl
11

El incorporado getoptsno puede hacer esto. Hay un programa getopt (1) externo que puede hacer esto, pero solo lo obtienes en Linux desde el paquete util-linux . Viene con un script de ejemplo getopt-parse.bash .

También hay una función getopts_longescrita como shell.

Nietzche-jou
fuente
3
Se getoptincluyó en FreeBSD versión 1.0 en 1993, y ha sido parte de FreeBSD desde entonces. Como tal, fue adoptado de FreeBSD 4.x para su inclusión en el proyecto Darwin de Apple. A partir de OS X 10.6.8, la página de manual incluida por Apple sigue siendo un duplicado exacto de la página de manual de FreeBSD. Entonces sí, está incluido en OS X y en muchos otros sistemas operativos además de Linux. -1 en esta respuesta para la desinformación.
ghoti
8
#!/bin/bash
while getopts "abc:d:" flag
do
  case $flag in
    a) echo "[getopts:$OPTIND]==> -$flag";;
    b) echo "[getopts:$OPTIND]==> -$flag";;
    c) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
    d) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
  esac
done

shift $((OPTIND-1))
echo "[otheropts]==> $@"

exit

.

#!/bin/bash
until [ -z "$1" ]; do
  case $1 in
    "--dlong")
      shift
      if [ "${1:1:0}" != "-" ]
      then
        echo "==> dlong $1"
        shift
      fi;;
    *) echo "==> other $1"; shift;;
  esac
done
exit
3ED
fuente
2
Una explicación estaría bien. El primer script acepta opciones cortas solo mientras que el segundo script tiene un error en su análisis de argumento de opción larga; su variable debe ser "${1:0:1}"para el argumento # 1, subcadena en el índice 0, longitud 1. Esto no permite mezclar opciones cortas y largas.
Adam Katz
7

En ksh93, getoptsadmite nombres largos ...

while getopts "f(file):s(server):" flag
do
    echo "$flag" $OPTIND $OPTARG
done

O eso dicen los tutoriales que he encontrado. Pruébalo y verás.

Richard Lynch
fuente
44
Este es el getopts de ksh93 integrado. Además de esta sintaxis, también tiene una sintaxis más complicada que también permite opciones largas sin un equivalente corto, y más.
jilles
2
Una respuesta razonable. El OP no especificó QUÉ shell.
ghoti
6

Solo escribo scripts de shell de vez en cuando y caigo en la práctica, por lo que cualquier comentario es apreciado.

Usando la estrategia propuesta por @Arvid Requate, notamos algunos errores de los usuarios. Un usuario que olvida incluir un valor accidentalmente tendrá el nombre de la siguiente opción tratado como un valor:

./getopts_test.sh --loglevel= --toc=TRUE

hará que el valor de "loglevel" se vea como "--toc = TRUE". Esto se puede evitar.

Adapte algunas ideas sobre la comprobación de errores de usuario para CLI de http://mwiki.wooledge.org/BashFAQ/035 discusión del análisis manual. Inserté la comprobación de errores en el manejo de los argumentos "-" y "-".

Luego comencé a jugar con la sintaxis, por lo que cualquier error aquí es estrictamente mi culpa, no los autores originales.

Mi enfoque ayuda a los usuarios que prefieren ingresar mucho tiempo con o sin el signo igual. Es decir, debería tener la misma respuesta a "--loglevel 9" que "--loglevel = 9". En el método - / space, no es posible saber con certeza si el usuario olvida un argumento, por lo que es necesario adivinar.

  1. si el usuario tiene el formato de signo largo / igual (--opt =), un espacio después de = desencadena un error porque no se proporcionó un argumento.
  2. si el usuario tiene argumentos largos / espaciados (--opt), este script produce un error si no sigue ningún argumento (final del comando) o si el argumento comienza con un guión

En caso de que esté comenzando con esto, hay una diferencia interesante entre los formatos "--opt = value" y "--opt value". Con el signo igual, el argumento de la línea de comando se ve como "opt = value" y el trabajo a manejar que es el análisis de cadenas, para separar en el "=". En contraste, con "--opt value", el nombre del argumento es "opt" y tenemos el desafío de obtener el siguiente valor proporcionado en la línea de comando. Ahí es donde @Arvid Requate utilizó $ {! OPTIND}, la referencia indirecta. Todavía no entiendo eso, bueno, en absoluto, y los comentarios en BashFAQ parecen advertir contra ese estilo ( http://mywiki.wooledge.org/BashFAQ/006 ). Por cierto, no creo que los comentarios del póster anterior sobre la importancia de OPTIND = $ (($ OPTIND + 1)) sean correctos. Quiero decir,

En la versión más reciente de este script, flag -v significa impresión VERBOSA.

Guárdelo en un archivo llamado "cli-5.sh", haga ejecutable y cualquiera de estos funcionará o fallará de la manera deseada

./cli-5.sh  -v --loglevel=44 --toc  TRUE
./cli-5.sh  -v --loglevel=44 --toc=TRUE
./cli-5.sh --loglevel 7
./cli-5.sh --loglevel=8
./cli-5.sh -l9

./cli-5.sh  --toc FALSE --loglevel=77
./cli-5.sh  --toc=FALSE --loglevel=77

./cli-5.sh   -l99 -t yyy
./cli-5.sh   -l 99 -t yyy

Aquí hay un ejemplo de salida de la comprobación de errores en la intpu del usuario

$ ./cli-5.sh  --toc --loglevel=77
ERROR: toc value must not have dash at beginning
$ ./cli-5.sh  --toc= --loglevel=77
ERROR: value for toc undefined

Debería considerar activar -v, porque imprime los elementos internos de OPTIND y OPTARG

#/usr/bin/env bash

## Paul Johnson
## 20171016
##

## Combines ideas from
## /programming/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
## by @Arvid Requate, and http://mwiki.wooledge.org/BashFAQ/035

# What I don't understand yet: 
# In @Arvid REquate's answer, we have 
# val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
# this works, but I don't understand it!


die() {
    printf '%s\n' "$1" >&2
    exit 1
}

printparse(){
    if [ ${VERBOSE} -gt 0 ]; then
        printf 'Parse: %s%s%s\n' "$1" "$2" "$3" >&2;
    fi
}

showme(){
    if [ ${VERBOSE} -gt 0 ]; then
        printf 'VERBOSE: %s\n' "$1" >&2;
    fi
}


VERBOSE=0
loglevel=0
toc="TRUE"

optspec=":vhl:t:-:"
while getopts "$optspec" OPTCHAR; do

    showme "OPTARG:  ${OPTARG[*]}"
    showme "OPTIND:  ${OPTIND[*]}"
    case "${OPTCHAR}" in
        -)
            case "${OPTARG}" in
                loglevel) #argument has no equal sign
                    opt=${OPTARG}
                    val="${!OPTIND}"
                    ## check value. If negative, assume user forgot value
                    showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
                    if [[ "$val" == -* ]]; then
                        die "ERROR: $opt value must not have dash at beginning"
                    fi
                    ## OPTIND=$(( $OPTIND + 1 )) # CAUTION! no effect?
                    printparse "--${OPTARG}" "  " "${val}"
                    loglevel="${val}"
                    shift
                    ;;
                loglevel=*) #argument has equal sign
                    opt=${OPTARG%=*}
                    val=${OPTARG#*=}
                    if [ "${OPTARG#*=}" ]; then
                        printparse "--${opt}" "=" "${val}"
                        loglevel="${val}"
                        ## shift CAUTION don't shift this, fails othewise
                    else
                        die "ERROR: $opt value must be supplied"
                    fi
                    ;;
                toc) #argument has no equal sign
                    opt=${OPTARG}
                    val="${!OPTIND}"
                    ## check value. If negative, assume user forgot value
                    showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
                    if [[ "$val" == -* ]]; then
                        die "ERROR: $opt value must not have dash at beginning"
                    fi
                    ## OPTIND=$(( $OPTIND + 1 )) #??
                    printparse "--${opt}" " " "${val}"
                    toc="${val}"
                    shift
                    ;;
                toc=*) #argument has equal sign
                    opt=${OPTARG%=*}
                    val=${OPTARG#*=}
                    if [ "${OPTARG#*=}" ]; then
                        toc=${val}
                        printparse "--$opt" " -> " "$toc"
                        ##shift ## NO! dont shift this
                    else
                        die "ERROR: value for $opt undefined"
                    fi
                    ;;

                help)
                    echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
                    exit 2
                    ;;
                *)
                    if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                        echo "Unknown option --${OPTARG}" >&2
                    fi
                    ;;
            esac;;
        h|-\?|--help)
            ## must rewrite this for all of the arguments
            echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
            exit 2
            ;;
        l)
            loglevel=${OPTARG}
            printparse "-l" " "  "${loglevel}"
            ;;
        t)
            toc=${OPTARG}
            ;;
        v)
            VERBOSE=1
            ;;

        *)
            if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                echo "Non-option argument: '-${OPTARG}'" >&2
            fi
            ;;
    esac
done



echo "
After Parsing values
"
echo "loglevel  $loglevel" 
echo "toc  $toc"
pauljohn32
fuente
OPTIND=$(( $OPTIND + 1 )): es necesario siempre que 'engulle' el parámetro OPTIND (por ejemplo: cuando uno usó --toc value : el valor está en el número de parámetro $ OPTIND. Una vez que lo recupere para el valor de toc, debe decirle a getopts que el siguiente parámetro para analizar no es el valor, pero OPTIND=$(( $OPTIND + 1 )) falta el siguiente (de ahí el:. y su secuencia de comandos (así como la secuencia de comandos a la que se refiere), después de que se hace: shift $(( $OPTIND -1 ))(como los getopts salieron después de analizar los parámetros 1 a OPTIND-1, debe cambiarlos así que $@es ahora cualquier parámetro "no opciones" restante
Olivier Dulac
oh, a medida que te desplazas, "desplazas" los parámetros debajo de getopts, por lo que OPTIND siempre apunta lo correcto ... pero me resulta muy confuso. Creo (no puedo probar su script en este momento) que todavía necesita el cambio $ (($ OPTIND - 1)) después del bucle while de getopts, por lo que $ 1 ahora no apunta al $ 1 original (una opción) pero al primero de los argumentos restantes (los que vienen después de todas las opciones y sus valores). ej .: myrm -foo -bar = baz thisarg thenthisone thenotother
Olivier Dulac
5

Inventando otra versión de la rueda ...

Esta función es (con suerte) un reemplazo de shell de bourne simple compatible con POSIX para GNU getopt. Admite opciones cortas / largas que pueden aceptar argumentos obligatorios / opcionales / sin argumentos, y la forma en que se especifican las opciones es casi idéntica a la getopt de GNU, por lo que la conversión es trivial.

Por supuesto, esto sigue siendo una porción considerable de código para colocar en un script, pero es aproximadamente la mitad de las líneas de la conocida función de shell getopt_long, y podría ser preferible en los casos en que solo desee reemplazar los usos existentes de GNU getopt.

Este es un código bastante nuevo, por lo que YMMV (y definitivamente avíseme si esto no es realmente compatible con POSIX por alguna razón; la portabilidad fue la intención desde el principio, pero no tengo un entorno de prueba POSIX útil).

El código y el ejemplo de uso son los siguientes:

#!/bin/sh
# posix_getopt shell function
# Author: Phil S.
# Version: 1.0
# Created: 2016-07-05
# URL: http://stackoverflow.com/a/37087374/324105

# POSIX-compatible argument quoting and parameter save/restore
# http://www.etalabs.net/sh_tricks.html
# Usage:
# parameters=$(save "$@") # save the original parameters.
# eval "set -- ${parameters}" # restore the saved parameters.
save () {
    local param
    for param; do
        printf %s\\n "$param" \
            | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"
    done
    printf %s\\n " "
}

# Exit with status $1 after displaying error message $2.
exiterr () {
    printf %s\\n "$2" >&2
    exit $1
}

# POSIX-compatible command line option parsing.
# This function supports long options and optional arguments, and is
# a (largely-compatible) drop-in replacement for GNU getopt.
#
# Instead of:
# opts=$(getopt -o "$shortopts" -l "$longopts" -- "$@")
# eval set -- ${opts}
#
# We instead use:
# opts=$(posix_getopt "$shortopts" "$longopts" "$@")
# eval "set -- ${opts}"
posix_getopt () { # args: "$shortopts" "$longopts" "$@"
    local shortopts longopts \
          arg argtype getopt nonopt opt optchar optword suffix

    shortopts="$1"
    longopts="$2"
    shift 2

    getopt=
    nonopt=
    while [ $# -gt 0 ]; do
        opt=
        arg=
        argtype=
        case "$1" in
            # '--' means don't parse the remaining options
            ( -- ) {
                getopt="${getopt}$(save "$@")"
                shift $#
                break
            };;
            # process short option
            ( -[!-]* ) {         # -x[foo]
                suffix=${1#-?}   # foo
                opt=${1%$suffix} # -x
                optchar=${opt#-} # x
                case "${shortopts}" in
                    ( *${optchar}::* ) { # optional argument
                        argtype=optional
                        arg="${suffix}"
                        shift
                    };;
                    ( *${optchar}:* ) { # required argument
                        argtype=required
                        if [ -n "${suffix}" ]; then
                            arg="${suffix}"
                            shift
                        else
                            case "$2" in
                                ( -* ) exiterr 1 "$1 requires an argument";;
                                ( ?* ) arg="$2"; shift 2;;
                                (  * ) exiterr 1 "$1 requires an argument";;
                            esac
                        fi
                    };;
                    ( *${optchar}* ) { # no argument
                        argtype=none
                        arg=
                        shift
                        # Handle multiple no-argument parameters combined as
                        # -xyz instead of -x -y -z. If we have just shifted
                        # parameter -xyz, we now replace it with -yz (which
                        # will be processed in the next iteration).
                        if [ -n "${suffix}" ]; then
                            eval "set -- $(save "-${suffix}")$(save "$@")"
                        fi
                    };;
                    ( * ) exiterr 1 "Unknown option $1";;
                esac
            };;
            # process long option
            ( --?* ) {            # --xarg[=foo]
                suffix=${1#*=}    # foo (unless there was no =)
                if [ "${suffix}" = "$1" ]; then
                    suffix=
                fi
                opt=${1%=$suffix} # --xarg
                optword=${opt#--} # xarg
                case ",${longopts}," in
                    ( *,${optword}::,* ) { # optional argument
                        argtype=optional
                        arg="${suffix}"
                        shift
                    };;
                    ( *,${optword}:,* ) { # required argument
                        argtype=required
                        if [ -n "${suffix}" ]; then
                            arg="${suffix}"
                            shift
                        else
                            case "$2" in
                                ( -* ) exiterr 1 \
                                       "--${optword} requires an argument";;
                                ( ?* ) arg="$2"; shift 2;;
                                (  * ) exiterr 1 \
                                       "--${optword} requires an argument";;
                            esac
                        fi
                    };;
                    ( *,${optword},* ) { # no argument
                        if [ -n "${suffix}" ]; then
                            exiterr 1 "--${optword} does not take an argument"
                        fi
                        argtype=none
                        arg=
                        shift
                    };;
                    ( * ) exiterr 1 "Unknown option $1";;
                esac
            };;
            # any other parameters starting with -
            ( -* ) exiterr 1 "Unknown option $1";;
            # remember non-option parameters
            ( * ) nonopt="${nonopt}$(save "$1")"; shift;;
        esac

        if [ -n "${opt}" ]; then
            getopt="${getopt}$(save "$opt")"
            case "${argtype}" in
                ( optional|required ) {
                    getopt="${getopt}$(save "$arg")"
                };;
            esac
        fi
    done

    # Generate function output, suitable for:
    # eval "set -- $(posix_getopt ...)"
    printf %s "${getopt}"
    if [ -n "${nonopt}" ]; then
        printf %s "$(save "--")${nonopt}"
    fi
}

Ejemplo de uso:

# Process command line options
shortopts="hvd:c::s::L:D"
longopts="help,version,directory:,client::,server::,load:,delete"
#opts=$(getopt -o "$shortopts" -l "$longopts" -n "$(basename $0)" -- "$@")
opts=$(posix_getopt "$shortopts" "$longopts" "$@")
if [ $? -eq 0 ]; then
    #eval set -- ${opts}
    eval "set -- ${opts}"
    while [ $# -gt 0 ]; do
        case "$1" in
            ( --                ) shift; break;;
            ( -h|--help         ) help=1; shift; break;;
            ( -v|--version      ) version_help=1; shift; break;;
            ( -d|--directory    ) dir=$2; shift 2;;
            ( -c|--client       ) useclient=1; client=$2; shift 2;;
            ( -s|--server       ) startserver=1; server_name=$2; shift 2;;
            ( -L|--load         ) load=$2; shift 2;;
            ( -D|--delete       ) delete=1; shift;;
        esac
    done
else
    shorthelp=1 # getopt returned (and reported) an error.
fi
phils
fuente
4

La respuesta aceptada hace un muy buen trabajo al señalar todas las deficiencias de bash incorporado getopts. La respuesta termina con:

Por lo tanto, si bien es posible escribir más código para evitar la falta de soporte para opciones largas, esto es mucho más trabajo y anula parcialmente el propósito de usar un analizador getopt para simplificar su código.

Y aunque en principio estoy de acuerdo con esa afirmación, creo que la cantidad de veces que todos implementamos esta característica en varios scripts justifica poner un poco de esfuerzo en crear una solución "estandarizada" y bien probada.

Como tal, he "actualizado" bash integrado getoptsmediante la implementación getopts_longen bash puro, sin dependencias externas. El uso de la función es 100% compatible con el incorporado getopts.

Al incluir getopts_long(que está alojado en GitHub ) en un script, la respuesta a la pregunta original se puede implementar de la siguiente manera:

source "${PATH_TO}/getopts_long.bash"

while getopts_long ':c: copyfile:' OPTKEY; do
    case ${OPTKEY} in
        'c'|'copyfile')
            echo 'file supplied -- ${OPTARG}'
            ;;
        '?')
            echo "INVALID OPTION -- ${OPTARG}" >&2
            exit 1
            ;;
        ':')
            echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2
            exit 1
            ;;
        *)
            echo "Misconfigured OPTSPEC or uncaught option -- ${OPTKEY}" >&2
            exit 1
            ;;
    esac
done

shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift
UrsaDK
fuente
3

Todavía no tengo suficiente representante para comentar o votar su solución, pero la respuesta de sme funcionó extremadamente bien para mí. El único problema con el que me topé fue que los argumentos terminan envueltos en comillas simples (así que tengo una tira de ellos).

También agregué algunos usos de ejemplo y texto de AYUDA. Incluiré mi versión ligeramente extendida aquí:

#!/bin/bash

# getopt example
# from: /programming/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
HELP_TEXT=\
"   USAGE:\n
    Accepts - and -- flags, can specify options that require a value, and can be in any order. A double-hyphen (--) will stop processing options.\n\n

    Accepts the following forms:\n\n

    getopt-example.sh -a -b -c value-for-c some-arg\n
    getopt-example.sh -c value-for-c -a -b some-arg\n
    getopt-example.sh -abc some-arg\n
    getopt-example.sh --along --blong --clong value-for-c -a -b -c some-arg\n
    getopt-example.sh some-arg --clong value-for-c\n
    getopt-example.sh
"

aflag=false
bflag=false
cargument=""

# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc:h\? -l along,blong,help,clong: -- "$@")
then
    # something went wrong, getopt will put out an error message for us
    exit 1
fi

set -- $options

while [ $# -gt 0 ]
do
    case $1 in
    -a|--along) aflag=true ;;
    -b|--blong) bflag=true ;;
    # for options with required arguments, an additional shift is required
    -c|--clong) cargument="$2" ; shift;;
    -h|--help|-\?) echo -e $HELP_TEXT; exit;;
    (--) shift; break;;
    (-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
    (*) break;;
    esac
    shift
done

# to remove the single quotes around arguments, pipe the output into:
# | sed -e "s/^'\\|'$//g"  (just leading/trailing) or | sed -e "s/'//g"  (all)

echo aflag=${aflag}
echo bflag=${bflag}
echo cargument=${cargument}

while [ $# -gt 0 ]
do
    echo arg=$1
    shift

    if [[ $aflag == true ]]; then
        echo a is true
    fi

done
kghastie
fuente
3

Aquí puede encontrar algunos enfoques diferentes para el análisis de opciones complejas en bash: http://mywiki.wooledge.org/ComplexOptionParsing

Creé el siguiente, y creo que es bueno, porque es un código mínimo y funcionan tanto las opciones largas como las cortas. Una opción larga también puede tener múltiples argumentos con este enfoque.

#!/bin/bash
# Uses bash extensions.  Not portable as written.

declare -A longoptspec
longoptspec=( [loglevel]=1 ) #use associative array to declare how many arguments a long option expects, in this case we declare that loglevel expects/has one argument, long options that aren't listed i n this way will have zero arguments by default
optspec=":h-:"
while getopts "$optspec" opt; do
while true; do
    case "${opt}" in
        -) #OPTARG is name-of-long-option or name-of-long-option=value
            if [[ "${OPTARG}" =~ .*=.* ]] #with this --key=value format only one argument is possible
            then
                opt=${OPTARG/=*/}
                OPTARG=${OPTARG#*=}
                ((OPTIND--))    
            else #with this --key value1 value2 format multiple arguments are possible
                opt="$OPTARG"
                OPTARG=(${@:OPTIND:$((longoptspec[$opt]))})
            fi
            ((OPTIND+=longoptspec[$opt]))
            continue #now that opt/OPTARG are set we can process them as if getopts would've given us long options
            ;;
        loglevel)
          loglevel=$OPTARG
            ;;
        h|help)
            echo "usage: $0 [--loglevel[=]<value>]" >&2
            exit 2
            ;;
    esac
break; done
done

# End of file
usuario3573558
fuente
2

He estado trabajando en ese tema durante bastante tiempo ... e hice mi propia biblioteca que necesitarás encontrar en tu script principal. Vea libopt4shell y cd2mpc para un ejemplo. Espero eso ayude !

liealgebra
fuente
2

Una solución mejorada:

# translate long options to short
# Note: This enable long options but disable "--?*" in $OPTARG, or disable long options after  "--" in option fields.
for ((i=1;$#;i++)) ; do
    case "$1" in
        --)
            # [ ${args[$((i-1))]} == ... ] || EndOpt=1 ;;& # DIRTY: we still can handle some execptions...
            EndOpt=1 ;;&
        --version) ((EndOpt)) && args[$i]="$1"  || args[$i]="-V";;
        # default case : short option use the first char of the long option:
        --?*) ((EndOpt)) && args[$i]="$1"  || args[$i]="-${1:2:1}";;
        # pass through anything else:
        *) args[$i]="$1" ;;
    esac
    shift
done
# reset the translated args
set -- "${args[@]}"

function usage {
echo "Usage: $0 [options] files" >&2
    exit $1
}

# now we can process with getopt
while getopts ":hvVc:" opt; do
    case $opt in
        h)  usage ;;
        v)  VERBOSE=true ;;
        V)  echo $Version ; exit ;;
        c)  source $OPTARG ;;
        \?) echo "unrecognized option: -$opt" ; usage -1 ;;
        :)
        echo "option -$OPTARG requires an argument"
        usage -1
        ;;
    esac
done

shift $((OPTIND-1))
[[ "$1" == "--" ]] && shift
jbar
fuente
2

Quizás sea más simple usar ksh, solo para la parte de getopts, si necesita opciones de línea de comando largas, ya que puede ser más fácil hacerlo allí.

# Working Getopts Long => KSH

#! /bin/ksh
# Getopts Long
USAGE="s(showconfig)"
USAGE+="c:(createdb)"
USAGE+="l:(createlistener)"
USAGE+="g:(generatescripts)"
USAGE+="r:(removedb)"
USAGE+="x:(removelistener)"
USAGE+="t:(createtemplate)"
USAGE+="h(help)"

while getopts "$USAGE" optchar ; do
    case $optchar in
    s)  echo "Displaying Configuration" ;;
        c)  echo "Creating Database $OPTARG" ;;
    l)  echo "Creating Listener LISTENER_$OPTARG" ;;
    g)  echo "Generating Scripts for Database $OPTARG" ;;
    r)  echo "Removing Database $OPTARG" ;;
    x)  echo "Removing Listener LISTENER_$OPTARG" ;;
    t)  echo "Creating Database Template" ;;
    h)  echo "Help" ;;
    esac
done
efstathiou_e
fuente
+1 - Tenga en cuenta que esto se limita a ksh93 - del proyecto AST de código abierto (AT&T Research).
Henk Langeveld
2

Quería algo sin dependencias externas, con estricto soporte de bash (-u), y lo necesitaba para funcionar incluso en las versiones de bash más antiguas. Esto maneja varios tipos de parámetros:

  • bools cortos (-h)
  • opciones cortas (-i "image.jpg")
  • bools largos (--help)
  • equivale a opciones (--file = "filename.ext")
  • opciones de espacio (--file "filename.ext")
  • bools concatinados (-hvm)

Simplemente inserte lo siguiente en la parte superior de su script:

# Check if a list of params contains a specific param
# usage: if _param_variant "h|?|help p|path f|file long-thing t|test-thing" "file" ; then ...
# the global variable $key is updated to the long notation (last entry in the pipe delineated list, if applicable)
_param_variant() {
  for param in $1 ; do
    local variants=${param//\|/ }
    for variant in $variants ; do
      if [[ "$variant" = "$2" ]] ; then
        # Update the key to match the long version
        local arr=(${param//\|/ })
        let last=${#arr[@]}-1
        key="${arr[$last]}"
        return 0
      fi
    done
  done
  return 1
}

# Get input parameters in short or long notation, with no dependencies beyond bash
# usage:
#     # First, set your defaults
#     param_help=false
#     param_path="."
#     param_file=false
#     param_image=false
#     param_image_lossy=true
#     # Define allowed parameters
#     allowed_params="h|?|help p|path f|file i|image image-lossy"
#     # Get parameters from the arguments provided
#     _get_params $*
#
# Parameters will be converted into safe variable names like:
#     param_help,
#     param_path,
#     param_file,
#     param_image,
#     param_image_lossy
#
# Parameters without a value like "-h" or "--help" will be treated as
# boolean, and will be set as param_help=true
#
# Parameters can accept values in the various typical ways:
#     -i "path/goes/here"
#     --image "path/goes/here"
#     --image="path/goes/here"
#     --image=path/goes/here
# These would all result in effectively the same thing:
#     param_image="path/goes/here"
#
# Concatinated short parameters (boolean) are also supported
#     -vhm is the same as -v -h -m
_get_params(){

  local param_pair
  local key
  local value
  local shift_count

  while : ; do
    # Ensure we have a valid param. Allows this to work even in -u mode.
    if [[ $# == 0 || -z $1 ]] ; then
      break
    fi

    # Split the argument if it contains "="
    param_pair=(${1//=/ })
    # Remove preceeding dashes
    key="${param_pair[0]#--}"

    # Check for concatinated boolean short parameters.
    local nodash="${key#-}"
    local breakout=false
    if [[ "$nodash" != "$key" && ${#nodash} -gt 1 ]]; then
      # Extrapolate multiple boolean keys in single dash notation. ie. "-vmh" should translate to: "-v -m -h"
      local short_param_count=${#nodash}
      let new_arg_count=$#+$short_param_count-1
      local new_args=""
      # $str_pos is the current position in the short param string $nodash
      for (( str_pos=0; str_pos<new_arg_count; str_pos++ )); do
        # The first character becomes the current key
        if [ $str_pos -eq 0 ] ; then
          key="${nodash:$str_pos:1}"
          breakout=true
        fi
        # $arg_pos is the current position in the constructed arguments list
        let arg_pos=$str_pos+1
        if [ $arg_pos -gt $short_param_count ] ; then
          # handle other arguments
          let orignal_arg_number=$arg_pos-$short_param_count+1
          local new_arg="${!orignal_arg_number}"
        else
          # break out our one argument into new ones
          local new_arg="-${nodash:$str_pos:1}"
        fi
        new_args="$new_args \"$new_arg\""
      done
      # remove the preceding space and set the new arguments
      eval set -- "${new_args# }"
    fi
    if ! $breakout ; then
      key="$nodash"
    fi

    # By default we expect to shift one argument at a time
    shift_count=1
    if [ "${#param_pair[@]}" -gt "1" ] ; then
      # This is a param with equals notation
      value="${param_pair[1]}"
    else
      # This is either a boolean param and there is no value,
      # or the value is the next command line argument
      # Assume the value is a boolean true, unless the next argument is found to be a value.
      value=true
      if [[ $# -gt 1 && -n "$2" ]]; then
        local nodash="${2#-}"
        if [ "$nodash" = "$2" ]; then
          # The next argument has NO preceding dash so it is a value
          value="$2"
          shift_count=2
        fi
      fi
    fi

    # Check that the param being passed is one of the allowed params
    if _param_variant "$allowed_params" "$key" ; then
      # --key-name will now become param_key_name
      eval param_${key//-/_}="$value"
    else
      printf 'WARNING: Unknown option (ignored): %s\n' "$1" >&2
    fi
    shift $shift_count
  done
}

Y úsalo así:

# Assign defaults for parameters
param_help=false
param_path=$(pwd)
param_file=false
param_image=true
param_image_lossy=true
param_image_lossy_quality=85

# Define the params we will allow
allowed_params="h|?|help p|path f|file i|image image-lossy image-lossy-quality"

# Get the params from arguments provided
_get_params $*
Heath Dutton
fuente
1

Para mantener la compatibilidad multiplataforma y evitar la dependencia de ejecutables externos, porté un código de otro idioma.

Me resulta muy fácil de usar, aquí hay un ejemplo:

ArgParser::addArg "[h]elp"    false    "This list"
ArgParser::addArg "[q]uiet"   false    "Supress output"
ArgParser::addArg "[s]leep"   1        "Seconds to sleep"
ArgParser::addArg "v"         1        "Verbose mode"

ArgParser::parse "$@"

ArgParser::isset help && ArgParser::showArgs

ArgParser::isset "quiet" \
   && echo "Quiet!" \
   || echo "Noisy!"

local __sleep
ArgParser::tryAndGetArg sleep into __sleep \
   && echo "Sleep for $__sleep seconds" \
   || echo "No value passed for sleep"

# This way is often more convienient, but is a little slower
echo "Sleep set to: $( ArgParser::getArg sleep )"

El BASH requerido es un poco más largo de lo que podría ser, pero quería evitar depender de los arrays asociativos de BASH 4. También puede descargar esto directamente desde http://nt4.com/bash/argparser.inc.sh

#!/usr/bin/env bash

# Updates to this script may be found at
# http://nt4.com/bash/argparser.inc.sh

# Example of runtime usage:
# mnc.sh --nc -q Caprica.S0*mkv *.avi *.mp3 --more-options here --host centos8.host.com

# Example of use in script (see bottom)
# Just include this file in yours, or use
# source argparser.inc.sh

unset EXPLODED
declare -a EXPLODED
function explode 
{
    local c=$# 
    (( c < 2 )) && 
    {
        echo function "$0" is missing parameters 
        return 1
    }

    local delimiter="$1"
    local string="$2"
    local limit=${3-99}

    local tmp_delim=$'\x07'
    local delin=${string//$delimiter/$tmp_delim}
    local oldifs="$IFS"

    IFS="$tmp_delim"
    EXPLODED=($delin)
    IFS="$oldifs"
}


# See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference
# Usage: local "$1" && upvar $1 "value(s)"
upvar() {
    if unset -v "$1"; then           # Unset & validate varname
        if (( $# == 2 )); then
            eval $1=\"\$2\"          # Return single value
        else
            eval $1=\(\"\${@:2}\"\)  # Return array
        fi
    fi
}

function decho
{
    :
}

function ArgParser::check
{
    __args=${#__argparser__arglist[@]}
    for (( i=0; i<__args; i++ ))
    do
        matched=0
        explode "|" "${__argparser__arglist[$i]}"
        if [ "${#1}" -eq 1 ]
        then
            if [ "${1}" == "${EXPLODED[0]}" ]
            then
                decho "Matched $1 with ${EXPLODED[0]}"
                matched=1

                break
            fi
        else
            if [ "${1}" == "${EXPLODED[1]}" ]
            then
                decho "Matched $1 with ${EXPLODED[1]}"
                matched=1

                break
            fi
        fi
    done
    (( matched == 0 )) && return 2
    # decho "Key $key has default argument of ${EXPLODED[3]}"
    if [ "${EXPLODED[3]}" == "false" ]
    then
        return 0
    else
        return 1
    fi
}

function ArgParser::set
{
    key=$3
    value="${1:-true}"
    declare -g __argpassed__$key="$value"
}

function ArgParser::parse
{

    unset __argparser__argv
    __argparser__argv=()
    # echo parsing: "$@"

    while [ -n "$1" ]
    do
        # echo "Processing $1"
        if [ "${1:0:2}" == '--' ]
        then
            key=${1:2}
            value=$2
        elif [ "${1:0:1}" == '-' ]
        then
            key=${1:1}               # Strip off leading -
            value=$2
        else
            decho "Not argument or option: '$1'" >& 2
            __argparser__argv+=( "$1" )
            shift
            continue
        fi
        # parameter=${tmp%%=*}     # Extract name.
        # value=${tmp##*=}         # Extract value.
        decho "Key: '$key', value: '$value'"
        # eval $parameter=$value
        ArgParser::check $key
        el=$?
        # echo "Check returned $el for $key"
        [ $el -eq  2 ] && decho "No match for option '$1'" >&2 # && __argparser__argv+=( "$1" )
        [ $el -eq  0 ] && decho "Matched option '${EXPLODED[2]}' with no arguments"        >&2 && ArgParser::set true "${EXPLODED[@]}"
        [ $el -eq  1 ] && decho "Matched option '${EXPLODED[2]}' with an argument of '$2'"   >&2 && ArgParser::set "$2" "${EXPLODED[@]}" && shift
        shift
    done
}

function ArgParser::isset
{
    declare -p "__argpassed__$1" > /dev/null 2>&1 && return 0
    return 1
}

function ArgParser::getArg
{
    # This one would be a bit silly, since we can only return non-integer arguments ineffeciently
    varname="__argpassed__$1"
    echo "${!varname}"
}

##
# usage: tryAndGetArg <argname> into <varname>
# returns: 0 on success, 1 on failure
function ArgParser::tryAndGetArg
{
    local __varname="__argpassed__$1"
    local __value="${!__varname}"
    test -z "$__value" && return 1
    local "$3" && upvar $3 "$__value"
    return 0
}

function ArgParser::__construct
{
    unset __argparser__arglist
    # declare -a __argparser__arglist
}

##
# @brief add command line argument
# @param 1 short and/or long, eg: [s]hort
# @param 2 default value
# @param 3 description
##
function ArgParser::addArg
{
    # check for short arg within long arg
    if [[ "$1" =~ \[(.)\] ]]
    then
        short=${BASH_REMATCH[1]}
        long=${1/\[$short\]/$short}
    else
        long=$1
    fi
    if [ "${#long}" -eq 1 ]
    then
        short=$long
        long=''
    fi
    decho short: "$short"
    decho long: "$long"
    __argparser__arglist+=("$short|$long|$1|$2|$3")
}

## 
# @brief show available command line arguments
##
function ArgParser::showArgs
{
    # declare -p | grep argparser
    printf "Usage: %s [OPTION...]\n\n" "$( basename "${BASH_SOURCE[0]}" )"
    printf "Defaults for the options are specified in brackets.\n\n";

    __args=${#__argparser__arglist[@]}
    for (( i=0; i<__args; i++ ))
    do
        local shortname=
        local fullname=
        local default=
        local description=
        local comma=

        explode "|" "${__argparser__arglist[$i]}"

        shortname="${EXPLODED[0]:+-${EXPLODED[0]}}" # String Substitution Guide: 
        fullname="${EXPLODED[1]:+--${EXPLODED[1]}}" # http://tldp.org/LDP/abs/html/parameter-substitution.html
        test -n "$shortname" \
            && test -n "$fullname" \
            && comma=","

        default="${EXPLODED[3]}"
        case $default in
            false )
                default=
                ;;
            "" )
                default=
                ;;
            * )
                default="[$default]"
        esac

        description="${EXPLODED[4]}"

        printf "  %2s%1s %-19s %s %s\n" "$shortname" "$comma" "$fullname" "$description" "$default"
    done
}

function ArgParser::test
{
    # Arguments with a default of 'false' do not take paramaters (note: default
    # values are not applied in this release)

    ArgParser::addArg "[h]elp"      false       "This list"
    ArgParser::addArg "[q]uiet" false       "Supress output"
    ArgParser::addArg "[s]leep" 1           "Seconds to sleep"
    ArgParser::addArg "v"           1           "Verbose mode"

    ArgParser::parse "$@"

    ArgParser::isset help && ArgParser::showArgs

    ArgParser::isset "quiet" \
        && echo "Quiet!" \
        || echo "Noisy!"

    local __sleep
    ArgParser::tryAndGetArg sleep into __sleep \
        && echo "Sleep for $__sleep seconds" \
        || echo "No value passed for sleep"

    # This way is often more convienient, but is a little slower
    echo "Sleep set to: $( ArgParser::getArg sleep )"

    echo "Remaining command line: ${__argparser__argv[@]}"

}

if [ "$( basename "$0" )" == "argparser.inc.sh" ]
then
    ArgParser::test "$@"
fi
Orwellophile
fuente
1

Si todas sus opciones largas tienen primeros caracteres únicos y coincidentes como las opciones cortas, por ejemplo,

./slamm --chaos 23 --plenty test -quiet

Es lo mismo que

./slamm -c 23 -p test -q

Puedes usar esto antes de getopts para reescribir $ args:

# change long options to short options

for arg; do 
    [[ "${arg:0:1}" == "-" ]] && delim="" || delim="\""
    if [ "${arg:0:2}" == "--" ]; 
       then args="${args} -${arg:2:1}" 
       else args="${args} ${delim}${arg}${delim}"
    fi
done

# reset the incoming args
eval set -- $args

# proceed as usual
while getopts ":b:la:h" OPTION; do
    .....

Gracias por mtvee por la inspiración ;-)

Pico común
fuente
No entiendo la importancia de
evaluar
1

si simplemente así es como quieres llamar al script

myscript.sh --input1 "ABC" --input2 "PQR" --input2 "XYZ"

entonces puede seguir esta forma más sencilla de lograrlo con la ayuda de getopt y --longoptions

intente esto, espero que esto sea útil

# Read command line options
ARGUMENT_LIST=(
    "input1"
    "input2"
    "input3"
)



# read arguments
opts=$(getopt \
    --longoptions "$(printf "%s:," "${ARGUMENT_LIST[@]}")" \
    --name "$(basename "$0")" \
    --options "" \
    -- "$@"
)


echo $opts

eval set --$opts

while true; do
    case "$1" in
    --input1)  
        shift
        empId=$1
        ;;
    --input2)  
        shift
        fromDate=$1
        ;;
    --input3)  
        shift
        toDate=$1
        ;;
      --)
        shift
        break
        ;;
    esac
    shift
done
Ashish Shetkar
fuente
0

getopts "podría usarse" para analizar opciones largas siempre y cuando no esperes que tengan argumentos ...

Aquí se explica cómo:

$ cat > longopt
while getopts 'e:-:' OPT; do
  case $OPT in
    e) echo echo: $OPTARG;;
    -) #long option
       case $OPTARG in
         long-option) echo long option;;
         *) echo long option: $OPTARG;;
       esac;;
  esac
done

$ bash longopt -e asd --long-option --long1 --long2 -e test
echo: asd
long option
long option: long1
long option: long2
echo: test

Si intenta utilizar OPTIND para obtener un parámetro para la opción larga, getopts lo tratará como el primer parámetro posicional no opcional y dejará de analizar cualquier otro parámetro. En tal caso, será mejor que lo manejes manualmente con una simple declaración de caso.

Esto "siempre" funcionará:

$ cat >longopt2
while (($#)); do
    OPT=$1
    shift
    case $OPT in
        --*) case ${OPT:2} in
            long1) echo long1 option;;
            complex) echo comples with argument $1; shift;;
        esac;;

        -*) case ${OPT:1} in
            a) echo short option a;;
            b) echo short option b with parameter $1; shift;;
        esac;;
    esac
done


$ bash longopt2 --complex abc -a --long -b test
comples with argument abc
short option a
short option b with parameter test

Aunque no es tan flexible como getopts y usted tiene que hacer la mayor parte del código de verificación de errores usted mismo dentro de las instancias del caso ...

Pero es una opción.

estani
fuente
Pero las opciones largas a menudo esperan argumentos. Y podría hacer más con el - para que funcione aunque sea algo así como un truco. Al final se podría argumentar que si no soporta de forma nativa, entonces todas las formas de poner en práctica es una especie de truco, pero sin embargo se podría extender el - también. Y sí, shift es muy útil pero, por supuesto, si espera un argumento, podría terminar en que el siguiente argumento (si el usuario no especifica ninguno) es parte del argumento esperado.
Pryftan
Sí, esta es una poc para nombres largos de argumentos sin argumentos, para diferenciar entre los dos necesita algún tipo de configuración, como getops. Y con respecto a shift, siempre puedes "volver a ponerlo" con set. En cualquier caso, debe ser configurable si se espera un parámetro o no. Incluso podría usar algo de magia para ello, pero luego obligará a los usuarios a usar, para indicar que la magia se detiene y comienzan los parámetros posicionales, lo que es peor en mi humilde opinión.
estani
Lo suficientemente justo. Eso es más que razonable. Por cierto, ni siquiera recuerdo a qué me refería y me había olvidado por completo de esta pregunta. Solo recuerdo vagamente cómo lo encontré incluso. Salud. Ah, y tiene un +1 para la idea. Usted hizo el esfuerzo y también aclaró a qué se refería. Respeto a las personas que se esfuerzan por dar ideas a otros, etc.
Pryftan
0

La función integradagetopts solo analiza opciones cortas (excepto en ksh93), pero aún puede agregar algunas líneas de secuencias de comandos para que getopts maneje opciones largas.

Aquí hay una parte del código que se encuentra en http://www.uxora.com/unix/shell-script/22-handle-long-options-with-getopts

  #== set short options ==#
SCRIPT_OPTS=':fbF:B:-:h'
  #== set long options associated with short one ==#
typeset -A ARRAY_OPTS
ARRAY_OPTS=(
    [foo]=f
    [bar]=b
    [foobar]=F
    [barfoo]=B
    [help]=h
    [man]=h
)

  #== parse options ==#
while getopts ${SCRIPT_OPTS} OPTION ; do
    #== translate long options to short ==#
    if [[ "x$OPTION" == "x-" ]]; then
        LONG_OPTION=$OPTARG
        LONG_OPTARG=$(echo $LONG_OPTION | grep "=" | cut -d'=' -f2)
        LONG_OPTIND=-1
        [[ "x$LONG_OPTARG" = "x" ]] && LONG_OPTIND=$OPTIND || LONG_OPTION=$(echo $OPTARG | cut -d'=' -f1)
        [[ $LONG_OPTIND -ne -1 ]] && eval LONG_OPTARG="\$$LONG_OPTIND"
        OPTION=${ARRAY_OPTS[$LONG_OPTION]}
        [[ "x$OPTION" = "x" ]] &&  OPTION="?" OPTARG="-$LONG_OPTION"

        if [[ $( echo "${SCRIPT_OPTS}" | grep -c "${OPTION}:" ) -eq 1 ]]; then
            if [[ "x${LONG_OPTARG}" = "x" ]] || [[ "${LONG_OPTARG}" = -* ]]; then 
                OPTION=":" OPTARG="-$LONG_OPTION"
            else
                OPTARG="$LONG_OPTARG";
                if [[ $LONG_OPTIND -ne -1 ]]; then
                    [[ $OPTIND -le $Optnum ]] && OPTIND=$(( $OPTIND+1 ))
                    shift $OPTIND
                    OPTIND=1
                fi
            fi
        fi
    fi

    #== options follow by another option instead of argument ==#
    if [[ "x${OPTION}" != "x:" ]] && [[ "x${OPTION}" != "x?" ]] && [[ "${OPTARG}" = -* ]]; then 
        OPTARG="$OPTION" OPTION=":"
    fi

    #== manage options ==#
    case "$OPTION" in
        f  ) foo=1 bar=0                    ;;
        b  ) foo=0 bar=1                    ;;
        B  ) barfoo=${OPTARG}               ;;
        F  ) foobar=1 && foobar_name=${OPTARG} ;;
        h ) usagefull && exit 0 ;;
        : ) echo "${SCRIPT_NAME}: -$OPTARG: option requires an argument" >&2 && usage >&2 && exit 99 ;;
        ? ) echo "${SCRIPT_NAME}: -$OPTARG: unknown option" >&2 && usage >&2 && exit 99 ;;
    esac
done
shift $((${OPTIND} - 1))

Aquí hay una prueba:

# Short options test
$ ./foobar_any_getopts.sh -bF "Hello world" -B 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello world
files=file1 file2

# Long and short options test
$ ./foobar_any_getopts.sh --bar -F Hello --barfoo 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello
files=file1 file2

De lo contrario, en el reciente Korn Shell ksh93, getoptsnaturalmente puede analizar opciones largas e incluso mostrar una página de manual por igual. (Ver http://www.uxora.com/unix/shell-script/20-getopts-with-man-page-and-long-options )

Michel VONGVILAY uxora.com
fuente
0

Th incorporado en OS X (BSD) getopt no es compatible con las opciones largas, pero la versión de GNU hace: brew install gnu-getopt. Entonces, algo similar a: cp /usr/local/Cellar/gnu-getopt/1.1.6/bin/getopt /usr/local/bin/gnu-getopt.

wprl
fuente
0

EasyOptions maneja opciones cortas y largas:

## Options:
##   --verbose, -v   Verbose mode
##   --logfile=NAME  Log filename

source easyoptions || exit

if test -n "${verbose}"; then
    echo "log file: ${logfile}"
    echo "arguments: ${arguments[@]}"
fi
Renato Silva
fuente