¿Cómo analizo los argumentos de la línea de comandos en Bash?


1922

Digamos, tengo un script que se llama con esta línea:

./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile

o este:

./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile 

¿Cuál es la forma aceptada de este análisis de manera que en cada caso (o alguna combinación de los dos) $v, $fy $dtodos se ponen a truey $outFileserán iguales a /fizz/someOtherFile?


1
Para los usuarios de zsh hay una gran función llamada zparseopts que puede hacer: zparseopts -D -E -M -- d=debug -debug=dY tener ambos -dy --debugen la $debugmatriz echo $+debug[1]devolverá 0 o 1 si se usa uno de esos. Ref: zsh.org/mla/users/2011/msg00350.html
dezza

1
Muy buen tutorial: linuxcommand.org/lc3_wss0120.php . Me gusta especialmente el ejemplo de "Opciones de línea de comandos".
Gabriel Staples

Respuestas:


2677

Método # 1: Usando bash sin getopt [s]

Dos formas comunes de pasar argumentos par clave-valor son:

Bash Separado por espacio (p. Ej. --option argument) (Sin getopt [s])

Uso demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts

cat >/tmp/demo-space-separated.sh <<'EOF'
#!/bin/bash

POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"

case $key in
    -e|--extension)
    EXTENSION="$2"
    shift # past argument
    shift # past value
    ;;
    -s|--searchpath)
    SEARCHPATH="$2"
    shift # past argument
    shift # past value
    ;;
    -l|--lib)
    LIBPATH="$2"
    shift # past argument
    shift # past value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument
    ;;
    *)    # unknown option
    POSITIONAL+=("$1") # save it in an array for later
    shift # past argument
    ;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters

echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 "$1"
fi
EOF

chmod +x /tmp/demo-space-separated.sh

/tmp/demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts

salida de copiar y pegar el bloque de arriba:

FILE EXTENSION  = conf
SEARCH PATH     = /etc
LIBRARY PATH    = /usr/lib
DEFAULT         =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34    example.com

Bash Equals-Separated (eg, --option=argument) (sin getopt [s])

Uso demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

cat >/tmp/demo-equals-separated.sh <<'EOF'
#!/bin/bash

for i in "$@"
do
case $i in
    -e=*|--extension=*)
    EXTENSION="${i#*=}"
    shift # past argument=value
    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    shift # past argument=value
    ;;
    -l=*|--lib=*)
    LIBPATH="${i#*=}"
    shift # past argument=value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument with no value
    ;;
    *)
          # unknown option
    ;;
esac
done
echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 $1
fi
EOF

chmod +x /tmp/demo-equals-separated.sh

/tmp/demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

salida de copiar y pegar el bloque de arriba:

FILE EXTENSION  = conf
SEARCH PATH     = /etc
LIBRARY PATH    = /usr/lib
DEFAULT         =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34    example.com

Para comprender mejor, ${i#*=}busque "Eliminación de subcadenas" en esta guía . Es funcionalmente equivalente a lo `sed 's/[^=]*=//' <<< "$i"`que llama un subproceso innecesario o lo `echo "$i" | sed 's/[^=]*=//'`que llama a dos subprocesos innecesarios.

Método # 2: Usando bash con getopt [s]

de: http://mywiki.wooledge.org/BashFAQ/035#getopts

limitaciones de getopt (1) ( getoptversiones anteriores, relativamente recientes ):

  • no puede manejar argumentos que son cadenas vacías
  • no puede manejar argumentos con espacios en blanco incrustados

Las getoptversiones más recientes no tienen estas limitaciones.

Además, la oferta de shell POSIX (y otros) getoptsque no tiene estas limitaciones. He incluido un getoptsejemplo simplista .

Uso demo-getopts.sh -vf /etc/hosts foo bar

cat >/tmp/demo-getopts.sh <<'EOF'
#!/bin/sh

# A POSIX variable
OPTIND=1         # Reset in case getopts has been used previously in the shell.

# Initialize our own variables:
output_file=""
verbose=0

while getopts "h?vf:" opt; do
    case "$opt" in
    h|\?)
        show_help
        exit 0
        ;;
    v)  verbose=1
        ;;
    f)  output_file=$OPTARG
        ;;
    esac
done

shift $((OPTIND-1))

[ "${1:-}" = "--" ] && shift

echo "verbose=$verbose, output_file='$output_file', Leftovers: $@"
EOF

chmod +x /tmp/demo-getopts.sh

/tmp/demo-getopts.sh -vf /etc/hosts foo bar

salida de copiar y pegar el bloque de arriba:

verbose=1, output_file='/etc/hosts', Leftovers: foo bar

Las ventajas de getoptsson:

  1. Es más portátil y funcionará en otros shells como dash.
  2. Puede manejar múltiples opciones individuales como -vf filenameen la forma típica de Unix, automáticamente.

La desventaja getoptses que solo puede manejar opciones cortas ( -hno --help) sin código adicional.

Hay un tutorial de getopts que explica lo que significan todas las sintaxis y variables. En bash, también hay help getopts, que podría ser informativo.


44
¿Es esto realmente cierto? Según Wikipedia, hay una nueva versión mejorada de GNU getoptque incluye toda la funcionalidad de getoptsy más. man getopten las salidas de Ubuntu 13.04 getopt - parse command options (enhanced)como nombre, por lo que supongo que esta versión mejorada es estándar ahora.
Livven

47
Que algo es de cierta manera en su sistema es una premisa muy débil para basar los supuestos de "ser estándar".
szablica

13
@Livven, eso getoptno es una utilidad GNU, es parte de util-linux.
Stephane Chazelas

44
Si lo usa -gt 0, elimine su shiftdespués de esac, aumente todo shiftpor 1 y agregue este caso: *) break;;puede manejar argumentos no opcionales. Ej: pastebin.com/6DJ57HTc
Nicolas Lacombe

2
No haces eco –default. En el primer ejemplo, noto que si –defaultes el último argumento, no se procesa (se considera como no opt), a menos que while [[ $# -gt 1 ]]se establezca como while [[ $# -gt 0 ]]
kolydart

562

Ninguna respuesta menciona getopt mejorado . Y la respuesta más votada es engañosa: ignora -⁠vfdlas opciones cortas de estilo (solicitadas por el OP) u opciones después de argumentos posicionales (también solicitados por el OP); e ignora los errores de análisis. En lugar:

  • Uso mejorado getoptde util-linux o anteriormente GNU glibc . 1
  • Funciona con getopt_long()la función C de GNU glibc.
  • Tiene todas las características distintivas útiles (las otras no las tienen):
    • maneja espacios, citando caracteres e incluso binarios en argumentos 2 (no mejorado getoptno puede hacer esto)
    • puede manejar opciones al final: script.sh -o outFile file1 file2 -v( getoptsno hace esto)
    • permite =opciones largas de estilo: script.sh --outfile=fileOut --infile fileIn(permitir que ambas sean largas si se analiza automáticamente)
    • permite opciones cortas combinadas, p. ej. -vfd(trabajo real si se analiza automáticamente)
    • permite tocar argumentos de opción, por ejemplo -oOutfileo-vfdoOutfile
  • Ya es tan antiguo 3 que a ningún sistema GNU le falta esto (por ejemplo, cualquier Linux lo tiene).
  • Puede comprobar su existencia con: getopt --test→ valor de retorno 4.
  • Otros getopto de concha incorporada getoptsson de uso limitado.

Las siguientes llamadas

myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile
myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile
myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd
myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile

todo vuelve

verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile

con lo siguiente myscript

#!/bin/bash
# saner programming env: these switches turn some bugs into errors
set -o errexit -o pipefail -o noclobber -o nounset

# -allow a command to fail with !’s side effect on errexit
# -use return value from ${PIPESTATUS[0]}, because ! hosed $?
! getopt --test > /dev/null 
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
    echo 'I’m sorry, `getopt --test` failed in this environment.'
    exit 1
fi

OPTIONS=dfo:v
LONGOPTS=debug,force,output:,verbose

# -regarding ! and PIPESTATUS see above
# -temporarily store output to be able to check for errors
# -activate quoting/enhanced mode (e.g. by writing out “--options”)
# -pass arguments only via   -- "$@"   to separate them correctly
! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    # e.g. return value is 1
    #  then getopt has complained about wrong arguments to stdout
    exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"

d=n f=n v=n outFile=-
# now enjoy the options in order and nicely split until we see --
while true; do
    case "$1" in
        -d|--debug)
            d=y
            shift
            ;;
        -f|--force)
            f=y
            shift
            ;;
        -v|--verbose)
            v=y
            shift
            ;;
        -o|--output)
            outFile="$2"
            shift 2
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Programming error"
            exit 3
            ;;
    esac
