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

Diga, 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 analizar esto de forma tal que en cada caso (o una combinación de ambos) $v , $f y $d se establecerán en true y $outFile será igual a /fizz/someOtherFile ?

Método n. ° 1: Usar bash sin getopt [s]

Dos formas comunes de pasar los pares clave-valor-par son:

Bash Space-Separated (p. Ej., --option argument ) (sin getopt [s])

Uso ./myscript.sh -e conf -s /etc -l /usr/lib /etc/hosts

 #!/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[@]}" # restre 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 

Bash Equals-Separated (p. Ej., --option=argument ) (sin getopt [s])

Uso ./myscript.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

 #!/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 "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 

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

Método n. ° 2: usar bash con getopt [s]

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

limitaciones de getopt (1) (versiones antiguas, relativamente recientes de getopt ):

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

Las versiones más recientes de getopt no tienen estas limitaciones.

Además, el shell POSIX (y otros) ofrecen getopts que no tiene estas limitaciones. Aquí hay un ejemplo simplista de getopts :

 #!/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: $@" # End of file 

Las ventajas de getopts son:

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

La desventaja de getopts es que solo puede manejar opciones cortas ( -h , no --help ) sin código adicional.

Hay un tutorial de getopts que explica lo que significan todas las syntax y variables. En bash, también hay help getopts , que pueden ser informativos.

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

  • Use getopt mejorado de util-linux o glibc GNU anteriormente . 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
    • Puede manejar las opciones al final: script.sh -o outFile file1 file2 -v
    • permite opciones largas de estilo = : script.sh --outfile=fileOut --infile fileIn
  • Ya es tan viejo 3 que no falta ningún sistema GNU (por ejemplo, cualquier Linux lo tiene).
  • Puedes probar su existencia con: getopt --test → return value 4.
  • Otros getopt o getopts son 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 regresa

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

con el siguiente myscript

 #!/bin/bash # saner programming env: these switches turn some bugs into errors set -o errexit -o pipefail -o noclobber -o nounset ! 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 # -use ! and PIPESTATUS to get exit code with errexit set # -temporarily store output to be able to check for errors # -activate quoting/enhanced mode (eg 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 # eg 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=nf=nv=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 try brew install gnu-getopt o sudo port install getopt
2 las convenciones POSIX exec() no tienen una manera confiable de pasar NULL binario en argumentos de línea de comando; esos bytes terminan prematuramente el argumento
3 primera versión lanzada en 1997 o antes (solo la rastreé hasta 1997)

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 `sed 's/[^=]*=//' <<< "$i"` que invoca un subproceso innecesario o `echo "$i" | sed 's/[^=]*=//'` `echo "$i" | sed 's/[^=]*=//'` que llama a dos subprocesos innecesarios.

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

El uso simple de “getopt” se muestra en este mini guión:

 #!/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 -c es seguido por un argumento (la “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ó “-“.

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

  • maneja -n arg y --name=arg
  • permite argumentos al final
  • muestra errores correctos 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 

De manera más sucinta

script.sh

 #!/bin/bash while [[ "$#" > 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 

Tengo aproximadamente 4 años de retraso en esta pregunta, pero quiero devolver algo. Utilicé las respuestas anteriores como punto de partida para ordenar mi antiguo análisis de parámtero adhoc. Luego refactoreé el siguiente código de plantilla. Maneja params largos y cortos, usando = o argumentos separados por espacios, así como múltiples params 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 

Mi respuesta se basa en gran medida en la respuesta de Bruno Bronosky , pero de alguna manera aplasté 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 puedas usar en bash

    • Significado -a o -arg o –arg o -arg o lo que sea
  • Bash puro. No es necesario aprender / usar getopt o getopts

CONTRAS:

  • No se puede combinar argumentos

    • Es decir, no -abc. Debes hacer -a -b -c

Estos son los únicos pros / contras que puedo pensar en la parte superior de mi cabeza

He encontrado la cuestión de escribir un análisis portátil en secuencias de comandos 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 además de que tiene algunas características interesantes :

https://argbash.io

Creo que este es lo suficientemente simple de 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 

Ampliando la excelente respuesta de @guneysus, aquí hay una modificación que le permite al usuario usar la syntax 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.

Esta “interpretación difusa” podría no ser de su agrado, pero si está haciendo guiones que son intercambiables con otras utilidades (como es el caso del 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 

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 

Así es como lo hago en una función para evitar que getopts se rompan al mismo tiempo en algún lugar más alto en la stack:

 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 ... } 

Te doy la función parse_params que 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 scope global.
  3. Le devuelve sin esfuerzo variables fáciles de usar, sobre las cuales puede construir más lógica.
  4. La cantidad de guiones antes de params no importa ( --all iguales -all iguales a all=all )

