¿Cómo analizo los argumentos de la línea de comando en Bash?
Digamos que 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 manera que en cada caso (o alguna combinación de los dos) $v
, $f
y $d
se establezcan en true
y $outFile
sean iguales a /fizz/someOtherFile
?
Bash separado por espacios (por ejemplo, --option argument
)
cat >/tmp/demo-space-separated.sh <<'EOF'
#!/bin/bash
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
-e|--extension)
EXTENSION="$2"
shift # past argument
shift # past value
;;
-s|--searchpath)
SEARCHPATH="$2"
shift # past argument
shift # past value
;;
--default)
DEFAULT=YES
shift # past argument
;;
-*|--*)
echo "Unknown option $1"
exit 1
;;
*)
POSITIONAL_ARGS+=("$1") # save positional arg
shift # past argument
;;
esac
done
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
echo "FILE EXTENSION = ${EXTENSION}"
echo "SEARCH PATH = ${SEARCHPATH}"
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 /etc/hosts
Resultado de copiar y pegar el bloque de arriba
FILE EXTENSION = conf
SEARCH PATH = /etc
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
Uso
demo-space-separated.sh -e conf -s /etc /etc/hosts
Bash separados por iguales (por ejemplo, --option=argument
)
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
;;
--default)
DEFAULT=YES
shift # past argument with no value
;;
-*|--*)
echo "Unknown option $i"
exit 1
;;
*)
;;
esac
done
echo "FILE EXTENSION = ${EXTENSION}"
echo "SEARCH PATH = ${SEARCHPATH}"
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 /etc/hosts
Resultado de copiar y pegar el bloque de arriba
FILE EXTENSION = conf
SEARCH PATH = /etc
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
Uso
demo-equals-separated.sh -e=conf -s=/etc /etc/hosts
Para comprender mejor, ${i#*=}
busque "Eliminación de subcadenas" en esta guía . Es funcionalmente equivalente a `sed 's/[^=]*=//' <<< "$i"`
que llama a un subproceso innecesario o `echo "$i" | sed 's/[^=]*=//'`
que llama a dos subprocesos innecesarios.
Usando bash con getopt[s]
Limitaciones de getopt(1) (versiones más antiguas y relativamente recientes getopt
):
- no puedo manejar argumentos que son cadenas vacías
- no puedo manejar argumentos con espacios en blanco incrustados
Las versiones más recientes getopt
no tienen estas limitaciones. Para obtener más información, consulte estos documentos .
opciones POSIX
Además, el shell POSIX y otras ofertas getopts
no tienen estas limitaciones. He incluido un getopts
ejemplo simplista.
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
Resultado de copiar y pegar el bloque de arriba
verbose=1, output_file='/etc/hosts', Leftovers: foo bar
Uso
demo-getopts.sh -vf /etc/hosts foo bar
Las ventajas de getopts
son:
- Es más portátil y funcionará en otros shells como
dash
. - Puede manejar múltiples opciones únicas, como
-vf filename
en la forma típica de Unix, de forma automática.
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 qué significan todas las sintaxis y variables. En bash, también hay help getopts
, que puede ser informativo.
Ninguna respuesta muestra getopt mejorado . Y la respuesta más votada es engañosa: ignora -vfd
las opciones cortas de estilo (solicitadas por el OP) u opciones después de argumentos posicionales (también solicitadas por el OP); e ignora los errores de análisis. En cambio:
- Uso mejorado
getopt
de util-linux o anteriormente GNU glibc . 1 - Funciona con
getopt_long()
la función C de GNU glibc. - ninguna otra solución en esta página puede hacer todo esto :
- maneja espacios, cita caracteres e incluso binarios en los argumentos 2 (los no mejorados
getopt
no pueden hacer esto) - puede manejar opciones al final:
script.sh -o outFile file1 file2 -v
(getopts
no hace esto) - permite
=
opciones largas de estilo:script.sh --outfile=fileOut --infile fileIn
(permitir ambas es largo si se analiza automáticamente) - permite opciones cortas combinadas, por ejemplo
-vfd
(trabajo real si se analiza automáticamente) - permite tocar argumentos de opciones, por ejemplo
-oOutfile
o-vfdoOutfile
- maneja espacios, cita caracteres e incluso binarios en los argumentos 2 (los no mejorados
- Ya es tan antiguo que viene preinstalado en cualquier sistema GNU (es decir, Linux principalmente); ver nota al pie 1
- Puede probar su existencia con:
getopt --test
→ valor de retorno 4. - Otros
getopt
o integrados en shellgetopts
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
todos regresan
verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile
con lo siguientemyscript
#!/bin/bash
# More safety, by turning some bugs into errors.
# Without `errexit` you don’t need ! and can replace
# ${PIPESTATUS[0]} with a simple $?, but I prefer safety.
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
# option --output/-o requires 1 argument
LONGOPTS=debug,force,output:,verbose
OPTIONS=dfo:v
# -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, intentebrew install gnu-getopt
, brew install util-linux
osudo port install getopt
2, las convenciones POSIXexec()
no tienen una forma confiable de pasar NULL binario en los argumentos de la línea de comando; esos bytes terminan prematuramente la primera versión del argumento
3 lanzada en 1997 o antes (solo la rastreé hasta 1997)
desplegar.sh
#!/bin/bash
while [[ "$#" -gt 0 ]]; do
case $1 in
-t|--target) target="$2"; shift ;;
-u|--uglify) uglify=1 ;;
*) echo "Unknown parameter passed: $1"; exit 1 ;;
esac
shift
done
echo "Where to deploy: $target"
echo "Should uglify : $uglify"
Uso:
./deploy.sh -t dev -u
# OR:
./deploy.sh --target dev --uglify
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 llama a un subproceso innecesario o `echo "$i" | sed 's/[^=]*=//'`
que llama a dos subprocesos innecesarios.
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
Esta solución:
- manijas
-n arg
y--name=arg
- permite argumentos al final
- muestra errores sensatos si algo está mal escrito
- compatible, no usa bashismos
- legible, no requiere mantener el estado en un bucle