done

# handle non-option arguments
if [[ $# -ne 1 ]]; then
    echo "$0: A single input file is required."
    exit 4
fi

echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"

1 getopt mejorado está disponible en la mayoría de los "sistemas bash", incluido Cygwin; en OS X intente brew install gnu-getopt osudo port install getopt
2 lasexec()convencionesPOSIXno tienen una forma confiable de pasar NULL binario en los argumentos de la línea de comandos; esos bytes finalizan prematuramente laprimera versióndel argumento
3 lanzada en 1997 o antes (solo lo rastreé hasta 1997)


44
Gracias por esto. Recién confirmado de la tabla de características en en.wikipedia.org/wiki/Getopts , si necesita soporte para opciones largas y no está en Solaris, este getoptes el camino a seguir.
johncip

44
Creo que la única advertencia getoptes que no se puede usar convenientemente en scripts de envoltura donde uno podría tener pocas opciones específicas para la secuencia de comandos de envoltura, y luego pasar las opciones de la secuencia de comandos no envolvente al ejecutable envuelto, intacto. Digamos que tengo un grepcontenedor llamado mygrepy tengo una opción --fooespecífica para mygrep, entonces no puedo hacerlo mygrep --foo -A 2, y tengo el -A 2pase automáticamente grep; Yo necesito hacer mygrep --foo -- -A 2. Aquí está mi implementación además de su solución.
Kaushal Modi

2
@bobpaul Su declaración sobre util-linux también es incorrecta y engañosa: el paquete está marcado como "esencial" en Ubuntu / Debian. Como tal, siempre está instalado. - ¿De qué distribuciones estás hablando (donde dices que necesita ser instalado a propósito)?
Robert Siemer

3
Tenga en cuenta que esto no funciona en Mac al menos hasta el 10.14.3 actual. El getopt que se envía es BSD getopt de 1999 ...
jjj

2
@transang Negación booleana del valor de retorno. Y su efecto secundario: permitir que falle un comando (de lo contrario, errexit abortaría el programa por error). - Los comentarios en el guión te dicen más. De lo contrario:man bash
Robert Siemer el

144

Manera más sucinta

script.sh

#!/bin/bash

while [[ "$#" -gt 0 ]]; do
    case $1 in
        -d|--deploy) deploy="$2"; shift ;;
        -u|--uglify) uglify=1 ;;
        *) echo "Unknown parameter passed: $1"; exit 1 ;;
    esac
    shift
done

echo "Should deploy? $deploy"
echo "Should uglify? $uglify"

Uso:

./script.sh -d dev -u

# OR:

./script.sh --deploy dev --uglify

3
Esto es lo que estoy haciendo. Tengo que hacerlo while [[ "$#" > 1 ]]si quiero soportar terminar la línea con una bandera booleana ./script.sh --debug dev --uglify fast --verbose. Ejemplo: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli

12
¡Guauu! ¡Simple y limpio! Así es como estoy usando esto: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
hfossli

2
Es mucho mejor pegarlo en cada script en lugar de tratar con la fuente o hacer que la gente se pregunte dónde comienza realmente su funcionalidad.
RealHandy

Advertencia: esto tolera argumentos duplicados, prevalece el último argumento. por ejemplo ./script.sh -d dev -d prod, resultaría en deploy == 'prod'. Lo usé de todos modos: P :): +1:
yair

Estoy usando esto (¡gracias!) Pero tenga en cuenta que permite un valor de argumento vacío, por ejemplo ./script.sh -d, no generaría un error, sino que solo se establecería $deployen una cadena vacía.
EM0

137

de: digitalpeer.com con modificaciones menores

Uso myscript.sh -p=my_prefix -s=dirname -l=libname

#!/bin/bash
for i in "$@"
do
case $i in
    -p=*|--prefix=*)
    PREFIX="${i#*=}"

    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    ;;
    -l=*|--lib=*)
    DIR="${i#*=}"
    ;;
    --default)
    DEFAULT=YES
    ;;
    *)
            # unknown option
    ;;
esac
done
echo PREFIX = ${PREFIX}
echo SEARCH PATH = ${SEARCHPATH}
echo DIRS = ${DIR}
echo DEFAULT = ${DEFAULT}

Para comprender mejor, ${i#*=}busque "Eliminación de subcadenas" en esta guía . Es funcionalmente equivalente a lo `sed 's/[^=]*=//' <<< "$i"`que llama un subproceso innecesario o lo `echo "$i" | sed 's/[^=]*=//'`que llama a dos subprocesos innecesarios.


44
¡Ordenado! Aunque esto no funcionará para argumentos separados por espacios à la mount -t tempfs .... Probablemente se pueda arreglar esto a través de algo como while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;;etc.
Tobias Kienzler

3
Esto no puede manejar -vfdlas opciones cortas combinadas de estilo.
Robert Siemer

105

getopt()/ getopts()es una buena opción. Robado de aquí :

El uso simple de "getopt" se muestra en este mini script:

#!/bin/bash
echo "Before getopt"
for i
do
  echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
  echo "-->$i"
done

Lo que hemos dicho es que se permitirá cualquiera de -a, -b, -c o -d, pero que a -c le sigue un argumento (el "c:" dice eso).

Si llamamos a esto "g" y lo probamos:

bash-2.05a$ ./g -abc foo
Before getopt
-abc
foo
After getopt
-->-a
-->-b
-->-c
-->foo
-->--

Comenzamos con dos argumentos, y "getopt" separa las opciones y pone cada una en su propio argumento. También agregó "-".


44
El uso $*es uso roto de getopt. (Contiene argumentos con espacios). Vea mi respuesta para un uso adecuado.
Robert Siemer

¿Por qué querrías hacerlo más complicado?
SDsolar

@Matt J, la primera parte del script (para i) podría manejar argumentos con espacios en ellos si usa "$ i" en lugar de $ i. El getopts no parece ser capaz de manejar argumentos con espacios. ¿Cuál sería la ventaja de usar getopt sobre el ciclo for i?
thebunnyrules

99

A riesgo de agregar otro ejemplo para ignorar, aquí está mi esquema.

  • asas -n argy--name=arg
  • permite argumentos al final
  • muestra errores sanos si algo está mal escrito
  • compatible, no usa bashisms
  • legible, no requiere mantener el estado en un bucle

Espero que sea útil para alguien.

while [ "$#" -gt 0 ]; do
  case "$1" in
    -n) name="$2"; shift 2;;
    -p) pidfile="$2"; shift 2;;
    -l) logfile="$2"; shift 2;;

    --name=*) name="${1#*=}"; shift 1;;
    --pidfile=*) pidfile="${1#*=}"; shift 1;;
    --logfile=*) logfile="${1#*=}"; shift 1;;
    --name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;;

    -*) echo "unknown option: $1" >&2; exit 1;;
    *) handle_argument "$1"; shift 1;;
  esac