El siguiente script es una demostración de trabajo de copiar y pegar. Consulte la función show_use para comprender cómo usar parse_params .

Limitaciones

  1. No admite parámetros delimitados por espacios ( -d 1 )
  2. Los nombres --any-param perderán guiones entonces --any-param y -anyparam son 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 # Parses equal sign separated params into local variables (--name=bob creates variable $name=="bob") # 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) # Parses un-named params into ${ARGV[*]} array # Additionally puts all named params raw into ${ARGN[*]} array # Additionally puts all standalone "option" params raw into ${ARGO[*]} array # @author Oleksii Chekulaiev # @version v1.4 (Jun-26-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 _escaped=${1/\*/\'\"*\"\'} # If equals delimited named parameter if [[ "$1" =~ ^..*=..* ]]; then # Add to named parameters array echo "ARGN+=('$_escaped');" # key is part before first = local _key=$(echo "$1" | cut -d = -f 1) # 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 if [[ "$_key" == "" ]]; then shift continue fi # 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" =~ ^\-. ]]; then # remove dashes local _key=${1//\-} # skip when key is empty if [[ "$_key" == "" ]]; then 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 

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 gustaría ofrecer mi versión del 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 utilizar 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 

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

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

Una prueba del hecho de que getopt está muy desactualizado es que la página man de getopt(1) aún menciona "$*" lugar de "$@" , que se agregó a Bourne Shell en 1986 junto con getopts(1) shell incorporado para tratar con argumentos dentro de espacios.

Por cierto: si está interesado en analizar opciones largas en scripts de shell, puede ser interesante saber que la implementación getopt(3) de libc (Solaris) y ksh93 agregaron una implementación de opción larga uniforme que admite opciones largas como alias para abreviar opciones. Esto hace que ksh93 y Bourne Shell implementen una interfaz uniforme para opciones largas a través de getopts .

Un ejemplo para las opciones largas tomadas de la página man de Bourne Shell:

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

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

Consulte la página de manual de una Bourne Shell reciente:

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

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

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

y, por último, la página man de getopt (1) para verificar el $ * obsoleto:

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

Mezcla de argumentos posicionales y basados ​​en banderas

–param = arg (igual a delimitado)

Mezcla 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 los --flag=value y --flag value .

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

Esto es un poco arriesgado de leer, pero sigue siendo 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 

Assume we create a shell script named test_args.sh as follow

 #!/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" 

After we run the following command:

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

The output would be:

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

Use module “arguments” from bash-modules

Ejemplo:

 #!/bin/bash . import.sh log arguments NAME="world" parse_arguments "-n|--name)NAME;S" -- "$@" || { error "Cannot parse command line." exit 1 } info "Hello, $NAME!" 

This also might be useful to know, you can set a value and if someone provides input, override the default with that value..

myscript.sh -f ./serverlist.txt or just ./myscript.sh (and it takes defaults)

  #!/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}" 

Another solution without getopt[s], POSIX, old Unix style

Similar to the solution Bruno Bronosky posted this here is one without the usage of getopt(s) .

Main differentiating feature of my solution is that it allows to have options concatenated together just like tar -xzf foo.tar.gz is equal to tar -x -z -f foo.tar.gz . And just like in tar , ps etc. the leading hyphen is optional for a block of short options (but this can be changed easily). Long options are supported as well (but when a block starts with one then two leading hyphens are required).

Code with example options

 #!/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 

For the example usage please see the examples further below.

Position of options with arguments

For what its worth there the options with arguments don’t be the last (only long options need to be). So while eg in tar (at least in some implementations) the f options needs to be last because the file name follows ( tar xzf bar.tar.gz works but tar xfz bar.tar.gz does not) this is not the case here (see the later examples).

Multiple options with arguments

As another bonus the option parameters are consumed in the order of the options by the parameters with required options. Just look at the output of my script here with the command line abc XYZ (or -abc XYZ ):

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

Long options concatenated as well

Also you can also have long options in option block given that they occur last in the block. So the following command lines are all equivalent (including the order in which the options and its arguments are being processed):

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

All of these lead to:

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

Not in this solution

Optional arguments

Options with optional arguments should be possible with a bit of work, eg by looking forward whether there is a block without a hyphen; the user would then need to put a hyphen in front of every block following a block with a parameter having an optional parameter. Maybe this is too complicated to communicate to the user so better just require a leading hyphen altogether in this case.

Things get even more complicated with multiple possible parameters. I would advise against making the options trying to be smart by determining whether the an argument might be for it or not (eg with an option just takes a number as an optional argument) because this might break in the future.

I personally favor additional options instead of optional arguments.

Option arguments introduced with an equal sign

