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
niveau fait référence au paramètre de -R
, dir fait référence aux paramètres de -d
quand je saisis ./count.sh -R 1 -d test/
cela fonctionne correctement
quand je saisis ./count.sh -d test/ -R 1
cela fonctionne correctement
mais je veux le faire fonctionner quand je saisis ./count.sh -d test/ -R
ou ./count.sh -R -d test/
Cela signifie que je veux que -R
ait une valeur par défaut et que la séquence de commande soit plus flexible.
getopts
ne supporte pas vraiment cela; mais ce n'est pas difficile d'écrire votre propre remplaçant.
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
Faux. En fait, getopts
supporte les arguments optionnels! De la page de manuel 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.
Lorsque la page de manuel dit "silencieux", cela signifie que le signalement des erreurs est silencieux. Pour l'activer, le premier caractère de optstring doit être un signe deux-points:
while getopts ":hd:R:" arg; do
# ...rest of iverson's loop should work as posted
done
Etant donné que getopt de Bash ne reconnaît pas --
pour terminer la liste des options, il peut ne pas fonctionner lorsque -R
est la dernière option, suivie d'un argument de chemin.
P.S .: Traditionnellement, getopt.c utilise deux points (::
) pour spécifier un argument facultatif. Cependant, la version utilisée par Bash ne le fait pas.
Je suis d'accord avec tripleee, getopts ne supporte pas la gestion optionnelle des arguments.
La solution de compromis que j'ai choisie consiste à utiliser la combinaison majuscule/minuscule du même indicateur d'option pour différencier l'option qui prend un argument et l'autre qui ne prend pas.
Exemple:
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
C'est en fait assez facile. Il suffit de déposer les deux points après le R et d’utiliser 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
test count.sh -d
tester
count.sh -d test -R
1 test
test count.sh -R -d
1 test
count.sh -d test -R 2
2 test
test count.sh -R 2 -d
2 test
Je me suis heurté à ce problème moi-même et j'ai estimé qu'aucune des solutions existantes n'était vraiment propre. Après avoir travaillé un peu dessus et essayé diverses choses, je me suis aperçu que tirer parti du mode SILENCE de getopts avec :) ...
semblait avoir permis de résoudre ce problème tout en maintenant OPTIND synchronisé.
usage: test.sh [-abst] [-r [DEPTH]] filename
*NOTE: -r (recursive) with no depth given means full recursion
#!/usr/bin/env bash
depth='-d 1'
while getopts ':abr:st' opt; do
case "${opt}" in
a) echo a;;
b) echo b;;
r) if [[ "${OPTARG}" =~ ^[0-9]+$ ]]; then
depth="-d ${OPTARG}"
else
depth=
(( OPTIND-- ))
fi
;;
s) echo s;;
t) echo t;;
:) [[ "${OPTARG}" = 'r' ]] && depth=;;
*) echo >&2 "Invalid option: ${opt}"; exit 1;;
esac
done
shift $(( OPTIND - 1 ))
filename="$1"
...
Essayer:
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
Je pense que le code ci-dessus fonctionnera pour vos besoins tout en utilisant toujours getopts
. J'ai ajouté les trois lignes suivantes à votre code lorsque getopts
rencontre -R
:
Elif [[ $OPTARG =~ ^-. ]];then
level=1
let OPTIND=$OPTIND-1
Si -R
est rencontré et que le premier argument ressemble à un autre paramètre getopts, level est défini sur la valeur par défaut 1
et la variable $OPTIND
est réduite de un. La prochaine fois que getopts
saisira un argument, il récupérera le bon argument au lieu de le sauter.
Voici un exemple similaire basé sur le code de Le commentaire de Jan Schampera dans ce tutoriel :
#!/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
Lorsque vous découvrez que OPTARG von -c commence par un trait d'union, réinitialisez OPTIND et relancez getopts (continuez la boucle while). Oh, bien sûr, ce n'est pas parfait et a besoin de plus de robustesse. C'est juste un exemple.
Vous pouvez toujours décider de différencier l'option en minuscule ou en majuscule.
Cependant, mon idée est d'appeler getopts
deux fois et la première fois, sans les ignorer par les arguments (R
), puis la deuxième fois, analyser uniquement cette option avec le support des arguments (R:
). La seule astuce est que OPTIND
(index) doit être changé pendant le traitement, car il garde le pointeur sur l'argument actuel.
Voici le code:
#!/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
Voici quelques tests pour les scénarios qui fonctionnent:
$ ./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
Scénarios qui ne fonctionnent pas (le code nécessite donc quelques ajustements supplémentaires):
$ ./getopts.sh -R 123 -d /foo
Dir:
Level: 123
Plus d'informations sur l'utilisation de getopts
sont disponibles dans man bash
.
Voir aussi: Petit tutoriel getopts à Bash Hackers Wiki
Cette solution de contournement définit 'R' sans argument (no ':'), teste tout argument après le '-R' (option de gestion en dernier sur la ligne de commande) et détermine si un argument existant commence par un tiret.
# 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
Le code suivant résout ce problème en recherchant un tiret en tête et, s'il est trouvé, décrémente OPTIND pour qu'il renvoie à l'option ignorée pour le traitement. Cela fonctionne généralement bien sauf que vous ne connaissez pas l'ordre dans lequel l'utilisateur placera des options sur la ligne de commande - si votre option d'argument optionnel est last et ne fournit pas d'argument, getopts voudra se tromper.
Pour résoudre le problème de l'argument final manquant, le tableau "$ @" a simplement une chaîne vide "$ @" ajoutée afin que getopts soit convaincu qu'il a englouti un autre argument d'option. Pour corriger ce nouvel argument vide, une variable contenant le nombre total d'options à traiter est définie. Lorsque la dernière option est en cours de traitement, une fonction d'assistance appelée trim est appelée et supprime la chaîne vide avant que la valeur ne soit utilisée.
Ce code ne fonctionne pas, il ne comporte que des espaces réservés, mais vous pouvez le modifier facilement et avec un peu de soin, il peut être utile de construire un système robuste.
#!/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