done

44
Perdón por el retraso. En mi script, la función handle_argument recibe todos los argumentos no opcionales. Puede reemplazar esa línea con lo que desee, tal vez *) die "unrecognized argument: $1"o recopilar los argumentos en una variable *) args+="$1"; shift 1;;.
Bronson

¡Asombroso! He probado un par de respuestas, pero esta es la única que funcionó para todos los casos, incluidos muchos parámetros posicionales (tanto antes como después de las banderas)
Guilherme Garnier

2
buen código sucinto, pero el uso de -n y ningún otro argumento provoca un bucle infinito debido a un error shift 2, emitiendo shiftdos veces en lugar de shift 2. Sugirió la edición.
lauksas

42

Llegué 4 años tarde a esta pregunta, pero quiero retribuir. Utilicé las respuestas anteriores como punto de partida para ordenar mi antiguo análisis de parámetros ad hoc. Luego reescribí el siguiente código de plantilla. Maneja parámetros largos y cortos, utilizando argumentos separados por espacios o =, así como múltiples parámetros cortos agrupados. Finalmente, vuelve a insertar cualquier argumento que no sea param en las variables $ 1, $ 2 .. Espero que sea útil.

#!/usr/bin/env bash

# NOTICE: Uncomment if your script depends on bashisms.
#if [ -z "$BASH_VERSION" ]; then bash $0 $@ ; exit $? ; fi

echo "Before"
for i ; do echo - $i ; done


# Code template for parsing command line parameters using only portable shell
# code, while handling both long and short params, handling '-f file' and
# '-f=file' style param data and also capturing non-parameters to be inserted
# back into the shell positional parameters.

while [ -n "$1" ]; do
        # Copy so we can modify it (can't modify $1)
        OPT="$1"
        # Detect argument termination
        if [ x"$OPT" = x"--" ]; then
                shift
                for OPT ; do
                        REMAINS="$REMAINS \"$OPT\""
                done
                break
        fi
        # Parse current opt
        while [ x"$OPT" != x"-" ] ; do
                case "$OPT" in
                        # Handle --flag=value opts like this
                        -c=* | --config=* )
                                CONFIGFILE="${OPT#*=}"
                                shift
                                ;;
                        # and --flag value opts like this
                        -c* | --config )
                                CONFIGFILE="$2"
                                shift
                                ;;
                        -f* | --force )
                                FORCE=true
                                ;;
                        -r* | --retry )
                                RETRY=true
                                ;;
                        # Anything unknown is recorded for later
                        * )
                                REMAINS="$REMAINS \"$OPT\""
                                break
                                ;;
                esac
                # Check for multiple short options
                # NOTICE: be sure to update this pattern to match valid options
                NEXTOPT="${OPT#-[cfr]}" # try removing single short opt
                if [ x"$OPT" != x"$NEXTOPT" ] ; then
                        OPT="-$NEXTOPT"  # multiple short opts, keep going
                else
                        break  # long form, exit inner loop
                fi
        done
        # Done with that param. move to next
        shift
done
# Set the non-parameters back into the positional parameters ($1 $2 ..)
eval set -- $REMAINS


echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'"
for i ; do echo - $i ; done

Este código no puede manejar opciones con argumentos como este: -c1. Y el uso de =separar opciones cortas de sus argumentos es inusual ...
Robert Siemer

2
Me encontré con dos problemas con este útil fragmento de código: 1) el "cambio" en el caso de "-c = foo" termina comiendo el siguiente parámetro; y 2) 'c' no debe incluirse en el patrón "[cfr]" para opciones cortas combinables.
sfnd

36

El asunto de escribir un análisis portátil en scripts es tan frustrante que he escrito Argbash , un generador de código FOSS que puede generar el código de análisis de argumentos para su script y tiene algunas características interesantes :

https://argbash.io


Gracias por escribir argbash, lo acabo de usar y descubrí que funciona bien. Principalmente elegí argbash porque es un generador de código que admite el bash 3.x anterior que se encuentra en OS X 10.11 El Capitan. El único inconveniente es que el enfoque de generador de código significa bastante código en su script principal, en comparación con llamar a un módulo.
RichVel

En realidad, puede usar Argbash de una manera que produzca una biblioteca de análisis a medida solo para usted que puede haber incluido en su script o puede tenerlo en un archivo separado y simplemente obtenerlo. He agregado un ejemplo para demostrarlo y también lo he hecho más explícito en la documentación.
bubla

Bueno saber. Ese ejemplo es interesante pero aún no está realmente claro: tal vez pueda cambiar el nombre del script generado a 'parse_lib.sh' o similar y mostrar dónde lo llama el script principal (como en la sección del script de envoltura, que es un caso de uso más complejo).
RichVel

Los problemas se abordaron en la versión reciente de argbash: se mejoró la documentación, se introdujo un script de inicio rápido argbash-init e incluso puede usar argbash en línea en argbash.io/generate
bubla

29

Mi respuesta se basa en gran medida en la respuesta de Bruno Bronosky , pero mezclé sus dos implementaciones de bash puro en una que uso con bastante frecuencia.

# As long as there is at least one more argument, keep looping
while [[ $# -gt 0 ]]; do
    key="$1"
    case "$key" in
        # This is a flag type option. Will catch either -f or --foo
        -f|--foo)
        FOO=1
        ;;
        # Also a flag type option. Will catch either -b or --bar
        -b|--bar)
        BAR=1
        ;;
        # This is an arg value type option. Will catch -o value or --output-file value
        -o|--output-file)
        shift # past the key and to the value
        OUTPUTFILE="$1"
        ;;
        # This is an arg=value type option. Will catch -o=value or --output-file=value
        -o=*|--output-file=*)
        # No need to shift here since the value is part of the same string
        OUTPUTFILE="${key#*=}"
        ;;
        *)
        # Do whatever you want with extra options
        echo "Unknown option '$key'"
        ;;
    esac
    # Shift after checking all the cases to get the next option
    shift
done

Esto le permite tener opciones / valores separados por espacios, así como valores definidos iguales.

Para que pueda ejecutar su script usando:

./myscript --foo -b -o /fizz/file.txt

tanto como:

./myscript -f --bar -o=/fizz/file.txt

y ambos deberían tener el mismo resultado final.

PROS:

  • Permite tanto -arg = value como -arg value

  • Funciona con cualquier nombre arg que pueda usar en bash

    • Significado -a o -arg o --arg o -arg o lo que sea
  • Pura fiesta. No es necesario aprender / usar getopt o getopts

CONTRAS:

  • No se pueden combinar args.

    • Significado no -abc. Debes hacer -a -b -c

Estos son los únicos pros / contras en los que puedo pensar fuera de mi cabeza


15

Creo que este es lo suficientemente simple como para usar:

#!/bin/bash
#