Just like with optional arguments I am not a fan of this (BTW, is there a thread for discussing the pros/cons of different parameter styles?) but if you want this you could probably implement it yourself just like done at http://mywiki.wooledge.org/BashFAQ/035#Manual_loop with a --long-with-arg=?* case statement and then stripping the equal sign (this is BTW the site that says that making parameter concatenation is possible with some effort but “left [it] as an exercise for the reader” which made me take them at their word but I started from scratch).

Otras notas

POSIX-compliant, works even on ancient Busybox setups I had to deal with (with eg cut , head and getopts missing).

Solution that preserves unhandled arguments. Demos Included.

Aquí está mi solución. It is VERY flexible and unlike others, shouldn’t require external packages and handles leftover arguments cleanly.

Usage is: ./myscript -flag flagvariable -otherflag flagvar2

All you have to do is edit the validflags line. It prepends a hyphen and searches all arguments. It then defines the next argument as the flag name eg

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

The main code (short version, verbose with examples further down, also a version with erroring out):

 #!/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 restre the leftovers shift $# set -- $leftovers 

The verbose version with built in echo demos:

 #!/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 restre 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 

Final one, this one errors out if an invalid -argument is passed through.

 #!/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 restre the leftovers shift $# set -- $leftovers echo rate $rate time $time number $number echo leftovers: $leftovers 

Pros: What it does, it handles very well. It preserves unused arguments which a lot of the other solutions here don’t. It also allows for variables to be called without being defined by hand in the script. It also allows prepopulation of variables if no corresponding argument is given. (See verbose example).

Cons: Can’t parse a single complex arg string eg -xcvf would process as a single argument. You could somewhat easily write additional code into mine that adds this functionality though.

The top answer to this question seemed a bit buggy when I tried it — here’s my solution which I’ve found to be more robust:

 boolean_arg="" arg_with_value="" while [[ $# -gt 0 ]] do key="$1" case $key in -b|--boolean-arg) boolean_arg=true shift ;; -a|--arg-with-value) arg_with_value="$2" shift shift ;; -*) echo "Unknown option: $1" exit 1 ;; *) arg_num=$(( $arg_num + 1 )) case $arg_num in 1) first_normal_arg="$1" shift ;; 2) second_normal_arg="$1" shift ;; *) bad_args=TRUE esac ;; esac done # Handy to have this here when adding arguments to # see if they're working. Just edit the '0' to be '1'. if [[ 0 == 1 ]]; then echo "first_normal_arg: $first_normal_arg" echo "second_normal_arg: $second_normal_arg" echo "boolean_arg: $boolean_arg" echo "arg_with_value: $arg_with_value" exit 0 fi if [[ $bad_args == TRUE || $arg_num < 2 ]]; then echo "Usage: $(basename "$0")   [--boolean-arg] [--arg-with-value VALUE]" exit 1 fi 

This example shows how to use getopt and eval and HEREDOC and shift to handle short and long parameters with and without a required value that follows. Also the switch/case statement is concise and easy to follow.

 #!/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, don't 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 

The most significant lines of the script above are these:

 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 

Short, to the point, readable, and handles just about everything (IMHO).

Espero que ayude a alguien.

I have write a bash helper to write a nice bash tool

project home: 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 -of -d "First name" -t string -i -s -r bashopts_declare -n last_name -l last -ol -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 

will give help:

 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:"") 

enjoy 🙂

Here is my approach – using regexp.

  • no getopts
  • it handles block of short parameters -qwerty
  • it handles short parameters -q -w -e
  • it handles long options --qwerty
  • you can pass attribute to short or long option (if you are using block of short options, attribute is attached to the last option)
  • you can use spaces or = to provide attributes, but attribute matches until encountering hyphen+space “delimiter”, so in --q=qwe ty qwe ty is one attribute
  • it handles mix of all above so -oa -op attr ibute --option=att ribu te --op-tion attribute --option att-ribute is valid

script:

 #!/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 

Here is my improved solution of Bruno Bronosky’s answer using variable arrays.

it lets you mix parameters position and give you a parameter array preserving the order without the options

 #!/bin/bash echo $@ PARAMS=() SOFT=0 SKIP=() for i in "$@" do case $i in -n=*|--skip=*) SKIP+=("${i#*=}") ;; -s|--soft) SOFT=1 ;; *) # unknown option PARAMS+=("$i") ;; esac done echo "SKIP = ${SKIP[@]}" echo "SOFT = $SOFT" echo "Parameters:" echo ${PARAMS[@]} 

Will output for example:

 $ ./test.sh parameter -s somefile --skip=.c --skip=.obj parameter -s somefile --skip=.c --skip=.obj SKIP = .c .obj SOFT = 1 Parameters: parameter somefile