Argumento de opción opcional con getopts

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 ingreso ./count.sh -R 1 -d test/ funciona correctamente

  • cuando ingreso ./count.sh -d test/ -R 1 funciona correctamente

  • pero quiero que funcione cuando ./count.sh -d test/ -R o ./count.sh -R -d test/

Esto significa que quiero que -R tenga un valor predeterminado y que la secuencia de comando sea más flexible.

getopts realidad no es compatible con 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 

Incorrecto. ¡En realidad, getopts admite argumentos opcionales! Desde la página del hombre 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 del manual dice “silenciosa”, significa informar silenciosamente. Para habilitarlo, el primer carácter de optstring debe ser un punto y coma:

 while getopts ":hd:R:" arg; do # ...rest of iverson's loop should work as posted done 

Como el getopt de Bash no reconoce -- para finalizar la lista de opciones, puede que no funcione cuando -R es la última opción, seguida de algún argumento de ruta.

PD: Tradicionalmente, getopt.c usa dos puntos ( :: 🙂 para especificar un argumento opcional. Sin embargo, la versión utilizada por Bash no lo hace.

Estoy de acuerdo con tripleee, getopts no admite el manejo opcional de argumentos.

La solución comprometida en la que me he basado es utilizar la combinación de mayúsculas y minúsculas del mismo distintivo de opción para diferenciar entre la opción que toma un argumento y la otra que no.

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 

Esto es realmente bastante fácil. Simplemente suelta el colon que sigue detrás de la R y usa 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 test -R

1 prueba

count.sh -R -d test

1 prueba

count.sh -d test -R 2

2 prueba

count.sh -R 2 -d test

2 prueba

El siguiente código soluciona este problema al buscar un guion delantero y, si se encuentra, disminuye OPTIND para que apunte a la opción omitida para su procesamiento. Por lo general, esto funciona bien, excepto que no conoce el orden en que el usuario colocará las opciones en la línea de comando; si su opción de argumento opcional es la última y no proporciona un argumento, getopts querrá omitir el error.

Para solucionar el problema del último argumento faltante, la matriz “$ @” simplemente tiene una cadena vacía “$ @” adjuntada para que getopts se conforme con que ha engullido otro argumento de opción. Para solucionar este nuevo argumento vacío, se establece una variable que contiene el recuento total de todas las opciones para procesar: cuando se procesa la última opción, se llama a una función auxiliar llamada trim y se elimina la cadena vacía antes del valor que se utiliza.

Este código no funciona, solo tiene marcadores de posición, pero puede modificarlo fácilmente y, con un poco de cuidado, puede ser útil construir un sistema robusto.

 #!/usr/bin/env bash declare -r CHECK_FLOAT="%f" declare -r CHECK_INTEGER="%i" ##  Number - Number to check ##  String - Number type to check ##  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. ## Eg "'1e+03" is valid POSIX float format, see http://mywiki.wooledge.org/BashFAQ/054 ##  Number - Number to print ##  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 } ##  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. Eg 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 hdrs MRSD 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 

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 utilizas getopts . He agregado las siguientes tres líneas a tu código cuando getopts encuentra con -R :

  elif [[ $OPTARG =~ ^-. ]];then level=1 let OPTIND=$OPTIND-1 

Si se encuentra -R y el primer argumento se parece a otro parámetro 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 tomar un argumento, tomará el argumento correcto en lugar de saltearse.


Aquí hay 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, reinicie OPTIND y vuelva a ejecutar getopts (continúe con el ciclo while). Oh, por supuesto, esto no es perfecto y necesita algo más de solidez. Es solo un ejemplo.

Siempre puede decidir diferenciar la opción con minúsculas o mayúsculas.

Sin embargo, mi idea es llamar a getopts dos veces y analizar por primera vez sin argumentos que los ignoran ( R ), luego 2da vez analizar solo esa opción con soporte de argumento ( R: getopts . El único truco es que OPTIND (índice) necesita 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, eg -d /foo dir=$OPTARG ;; R) # Optional level value, eg -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, eg -d /foo R) # Optional level value, eg -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 

Se puede encontrar más información sobre el uso de getopts en man bash .

Ver también: Pequeño tutorial getopts en Bash Hackers Wiki

Esta solución define ‘R’ sin ningún argumento (no ‘:’), prueba cualquier argumento después de ‘-R’ (administra la última opción en la línea de comando) y prueba 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