readopt='getopts $opts opt;rc=$?;[ $rc$opt == 0? ]&&exit 1;[ $rc == 0 ]||{ shift $[OPTIND-1];false; }'

opts=vfdo:

# Enumerating options
while eval $readopt
do
    echo OPT:$opt ${OPTARG+OPTARG:$OPTARG}
done

# Enumerating arguments
for arg
do
    echo ARG:$arg
done

Ejemplo de invocación:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile

1
Lo leí todo y este es mi preferido. No me gusta usar -a=1como estilo argc. Prefiero poner primero la opción principal -opciones y luego las especiales con espaciado simple -o option. Estoy buscando la forma más sencilla de leer argumentos.
m3nda

Funciona muy bien, pero si pasa un argumento a una opción no a: todas las siguientes opciones se tomarán como argumentos. Puede verificar esta línea ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFilecon su propio script. -d opción no está establecida como d:
m3nda

15

Ampliando la excelente respuesta de @guneysus, aquí hay un ajuste que permite al usuario usar la sintaxis que prefiera, por ejemplo

command -x=myfilename.ext --another_switch 

vs

command -x myfilename.ext --another_switch

Es decir, los iguales se pueden reemplazar con espacios en blanco.

Es posible que esta "interpretación difusa" no sea de su agrado, pero si está creando scripts que son intercambiables con otras utilidades (como es el caso con el mío, que debe funcionar con ffmpeg), la flexibilidad es útil.

STD_IN=0

prefix=""
key=""
value=""
for keyValue in "$@"
do
  case "${prefix}${keyValue}" in
    -i=*|--input_filename=*)  key="-i";     value="${keyValue#*=}";; 
    -ss=*|--seek_from=*)      key="-ss";    value="${keyValue#*=}";;
    -t=*|--play_seconds=*)    key="-t";     value="${keyValue#*=}";;
    -|--stdin)                key="-";      value=1;;
    *)                                      value=$keyValue;;
  esac
  case $key in
    -i) MOVIE=$(resolveMovie "${value}");  prefix=""; key="";;
    -ss) SEEK_FROM="${value}";          prefix=""; key="";;
    -t)  PLAY_SECONDS="${value}";           prefix=""; key="";;
    -)   STD_IN=${value};                   prefix=""; key="";; 
    *)   prefix="${keyValue}=";;
  esac
done

13

Este ejemplo muestra cómo se utilizan getopty eval, y HEREDOC, y shiftpara manejar los parámetros de corto y largo plazo con y sin un valor requerido que sigue. Además, la declaración de cambio / caso es concisa y fácil de seguir.

#!/usr/bin/env bash

# usage function
function usage()
{
   cat << HEREDOC

   Usage: $progname [--num NUM] [--time TIME_STR] [--verbose] [--dry-run]

   optional arguments:
     -h, --help           show this help message and exit
     -n, --num NUM        pass in a number
     -t, --time TIME_STR  pass in a time string
     -v, --verbose        increase the verbosity of the bash script
     --dry-run            do a dry run, dont change any files

HEREDOC
}  

# initialize variables
progname=$(basename $0)
verbose=0
dryrun=0
num_str=
time_str=

# use getopt and store the output into $OPTS
# note the use of -o for the short options, --long for the long name options
# and a : for any option that takes a parameter
OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; usage; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  # uncomment the next line to see how shift is working
  # echo "\$1:\"$1\" \$2:\"$2\""
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

if (( $verbose > 0 )); then

   # print out all the parameters we read in
   cat <<-EOM
   num=$num_str
   time=$time_str
   verbose=$verbose
   dryrun=$dryrun
EOM
fi

# The rest of your script below

Las líneas más significativas del script anterior son estas:

OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

Corto, directo, legible y maneja casi todo (en mi humilde opinión).

Espero que ayude a alguien.


1
Esta es una de las mejores respuestas.
Sr. Polywhirl

11

Te doy la función parse_paramsque analizará los parámetros desde la línea de comandos.

  1. Es una solución pura de Bash, sin utilidades adicionales.
  2. No contamina el alcance global.
  3. Sin esfuerzo, le devuelve variables fáciles de usar, sobre las que podría construir más lógica.
  4. La cantidad de guiones antes de los parámetros no importa ( --alles -alligual a igual all=all)

El siguiente script es una demostración de trabajo de copiar y pegar. Vea la show_usefunción para entender cómo usarparse_params .

Limitaciones:

  1. No admite parámetros delimitados por espacios ( -d 1)
  2. Los nombres de los parámetros perderán guiones --any-paramy -anyparamson equivalentes
  3. eval $(parse_params "$@")debe usarse dentro de la función bash (no funcionará en el ámbito global)

#!/bin/bash

