bash - Opción opcional argumento con getopts
(8)
while getopts "hd:R:" arg; do
case $arg in
h)
echo "usgae"
;;
d)
dir=$OPTARG
;;
R)
if [[ $OPTARG =~ ^[0-9]+$ ]];then
level=$OPTARG
else
level=1
fi
;;
/?)
echo "WRONG" >&2
;;
esac
done
nivel se refiere al parámetro de
-R
, dir se refiere a los parámetros de-d
cuando ingrese
./count.sh -R 1 -d test/
funciona correctamentecuando ingrese
./count.sh -d test/ -R 1
funciona correctamentepero quiero que funcione cuando ingrese
./count.sh -d test/ -R
o./count.sh -R -d test/
Esto significa que quiero que -R
tenga un valor predeterminado y la secuencia de comandos podría ser más flexible.
El siguiente código resuelve este problema al buscar un guión inicial y, si se encuentra, disminuye OPTIND para volver a apuntar a la opción omitida para su procesamiento. Esto generalmente funciona bien, excepto que no conoce el orden en que el usuario colocará las opciones en la línea de comandos; si su opción de argumento opcional es la última y no proporciona un argumento, getopts querrá realizar un error.
Para solucionar el problema del argumento final que falta, la matriz "$ @" simplemente tiene una cadena vacía "$ @" anexada para que Getopts esté satisfecho de que se ha tragado otro argumento de opción. Para corregir este nuevo argumento vacío, se establece una variable que contiene el recuento total de todas las opciones que se procesarán: cuando se procesa la última opción, se llama a una función auxiliar llamada recortar y elimina la cadena vacía antes del valor que se está utilizando.
Este no es un código de trabajo, solo tiene titulares, pero puede modificarlo fácilmente y con un poco de cuidado puede ser útil para construir un sistema robusto.
#!/usr/bin/env bash
declare -r CHECK_FLOAT="%f"
declare -r CHECK_INTEGER="%i"
## <arg 1> Number - Number to check
## <arg 2> String - Number type to check
## <arg 3> String - Error message
function check_number() {
local NUMBER="${1}"
local NUMBER_TYPE="${2}"
local ERROR_MESG="${3}"
local FILTERED_NUMBER=$(sed ''s/[^.e0-9+/^]//g'' <<< "${NUMBER}")
local -i PASS=1
local -i FAIL=0
if [[ -z "${NUMBER}" ]]; then
echo "Empty number argument passed to check_number()." 1>&2
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
elif [[ -z "${NUMBER_TYPE}" ]]; then
echo "Empty number type argument passed to check_number()." 1>&2
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
elif [[ ! "${#NUMBER}" -eq "${#FILTERED_NUMBER}" ]]; then
echo "Non numeric characters found in number argument passed to check_number()." 1>&2
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
else
case "${NUMBER_TYPE}" in
"${CHECK_FLOAT}")
if ((! $(printf "${CHECK_FLOAT}" "${NUMBER}" &>/dev/random;echo $?))); then
echo "${PASS}"
else
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
fi
;;
"${CHECK_INTEGER}")
if ((! $(printf "${CHECK_INTEGER}" "${NUMBER}" &>/dev/random;echo $?))); then
echo "${PASS}"
else
echo "${ERROR_MESG}" 1>&2
echo "${FAIL}"
fi
;;
*)
echo "Invalid number type format: ${NUMBER_TYPE} to check_number()." 1>&2
echo "${FAIL}"
;;
esac
fi
}
## Note: Number can be any printf acceptable format and includes leading quotes and quotations,
## and anything else that corresponds to the POSIX specification.
## E.g. "''1e+03" is valid POSIX float format, see http://mywiki.wooledge.org/BashFAQ/054
## <arg 1> Number - Number to print
## <arg 2> String - Number type to print
function print_number() {
local NUMBER="${1}"
local NUMBER_TYPE="${2}"
case "${NUMBER_TYPE}" in
"${CHECK_FLOAT}")
printf "${CHECK_FLOAT}" "${NUMBER}" || echo "Error printing Float in print_number()." 1>&2
;;
"${CHECK_INTEGER}")
printf "${CHECK_INTEGER}" "${NUMBER}" || echo "Error printing Integer in print_number()." 1>&2
;;
*)
echo "Invalid number type format: ${NUMBER_TYPE} to print_number()." 1>&2
;;
esac
}
## <arg 1> String - String to trim single ending whitespace from
function trim_string() {
local STRING="${1}"
echo -En $(sed ''s/ $//'' <<< "${STRING}") || echo "Error in trim_string() expected a sensible string, found: ${STRING}" 1>&2
}
## This a hack for getopts because getopts does not support optional
## arguments very intuitively. E.g. Regardless of whether the values
## begin with a dash, getopts presumes that anything following an
## option that takes an option argument is the option argument. To fix
## this the index variable OPTIND is decremented so it points back to
## the otherwise skipped value in the array option argument. This works
## except for when the missing argument is on the end of the list,
## in this case getopts will not have anything to gobble as an
## argument to the option and will want to error out. To avoid this an
## empty string is appended to the argument array, yet in so doing
## care must be taken to manage this added empty string appropriately.
## As a result any option that doesn''t exit at the time its processed
## needs to be made to accept an argument, otherwise you will never
## know if the option will be the last option sent thus having an empty
## string attached and causing it to land in the default handler.
function process_options() {
local OPTIND OPTERR=0 OPTARG OPTION h d r s M R S D
local ERROR_MSG=""
local OPTION_VAL=""
local EXIT_VALUE=0
local -i NUM_OPTIONS
let NUM_OPTIONS=${#@}+1
while getopts “:h?d:DM:R:S:s:r:” OPTION "$@";
do
case "$OPTION" in
h)
help | more
exit 0
;;
r)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG="Invalid input: Integer or floating point number required."
if [[ -z "${OPTION_VAL}" ]]; then
## can set global flags here
:;
elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
let OPTIND=${OPTIND}-1
## can set global flags here
elif [ "${OPTION_VAL}" = "0" ]; then
## can set global flags here
:;
elif (($(check_number "${OPTION_VAL}" "${CHECK_FLOAT}" "${ERROR_MSG}"))); then
:; ## do something really useful here..
else
echo "${ERROR_MSG}" 1>&2 && exit -1
fi
;;
d)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
[[ ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]] && let OPTIND=${OPTIND}-1
DEBUGMODE=1
set -xuo pipefail
;;
s)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
if [[ ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]]; then ## if you want a variable value that begins with a dash, escape it
let OPTIND=${OPTIND}-1
else
GLOBAL_SCRIPT_VAR="${OPTION_VAL}"
:; ## do more important things
fi
;;
M)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG=$(echo "Error - Invalid input: ${OPTION_VAL}, Integer required"/
"retry with an appropriate option argument.")
if [[ -z "${OPTION_VAL}" ]]; then
echo "${ERROR_MSG}" 1>&2 && exit -1
elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
let OPTIND=${OPTIND}-1
echo "${ERROR_MSG}" 1>&2 && exit -1
elif (($(check_number "${OPTION_VAL}" "${CHECK_INTEGER}" "${ERROR_MSG}"))); then
:; ## do something useful here
else
echo "${ERROR_MSG}" 1>&2 && exit -1
fi
;;
R)
OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG=$(echo "Error - Invalid option argument: ${OPTION_VAL},"/
"the value supplied to -R is expected to be a "/
"qualified path to a random character device.")
if [[ -z "${OPTION_VAL}" ]]; then
echo "${ERROR_MSG}" 1>&2 && exit -1
elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
let OPTIND=${OPTIND}-1
echo "${ERROR_MSG}" 1>&2 && exit -1
elif [[ -c "${OPTION_VAL}" ]]; then
:; ## Instead of erroring do something useful here..
else
echo "${ERROR_MSG}" 1>&2 && exit -1
fi
;;
S)
STATEMENT=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
ERROR_MSG="Error - Default text string to set cannot be empty."
if [[ -z "${STATEMENT}" ]]; then
## Instead of erroring you could set a flag or do something else with your code here..
elif [[ "${STATEMENT}" =~ ^-. ]]; then ## if you want a statement that begins with a dash, escape it
let OPTIND=${OPTIND}-1
echo "${ERROR_MSG}" 1>&2 && exit -1
echo "${ERROR_MSG}" 1>&2 && exit -1
else
:; ## do something even more useful here you can modify the above as well
fi
;;
D)
## Do something useful as long as it is an exit, it is okay to not worry about the option arguments
exit 0
;;
*)
EXIT_VALUE=-1
;&
?)
usage
exit ${EXIT_VALUE}
;;
esac
done
}
process_options "$@ " ## extra space, so getopts can find arguments
Esta solución define ''R'' sin argumento (no '':''), prueba cualquier argumento después de ''-R'' (administrar la última opción en la línea de comando) y comprueba si un argumento existente comienza con un guión.
# No : after R
while getopts "hd:R" arg; do
case $arg in
(...)
R)
# Check next positional parameter
eval nextopt=/${$OPTIND}
# existing or starting with dash?
if [[ -n $nextopt && $nextopt != -* ]] ; then
OPTIND=$((OPTIND + 1))
level=$nextopt
else
level=1
fi
;;
(...)
esac
done
Esto es realmente bastante fácil. Simplemente suelte los dos puntos tras la R y use OPTIND
while getopts "hRd:" opt; do
case $opt in
h) echo -e $USAGE && exit
;;
d) DIR="$OPTARG"
;;
R)
if [[ ${@:$OPTIND} =~ ^[0-9]+$ ]];then
LEVEL=${@:$OPTIND}
OPTIND=$((OPTIND+1))
else
LEVEL=1
fi
;;
/?) echo "Invalid option -$OPTARG" >&2
;;
esac
done
echo $LEVEL $DIR
count.sh -d prueba
prueba
count.sh -d prueba -R
1 prueba
count.sh -R -d prueba
1 prueba
count.sh -d prueba -R 2
2 pruebas
count.sh -R 2 -d prueba
2 pruebas
Estoy de acuerdo con tripleee, getopts no admite el manejo opcional de argumentos.
La solución comprometida en la que me he conformado es usar la combinación de mayúsculas y minúsculas del mismo indicador de opción para diferenciar entre la opción que toma un argumento y la otra que no lo hace.
Ejemplo:
COMMAND_LINE_OPTIONS_HELP=''
Command line options:
-I Process all the files in the default dir: ''`pwd`''/input/
-i DIR Process all the files in the user specified input dir
-h Print this help menu
Examples:
Process all files in the default input dir
''`basename $0`'' -I
Process all files in the user specified input dir
''`basename $0`'' -i ~/my/input/dir
''
VALID_COMMAND_LINE_OPTIONS="i:Ih"
INPUT_DIR=
while getopts $VALID_COMMAND_LINE_OPTIONS options; do
#echo "option is " $options
case $options in
h)
echo "$COMMAND_LINE_OPTIONS_HELP"
exit $E_OPTERROR;
;;
I)
INPUT_DIR=`pwd`/input
echo ""
echo "***************************"
echo "Use DEFAULT input dir : $INPUT_DIR"
echo "***************************"
;;
i)
INPUT_DIR=$OPTARG
echo ""
echo "***************************"
echo "Use USER SPECIFIED input dir : $INPUT_DIR"
echo "***************************"
;;
/?)
echo "Usage: `basename $0` -h for help";
echo "$COMMAND_LINE_OPTIONS_HELP"
exit $E_OPTERROR;
;;
esac
done
Incorrecto. En realidad, getopts
soporta argumentos opcionales! Desde la página de manual de bash:
If a required argument is not found, and getopts is not silent,
a question mark (?) is placed in name, OPTARG is unset, and a diagnostic
message is printed. If getopts is silent, then a colon (:) is placed in name
and OPTARG is set to the option character found.
Cuando la página de manual dice "silencioso" significa un informe de error silencioso. Para habilitarlo, el primer carácter de optstring debe ser dos puntos:
while getopts ":hd:R:" arg; do
# ...rest of iverson''s loop should work as posted
done
Dado que getopt de Bash no reconoce --
para finalizar la lista de opciones, puede que no funcione cuando -R
sea la última opción, seguida de algún argumento de ruta.
PS: Tradicionalmente, getopt.c usa dos puntos ( ::
:) para especificar un argumento opcional. Sin embargo, la versión utilizada por Bash no lo hace.
Siempre puede decidir diferenciar la opción con minúsculas o mayúsculas.
Sin embargo, mi idea es llamar a getopts
dos veces y el primer análisis de tiempo sin argumentos ignorándolos ( R
), luego el segundo análisis solo esa opción con soporte de argumentos ( R:
getopts
. El único truco es que OPTIND
(índice) debe cambiarse durante el procesamiento, ya que mantiene el puntero al argumento actual.
Aquí está el código:
#!/usr/bin/env bash
while getopts ":hd:R" arg; do
case $arg in
d) # Set directory, e.g. -d /foo
dir=$OPTARG
;;
R) # Optional level value, e.g. -R 123
OI=$OPTIND # Backup old value.
((OPTIND--)) # Decrease argument index, to parse -R again.
while getopts ":R:" r; do
case $r in
R)
# Check if value is in numeric format.
if [[ $OPTARG =~ ^[0-9]+$ ]]; then
level=$OPTARG
else
level=1
fi
;;
:)
# Missing -R value.
level=1
;;
esac
done
[ -z "$level" ] && level=1 # If value not found, set to 1.
OPTIND=$OI # Restore old value.
;;
/? | h | *) # Display help.
echo "$0 usage:" && grep " .)/ #" $0
exit 0
;;
esac
done
echo Dir: $dir
echo Level: $level
Aquí hay algunas pruebas para escenarios que funcionan:
$ ./getopts.sh -h
./getopts.sh usage:
d) # Set directory, e.g. -d /foo
R) # Optional level value, e.g. -R 123
/? | h | *) # Display help.
$ ./getopts.sh -d /foo
Dir: /foo
Level:
$ ./getopts.sh -d /foo -R
Dir: /foo
Level: 1
$ ./getopts.sh -d /foo -R 123
Dir: /foo
Level: 123
$ ./getopts.sh -d /foo -R wtf
Dir: /foo
Level: 1
$ ./getopts.sh -R -d /foo
Dir: /foo
Level: 1
Escenarios que no funcionan (por lo que el código necesita un poco más de ajustes):
$ ./getopts.sh -R 123 -d /foo
Dir:
Level: 123
Puede encontrar más información sobre el uso de getopts
en man bash
.
Ver también: Pequeño tutorial de getopts en Bash Hackers Wiki
Tratar:
while getopts "hd:R:" arg; do
case $arg in
h)
echo "usage"
;;
d)
dir=$OPTARG
;;
R)
if [[ $OPTARG =~ ^[0-9]+$ ]];then
level=$OPTARG
elif [[ $OPTARG =~ ^-. ]];then
level=1
let OPTIND=$OPTIND-1
else
level=1
fi
;;
/?)
echo "WRONG" >&2
;;
esac
done
Creo que el código anterior funcionará para tus propósitos mientras usas getopts
. He añadido las siguientes tres líneas a su código cuando getopts
encuentra -R
:
elif [[ $OPTARG =~ ^-. ]];then
level=1
let OPTIND=$OPTIND-1
Si se encuentra -R
y el primer argumento parece otro parámetro de getopts, el nivel se establece en el valor predeterminado de 1
, y luego la variable $OPTIND
se reduce en uno. La próxima vez que getopts
vaya a agarrar un argumento, tomará el argumento correcto en lugar de saltárselo.
Este es un ejemplo similar basado en el código del comentario de Jan Schampera en este tutorial :
#!/bin/bash
while getopts :abc: opt; do
case $opt in
a)
echo "option a"
;;
b)
echo "option b"
;;
c)
echo "option c"
if [[ $OPTARG = -* ]]; then
((OPTIND--))
continue
fi
echo "(c) argument $OPTARG"
;;
/?)
echo "WTF!"
exit 1
;;
esac
done
Cuando descubra que OPTARG von -c es algo que comienza con un guión, luego restablezca OPTIND y vuelva a ejecutar getopts (continúe con el bucle while). Oh, por supuesto, esto no es perfecto y necesita algo más de robustez. Es solo un ejemplo.
getopts
realmente no soporta esto; Pero no es difícil escribir tu propio reemplazo.
while true; do
case $1 in
-R) level=1
shift
case $1 in
*[!0-9]* | "") ;;
*) level=$1; shift ;;
esac ;;
# ... Other options ...
-*) echo "$0: Unrecognized option $1" >&2
exit 2;;
*) break ;;
esac
done