# Universal Bash parameter parsing
# Parse equal sign separated params into named local variables
# Standalone named parameter value will equal its param name (--force creates variable $force=="force")
# Parses multi-valued named params into an array (--path=path1 --path=path2 creates ${path[*]} array)
# Puts un-named params as-is into ${ARGV[*]} array
# Additionally puts all named params as-is into ${ARGN[*]} array
# Additionally puts all standalone "option" params as-is into ${ARGO[*]} array
# @author Oleksii Chekulaiev
# @version v1.4.1 (Jul-27-2018)
parse_params ()
{
    local existing_named
    local ARGV=() # un-named params
    local ARGN=() # named params
    local ARGO=() # options (--params)
    echo "local ARGV=(); local ARGN=(); local ARGO=();"
    while [[ "$1" != "" ]]; do
        # Escape asterisk to prevent bash asterisk expansion, and quotes to prevent string breakage
        _escaped=${1/\*/\'\"*\"\'}
        _escaped=${_escaped//\'/\\\'}
        _escaped=${_escaped//\"/\\\"}
        # If equals delimited named parameter
        nonspace="[^[:space:]]"
        if [[ "$1" =~ ^${nonspace}${nonspace}*=..* ]]; then
            # Add to named parameters array
            echo "ARGN+=('$_escaped');"
            # key is part before first =
            local _key=$(echo "$1" | cut -d = -f 1)
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # val is everything after key and = (protect from param==value error)
            local _val="${1/$_key=}"
            # remove dashes from key name
            _key=${_key//\-}
            # skip when key is empty
            # search for existing parameter name
            if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then
                # if name already exists then it's a multi-value named parameter
                # re-declare it as an array if needed
                if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then
                    echo "$_key=(\"\$$_key\");"
                fi
                # append new value
                echo "$_key+=('$_val');"
            else
                # single-value named parameter
                echo "local $_key='$_val';"
                existing_named=" $_key"
            fi
        # If standalone named parameter
        elif [[ "$1" =~ ^\-${nonspace}+ ]]; then
            # remove dashes
            local _key=${1//\-}
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # Add to options array
            echo "ARGO+=('$_escaped');"
            echo "local $_key=\"$_key\";"
        # non-named parameter
        else
            # Escape asterisk to prevent bash asterisk expansion
            _escaped=${1/\*/\'\"*\"\'}
            echo "ARGV+=('$_escaped');"
        fi
        shift
    done
}

#--------------------------- DEMO OF THE USAGE -------------------------------

show_use ()
{
    eval $(parse_params "$@")
    # --
    echo "${ARGV[0]}" # print first unnamed param
    echo "${ARGV[1]}" # print second unnamed param
    echo "${ARGN[0]}" # print first named param
    echo "${ARG0[0]}" # print first option param (--force)
    echo "$anyparam"  # print --anyparam value
    echo "$k"         # print k=5 value
    echo "${multivalue[0]}" # print first value of multi-value
    echo "${multivalue[1]}" # print second value of multi-value
    [[ "$force" == "force" ]] && echo "\$force is set so let the force be with you"
}

show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2

Para usar la demostración para analizar los parámetros que entran en su script de bash, simplemente haga lo siguienteshow_use "$@"
Oleksii Chekulaiev

Básicamente descubrí que github.com/renatosilva/easyoptions hace lo mismo de la misma manera pero es un poco más masivo que esta función.
Oleksii Chekulaiev

10

EasyOptions no requiere ningún análisis:

## Options:
##   --verbose, -v  Verbose mode
##   --output=FILE  Output filename

source easyoptions || exit

if test -n "${verbose}"; then
    echo "output file is ${output}"
    echo "${arguments[@]}"
fi

Me tomó un minuto darme cuenta de que los comentarios en la parte superior de su script de ejemplo se están analizando para proporcionar una cadena de ayuda de uso predeterminada, así como especificaciones de argumentos. Esta es una solución brillante y lamento que solo haya obtenido 6 votos en 2 años. Quizás esta pregunta está demasiado saturada para que la gente se dé cuenta.
Metamórfico el

En cierto sentido, su solución es, con mucho, la mejor (aparte de @ OleksiiChekulaiev, que no admite la sintaxis de opción "estándar"). Esto se debe a que su solución solo requiere que el escritor de scripts especifique el nombre de cada opción una vez . El hecho de que otras soluciones requieran que se especifique 3 veces, en el uso, en el patrón de 'caso' y en la configuración de la variable, me ha molestado continuamente. Incluso getopt tiene este problema. Sin embargo, su código es lento en mi máquina: 0.11s para la implementación de Bash, 0.28s para Ruby. Versus 0.02s para el análisis explícito "while-case".
Metamórfico el

Quiero una versión más rápida, tal vez escrita en C. Además, una versión que sea compatible con zsh. Tal vez esto merece una pregunta por separado ("¿Hay alguna forma de analizar argumentos de línea de comandos en shells tipo Bash que acepte la sintaxis estándar de opciones largas y no requiera que los nombres de las opciones se tomen más de una vez?").
Metamórfico el

10

getopts funciona muy bien si # 1 lo tiene instalado y # 2 tiene la intención de ejecutarlo en la misma plataforma. OSX y Linux (por ejemplo) se comportan de manera diferente a este respecto.

Aquí hay una solución (no getopts) que admite indicadores iguales, no iguales y booleanos. Por ejemplo, podría ejecutar su script de esta manera:

./script --arg1=value1 --arg2 value2 --shouldClean

# parse the arguments.
COUNTER=0
ARGS=("$@")
while [ $COUNTER -lt $# ]
do
    arg=${ARGS[$COUNTER]}
    let COUNTER=COUNTER+1
    nextArg=${ARGS[$COUNTER]}

    if [[ $skipNext -eq 1 ]]; then
        echo "Skipping"
        skipNext=0
        continue
    fi

    argKey=""
    argVal=""
    if [[ "$arg" =~ ^\- ]]; then
        # if the format is: -key=value
        if [[ "$arg" =~ \= ]]; then
            argVal=$(echo "$arg" | cut -d'=' -f2)
            argKey=$(echo "$arg" | cut -d'=' -f1)
            skipNext=0

        # if the format is: -key value
        elif [[ ! "$nextArg" =~ ^\- ]]; then
            argKey="$arg"
            argVal="$nextArg"
            skipNext=1

        # if the format is: -key (a boolean flag)
        elif [[ "$nextArg" =~ ^\- ]] || [[ -z "$nextArg" ]]; then
            argKey="$arg"
            argVal=""
            skipNext=0
        fi
    # if the format has not flag, just a value.
    else
        argKey=""
        argVal="$arg"
        skipNext=0
    fi

    case "$argKey" in 
        --source-scmurl)
            SOURCE_URL="$argVal"
        ;;
        --dest-scmurl)
            DEST_URL="$argVal"
        ;;
        --version-num)
            VERSION_NUM="$argVal"
        ;;
        -c|--clean)
            CLEAN_BEFORE_START="1"
        ;;
        -h|--help|-help|--h)
            showUsage
            exit
        ;;
    esac
done

8

Así es como lo hago en una función para evitar romper getopts run al mismo tiempo en algún lugar más alto en la pila:

function waitForWeb () {
   local OPTIND=1 OPTARG OPTION
   local host=localhost port=8080 proto=http
   while getopts "h:p:r:" OPTION; do
      case "$OPTION" in
      h)
         host="$OPTARG"
         ;;
      p)
         port="$OPTARG"
         ;;
      r)
         proto="$OPTARG"
         ;;
      esac
   done
...
}

8

Ampliando la respuesta de @ bruno-bronosky, agregué un "preprocesador" para manejar algunos formatos comunes:

  • Se expande --longopt=val en--longopt val
  • Se expande -xyz en-x -y -z
  • Apoya -- para indicar el final de las banderas
  • Muestra un error para opciones inesperadas
  • Interruptor de opciones compacto y fácil de leer.
#!/bin/bash

# Report usage
usage() {
  echo "Usage:"
  echo "$(basename $0) [options] [--] [file1, ...]"

  # Optionally exit with a status code
  if [ -n "$1" ]; then
    exit "$1"
  fi
}

invalid() {
  echo "ERROR: Unrecognized argument: $1" >&2
  usage 1
}

# Pre-process options to:
# - expand -xyz into -x -y -z
# - expand --longopt=arg into --longopt arg
ARGV=()
END_OF_OPT=
while [[ $# -gt 0 ]]; do
  arg="$1"; shift
  case "${END_OF_OPT}${arg}" in
    --) ARGV+=("$arg"); END_OF_OPT=1 ;;
    --*=*)ARGV+=("${arg%%=*}" "${arg#*=}") ;;
    --*) ARGV+=("$arg"); END_OF_OPT=1 ;;
    -*) for i in $(seq 2 ${#arg}); do ARGV+=("-${arg:i-1:1}"); done ;;
    *) ARGV+=("$arg") ;;
  esac
done

# Apply pre-processed options
set -- "${ARGV[@]}"

# Parse options
END_OF_OPT=
POSITIONAL=()
while [[ $# -gt 0 ]]; do
  case "${END_OF_OPT}${1}" in
    -h|--help)      usage 0 ;;
    -p|--password)  shift; PASSWORD="$1" ;;
    -u|--username)  shift; USERNAME="$1" ;;
    -n|--name)      shift; names+=("$1") ;;
    -q|--quiet)     QUIET=1 ;;
    -C|--copy)      COPY=1 ;;
    -N|--notify)    NOTIFY=1 ;;
    --stdin)        READ_STDIN=1 ;;
    --)             END_OF_OPT=1 ;;
    -*)             invalid "$1" ;;
    *)              POSITIONAL+=("$1") ;;
  esac
  shift
done

# Restore positional parameters
set -- "${POSITIONAL[@]}"

6

Hay varias formas de analizar los argumentos de cmdline (por ejemplo, GNU getopt (no portátil) vs BSD (OSX) getopt vs getopts), todos problemáticos. Esta solución es

  • ¡portátil!
  • tiene cero dependencias, solo se basa en las funciones integradas de bash
  • permite tanto opciones cortas como largas
  • maneja espacios en blanco entre la opción y el argumento, pero también puede usar =separador
  • admite estilo de opción corta concatenado -vxf
  • maneja la opción con argumentos opcionales (ver ejemplo), y
  • no requiere código hinchado en comparación con las alternativas para el mismo conjunto de características. Es decir, sucinto y, por lo tanto, más fácil de mantener.

Ejemplos: cualquiera de

# flag
-f
--foo

# option with required argument
-b"Hello World"
-b "Hello World"
--bar "Hello World"
--bar="Hello World"

# option with optional argument
--baz
--baz="Optional Hello"

#!/usr/bin/env bash

usage() {
  cat - >&2 <<EOF
NAME
    program-name.sh - Brief description

SYNOPSIS
    program-name.sh [-h|--help]
    program-name.sh [-f|--foo]
                    [-b|--bar <arg>]
                    [--baz[=<arg>]]
                    [--]
                    FILE ...

REQUIRED ARGUMENTS
  FILE ...
          input files

OPTIONS
  -h, --help
          Prints this and exits

  -f, --foo
          A flag option

  -b, --bar <arg>
          Option requiring an argument <arg>

  --baz[=<arg>]
          Option that has an optional argument <arg>. If <arg>
          is not specified, defaults to 'DEFAULT'
  --     
          Specify end of options; useful if the first non option
          argument starts with a hyphen

EOF
}

fatal() {
    for i; do
        echo -e "${i}" >&2
    done
    exit 1
}

# For long option processing
next_arg() {
    if [[ $OPTARG == *=* ]]; then
        # for cases like '--opt=arg'
        OPTARG="${OPTARG#*=}"
    else
        # for cases like '--opt arg'
        OPTARG="${args[$OPTIND]}"
        OPTIND=$((OPTIND + 1))
    fi
}

# ':' means preceding option character expects one argument, except
# first ':' which make getopts run in silent mode. We handle errors with
# wildcard case catch. Long options are considered as the '-' character
optspec=":hfb:-:"
args=("" "$@")  # dummy first element so $1 and $args[1] are aligned
while getopts "$optspec" optchar; do
    case "$optchar" in
        h) usage; exit 0 ;;
        f) foo=1 ;;
        b) bar="$OPTARG" ;;
        -) # long option processing
            case "$OPTARG" in
                help)
                    usage; exit 0 ;;
                foo)
                    foo=1 ;;
                bar|bar=*) next_arg
                    bar="$OPTARG" ;;
                baz)
                    baz=DEFAULT ;;
                baz=*) next_arg
                    baz="$OPTARG" ;;
                -) break ;;
                *) fatal "Unknown option '--${OPTARG}'" "see '${0} --help' for usage" ;;
            esac
            ;;
        *) fatal "Unknown option: '-${OPTARG}'" "See '${0} --help' for usage" ;;
    esac
done

shift $((OPTIND-1))

if [ "$#" -eq 0 ]; then
    fatal "Expected at least one required argument FILE" \
    "See '${0} --help' for usage"
fi

echo "foo=$foo, bar=$bar, baz=$baz, files=${@}"

5

Me gustaría ofrecer mi versión de análisis de opciones, que permite lo siguiente:

-s p1
--stage p1
-w somefolder
--workfolder somefolder
-sw p1 somefolder
-e=hello

También permite esto (podría ser no deseado):

-s--workfolder p1 somefolder
-se=hello p1
-swe=hello p1 somefolder

Debe decidir antes de usar si = se va a usar en una opción o no. Esto es para mantener el código limpio (ish).

while [[ $# > 0 ]]
do
    key="$1"
    while [[ ${key+x} ]]
    do
        case $key in
            -s*|--stage)
                STAGE="$2"
                shift # option has parameter
                ;;
            -w*|--workfolder)
                workfolder="$2"
                shift # option has parameter
                ;;
            -e=*)
                EXAMPLE="${key#*=}"
                break # option has been fully handled
                ;;
            *)
                # unknown option
                echo Unknown option: $key #1>&2
                exit 10 # either this: my preferred way to handle unknown options
                break # or this: do this to signal the option has been handled (if exit isn't used)
                ;;
        esac
        # prepare for next option in this key, if any
        [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
    done
    shift # option(s) fully processed, proceed to next input argument
done

1
¿Cuál es el significado de "+ x" en $ {key + x}?
Luca Davanzo

1
Es una prueba para ver si 'clave' está presente o no. Más abajo, desarmo la tecla y esto rompe el bucle while interno.
galmok

5

Solución que conserva argumentos no manejados. Demos incluidas.

Aquí está mi solución. Es MUY flexible y, a diferencia de otros, no debería requerir paquetes externos y maneja los argumentos sobrantes limpiamente.

El uso es: ./myscript -flag flagvariable -otherflag flagvar2

Todo lo que tiene que hacer es editar la línea de banderas válidas. Antepone un guión y busca todos los argumentos. Luego define el siguiente argumento como el nombre de la bandera, por ejemplo

./myscript -flag flagvariable -otherflag flagvar2
echo $flag $otherflag
flagvariable flagvar2

El código principal (versión corta, detallada con ejemplos más abajo, también una versión con error):

#!/usr/bin/env bash
#shebang.io
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
    for flag in $validflags
    do
        sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers

La versión detallada con demos de eco incorporadas:

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
echo "all args
$@"
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
#   argval=$(echo $@ | cut -d ' ' -f$count)
    for flag in $validflags
    do
            sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done

#Cleanup then restore the leftovers
echo "pre final clear args:
$@"
shift $#
echo "post final clear args:
$@"
set -- $leftovers
echo "all post set args:
$@"
echo arg1: $1 arg2: $2

echo leftovers: $leftovers
echo rate $rate time $time number $number

La última, esta falla si se pasa un argumento inválido.

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
validflags="rate time number"
count=1
for arg in $@
do
    argval=$1
    match=0
        if [ "${argval:0:1}" == "-" ]
    then
        for flag in $validflags
        do
                sflag="-"$flag
            if [ "$argval" == "$sflag" ]
            then
                declare $flag=$2
                match=1
            fi
        done
        if [ "$match" == "0" ]
        then
            echo "Bad argument: $argval"
            exit 1
        fi
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers
echo rate $rate time $time number $number
echo leftovers: $leftovers

Pros: lo que hace, se maneja muy bien. Conserva argumentos no utilizados que muchas de las otras soluciones aquí no tienen. También permite que se invoquen variables sin definirse manualmente en el script. También permite la prepoblación de variables si no se proporciona un argumento correspondiente. (Ver ejemplo detallado).

Contras: No se puede analizar una única cadena arg compleja, por ejemplo, -xcvf se procesaría como un solo argumento. Sin embargo, podría escribir un código adicional en el mío que agregue esta funcionalidad.



3

Tenga en cuenta que getopt(1) fue un breve error de vida de AT&T.

getopt fue creado en 1984 pero ya enterrado en 1986 porque no era realmente utilizable.

Una prueba del hecho de que getoptestá muy desactualizado es que la getopt(1)página de manual todavía menciona en "$*"lugar de "$@"eso, que se agregó a Bourne Shell en 1986 junto con elgetopts(1) shell incorporado para tratar los argumentos con espacios en el interior.

Por cierto: si está interesado en analizar opciones largas en scripts de shell, puede ser interesante saber que la getopt(3)implementación de libc (Solaris) y ksh93ambos agregaron una implementación uniforme de opciones largas que admite opciones largas como alias para opciones cortas. Esto causa ksh93y la Bourne Shellde implementar una interfaz uniforme durante largos opciones a través getopts.

Un ejemplo de opciones largas tomadas de la página de manual de Bourne Shell:

getopts "f:(file)(input-file)o:(output-file)" OPTX "$@"

muestra cuánto tiempo se pueden usar los alias de opción en Bourne Shell y ksh93.

Vea la página del manual de un Bourne Shell reciente:

http://schillix.sourceforge.net/man/man1/bosh.1.html

y la página del manual para getopt (3) de OpenSolaris:

http://schillix.sourceforge.net/man/man3c/getopt.3c.html

y por último, la página del comando man getopt (1) para verificar los $ * obsoletos:

http://schillix.sourceforge.net/man/man1/getopt.1.html


3

Escribí un bash helper para escribir una buena herramienta bash

inicio del proyecto: https://gitlab.mbedsys.org/mbedsys/bashopts

ejemplo:

#!/bin/bash -ei

# load the library
. bashopts.sh

# Enable backtrace dusplay on error
trap 'bashopts_exit_handle' ERR

# Initialize the library
bashopts_setup -n "$0" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc"

# Declare the options
bashopts_declare -n first_name -l first -o f -d "First name" -t string -i -s -r
bashopts_declare -n last_name -l last -o l -d "Last name" -t string -i -s -r
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name"
bashopts_declare -n age -l number -d "Age" -t number
bashopts_declare -n email_list -t string -m add -l email -d "Email adress"

# Parse arguments
bashopts_parse_args "$@"

# Process argument
bashopts_process_args

le dará ayuda:

NAME:
    ./example.sh - This is myapp tool description displayed on help message

USAGE:
    [options and commands] [-- [extra args]]

OPTIONS:
    -h,--help                          Display this help
    -n,--non-interactive true          Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false)
    -f,--first "John"                  First name - [$first_name] (type:string, default:"")
    -l,--last "Smith"                  Last name - [$last_name] (type:string, default:"")
    --display-name "John Smith"        Display name - [$display_name] (type:string, default:"$first_name $last_name")
    --number 0                         Age - [$age] (type:number, default:0)
    --email                            Email adress - [$email_list] (type:string, default:"")

disfruta :)


Obtengo esto en Mac OS X: `` `lib / bashopts.sh: línea 138: declare: -A: opción no válida declare: uso: declare [-afFirtx] [-p] [nombre [= valor] ...] Error en lib / bashopts.sh: 138. 'declare -x -A bashopts_optprop_name' salió con el estado 2 Árbol de llamadas: 1: lib / controller.sh: 4 source (...) Saliendo con el estado 1 `` `
Josh Wulf

Necesita Bash versión 4 para usar esto. En Mac, la versión predeterminada es 3. Puede usar home brew para instalar bash 4.
Josh Wulf

3

Aquí está mi enfoque: usar regexp.

  • no getopts
  • maneja bloque de parámetros cortos -qwerty
  • maneja parámetros cortos -q -w -e
  • maneja opciones largas --qwerty
  • puede pasar el atributo a la opción corta o larga (si está utilizando un bloque de opciones cortas, el atributo se adjunta a la última opción)
  • puede usar espacios o =para proporcionar atributos, pero las coincidencias de atributos hasta encontrar guión + espacio "delimitador", por lo que en--q=qwe ty qwe ty hay un atributo
  • maneja la mezcla de todo lo anterior, por lo que -o a -op attr ibute --option=att ribu te --op-tion attribute --option att-ributees válido

guión:

#!/usr/bin/env sh

help_menu() {
  echo "Usage:

  ${0##*/} [-h][-l FILENAME][-d]

Options:

  -h, --help
    display this help and exit

  -l, --logfile=FILENAME
    filename

  -d, --debug
    enable debug
  "
}

parse_options() {
  case $opt in
    h|help)
      help_menu
      exit
     ;;
    l|logfile)
      logfile=${attr}
      ;;
    d|debug)
      debug=true
      ;;
    *)
      echo "Unknown option: ${opt}\nRun ${0##*/} -h for help.">&2
      exit 1
  esac
}
options=$@

until [ "$options" = "" ]; do
  if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then
    if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute]
      opt=${BASH_REMATCH[3]}
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute]
      pile=${BASH_REMATCH[4]}
      while (( ${#pile} > 1 )); do
        opt=${pile:0:1}
        attr=""
        pile=${pile/${pile:0:1}/}
        parse_options
      done
      opt=$pile
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    else # leftovers that don't match
      opt=${BASH_REMATCH[10]}
      options=""
    fi
    parse_options
  fi
done

Como éste. Tal vez solo agregue -e param para hacer eco con una nueva línea.
mauron85

3

Supongamos que creamos un script de shell llamado de la test_args.shsiguiente manera

#!/bin/sh
until [ $# -eq 0 ]
do
  name=${1:1}; shift;
  if [[ -z "$1" || $1 == -* ]] ; then eval "export $name=true"; else eval "export $name=$1"; shift; fi  
done
echo "year=$year month=$month day=$day flag=$flag"

Después de ejecutar el siguiente comando:

sh test_args.sh  -year 2017 -flag  -month 12 -day 22 

El resultado sería:

year=2017 month=12 day=22 flag=true

55
Esto toma el mismo enfoque que la respuesta de Noah , pero tiene menos controles de seguridad / salvaguardas. Esto nos permite escribir argumentos arbitrarios en el entorno del script y estoy bastante seguro de que su uso de eval aquí puede permitir la inyección de comandos.
Will Barnwell


2

Mezcla de argumentos posicionales y basados ​​en banderas

--param = arg (igual delimitado)

Mezclar libremente banderas entre argumentos posicionales:

./script.sh dumbo 127.0.0.1 --environment=production -q -d
./script.sh dumbo --environment=production 127.0.0.1 --quiet -d

Se puede lograr con un enfoque bastante conciso:

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   param=${!pointer}
   if [[ $param != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      case $param in
         # paramter-flags with arguments
         -e=*|--environment=*) environment="${param#*=}";;
                  --another=*) another="${param#*=}";;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + 1)):$#} \
         || set -- ${@:((pointer + 1)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2

--param arg (espacio delimitado)

Por lo general, es más claro no mezclar --flag=valuey --flag valuepeinar.

./script.sh dumbo 127.0.0.1 --environment production -q -d

Es un poco difícil de leer, pero aún es válido.

./script.sh dumbo --environment production 127.0.0.1 --quiet -d

Fuente

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   if [[ ${!pointer} != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      param=${!pointer}
      ((pointer_plus = pointer + 1))
      slice_len=1

      case $param in
         # paramter-flags with arguments
         -e|--environment) environment=${!pointer_plus}; ((slice_len++));;
                --another) another=${!pointer_plus}; ((slice_len++));;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + $slice_len)):$#} \
         || set -- ${@:((pointer + $slice_len)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2

2

Aquí hay un getopts que logra el análisis con un código mínimo y le permite definir lo que desea extraer en un caso usando eval con subcadena.

Básicamente eval "local key='val'"

function myrsync() {

        local backup=("${@}") args=(); while [[ $# -gt 0 ]]; do k="$1";
                case "$k" in
                    ---sourceuser|---sourceurl|---targetuser|---targeturl|---file|---exclude|---include)
                        eval "local ${k:3}='${2}'"; shift; shift    # Past two arguments
                    ;;
                    *)  # Unknown option  
                        args+=("$1"); shift;                        # Past argument only
                    ;;                                              
                esac                                                
        done; set -- "${backup[@]}"                                 # Restore $@


        echo "${sourceurl}"
}

Declara las variables como locales en lugar de globales como la mayoría de las respuestas aquí.

Llamado:

myrsync ---sourceurl http://abc.def.g ---sourceuser myuser ... 

El $ {k: 3} es básicamente una subcadena para eliminar el primero ---de la clave.


1

Esto también puede ser útil para saber, puede establecer un valor y si alguien proporciona información, anule el valor predeterminado con ese valor.

myscript.sh -f ./serverlist.txt o simplemente ./myscript.sh (y toma los valores predeterminados)

    #!/bin/bash
    # --- set the value, if there is inputs, override the defaults.

    HOME_FOLDER="${HOME}/owned_id_checker"
    SERVER_FILE_LIST="${HOME_FOLDER}/server_list.txt"

    while [[ $# > 1 ]]
    do
    key="$1"
    shift

    case $key in
        -i|--inputlist)
        SERVER_FILE_LIST="$1"
        shift
        ;;
    esac
    done


    echo "SERVER LIST   = ${SERVER_FILE_LIST}"

1

Otra solución sin getopt [s], POSIX, antiguo estilo Unix

Similar a la solución que Bruno Bronosky publicó aquí es una sin el uso de getopt(s).

La principal característica diferenciadora de mi solución es que permite tener opciones concatenadas juntas como tar -xzf foo.tar.gzes igual a tar -x -z -f foo.tar.gz. Y al igual que en tar,ps etc. el guión principal es opcional para un bloque de opciones cortas (pero esto se puede cambiar fácilmente). Las opciones largas también son compatibles (pero cuando un bloque comienza con uno, se requieren dos guiones iniciales).

Código con opciones de ejemplo

#!/bin/sh

echo
echo "POSIX-compliant getopt(s)-free old-style-supporting option parser from phk@[se.unix]"
echo

print_usage() {
  echo "Usage:

  $0 {a|b|c} [ARG...]

Options:

  --aaa-0-args
  -a
    Option without arguments.

  --bbb-1-args ARG
  -b ARG
    Option with one argument.

  --ccc-2-args ARG1 ARG2
  -c ARG1 ARG2
    Option with two arguments.

" >&2
}

if [ $# -le 0 ]; then
  print_usage
  exit 1
fi

opt=
while :; do

  if [ $# -le 0 ]; then

    # no parameters remaining -> end option parsing
    break

  elif [ ! "$opt" ]; then

    # we are at the beginning of a fresh block
    # remove optional leading hyphen and strip trailing whitespaces
    opt=$(echo "$1" | sed 's/^-\?\([a-zA-Z0-9\?-]*\)/\1/')

  fi

  # get the first character -> check whether long option
  first_chr=$(echo "$opt" | awk '{print substr($1, 1, 1)}')
  [ "$first_chr" = - ] && long_option=T || long_option=F

  # note to write the options here with a leading hyphen less
  # also do not forget to end short options with a star
  case $opt in

    -)

      # end of options
      shift
      break
      ;;

    a*|-aaa-0-args)

      echo "Option AAA activated!"
      ;;

    b*|-bbb-1-args)

      if [ "$2" ]; then
        echo "Option BBB with argument '$2' activated!"
        shift
      else
        echo "BBB parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    c*|-ccc-2-args)

      if [ "$2" ] && [ "$3" ]; then
        echo "Option CCC with arguments '$2' and '$3' activated!"
        shift 2
      else
        echo "CCC parameters incomplete!" >&2
        print_usage
        exit 1
      fi
      ;;

    h*|\?*|-help)

      print_usage
      exit 0
      ;;

    *)

      if [ "$long_option" = T ]; then
        opt=$(echo "$opt" | awk '{print substr($1, 2)}')
      else
        opt=$first_chr
      fi
      printf 'Error: Unknown option: "%s"\n' "$opt" >&2
      print_usage
      exit 1
      ;;

  esac

  if [ "$long_option" = T ]; then

    # if we had a long option then we are going to get a new block next
    shift
    opt=

  else

    # if we had a short option then just move to the next character
    opt=$(echo "$opt" | awk '{print substr($1, 2)}')

    # if block is now empty then shift to the next one
    [ "$opt" ] || shift

  fi

done

echo "Doing something..."

exit 0

Para el uso de ejemplo, vea los ejemplos más abajo.

Posición de opciones con argumentos

Por lo que vale, las opciones con argumentos no son las últimas (solo las opciones largas deben ser). Entonces, por ejemplo, en tar(al menos en algunas implementaciones) las fopciones deben ser las últimas porque el nombre del archivo sigue ( tar xzf bar.tar.gzfunciona pero tar xfz bar.tar.gzno funciona) este no es el caso aquí (ver los ejemplos posteriores).

Múltiples opciones con argumentos

Como otra ventaja, los parámetros de las opciones se consumen en el orden de las opciones por los parámetros con las opciones requeridas. Solo mira la salida de mi script aquí con la línea de comando abc X Y Z(o -abc X Y Z):

Option AAA activated!
Option BBB with argument 'X' activated!
Option CCC with arguments 'Y' and 'Z' activated!

Las opciones largas también se concatenan

Además, también puede tener opciones largas en el bloque de opciones dado que ocurren en último lugar en el bloque. Por lo tanto, las siguientes líneas de comando son todas equivalentes (incluido el orden en que se procesan las opciones y sus argumentos):

  • -cba Z Y X
  • cba Z Y X
  • -cb-aaa-0-args Z Y X
  • -c-bbb-1-args Z Y X -a
  • --ccc-2-args Z Y -ba X
  • c Z Y b X a
  • -c Z Y -b X -a
  • --ccc-2-args Z Y --bbb-1-args X --aaa-0-args

Todos estos conducen a:

Option CCC with arguments 'Z' and 'Y' activated!
Option BBB with argument 'X' activated!
Option AAA activated!
Doing something...

No en esta solución

Argumentos opcionales

Las opciones con argumentos opcionales deberían ser posibles con un poco de trabajo, por ejemplo, mirando hacia adelante si hay un bloque sin guión; el usuario necesitaría poner un guión delante de cada bloque después de un bloque con un parámetro que tenga un parámetro opcional. Tal vez esto sea demasiado complicado para comunicarse con el usuario, por lo que es mejor que solo requiera un guión principal en este caso.

Las cosas se vuelven aún más complicadas con múltiples parámetros posibles. Aconsejaría no hacer que las opciones intenten ser inteligentes al determinar si un argumento puede ser adecuado o no (por ejemplo, con una opción solo toma un número como argumento opcional) porque esto podría romperse en el futuro.

Personalmente, prefiero opciones adicionales en lugar de argumentos opcionales.

Opción argumentos introducidos con un signo igual

Al igual que con los argumentos opcionales, no soy fanático de esto (por cierto, ¿hay un hilo para discutir los pros / contras de los diferentes estilos de parámetros?) Pero si lo desea, probablemente podría implementarlo usted mismo como lo hizo en http: // mywiki.wooledge.org/BashFAQ/035#Manual_loop con un --long-with-arg=?*enunciado de caso y luego quitando el signo igual (esto es, por cierto, el sitio que dice que es posible realizar la concatenación de parámetros con cierto esfuerzo, pero "lo dejó como un ejercicio para el lector "lo que me hizo tomarles la palabra pero empecé desde cero".

Otras notas

Compatible con POSIX, funciona incluso en configuraciones antiguas de Busybox con las que tuve que lidiar (por ejemplo cut, heady getoptsfalta).

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.