Необязательный аргумент опции с 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
уровень относится к параметру -R
, dir относится к параметрам -d
когда я ввожу ./count.sh -R 1 -d test/
, он работает правильно
когда я ввожу ./count.sh -d test/ -R 1
, он работает правильно
но я хочу, чтобы он работал, когда я вводил ./count.sh -d test/ -R
или ./count.sh -R -d test/
Это означает, что я хочу, чтобы -R
имел значение по умолчанию, и последовательность команд могла бы быть более гибкой.
Ответы
Ответ 1
getopts
на самом деле не поддерживает это; но не сложно написать собственную замену.
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
Ответ 2
Неправильно. Фактически getopts
поддерживает необязательные аргументы! На странице bash man:
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.
Когда man-страница говорит "silent", это означает, что отчет об ошибках в бесшумном режиме. Чтобы включить его, первым символом optstring должно быть двоеточие:
while getopts ":hd:R:" arg; do
# ...rest of iverson loop should work as posted
done
Так как bash getopt не распознает --
, чтобы завершить список опций, он может не работать, когда -R
- последний параметр, за которым следует некоторый аргумент пути.
P.S.: Традиционно getopt.c использует два двоеточия (::
) для указания необязательного аргумента. Однако версия, используемая bash, не поддерживает.
Ответ 3
Я согласен с tripleee, getopts не поддерживает обработку необязательных аргументов.
Скомпрометированное решение, на котором я остановился, - это использовать комбинацию с верхним регистром/нижним регистром того же флаг опции, чтобы различать параметр, который принимает аргумент, а другой - нет.
Пример:
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
Ответ 4
На самом деле это довольно просто. Просто оставьте трейлинг-двоеточие после R и используйте 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 test
Тест
count.sh -d test -R
1 тест
count.sh -R -d test
1 тест
count.sh -d test -R 2
2 тест
count.sh -R 2 -d test
2 тест
Ответ 5
Этот обход определяет "R" без аргумента (no ':'), проверяет любой аргумент после "-R" (управляет последней опцией в командной строке) и проверяет, начинается ли существующий аргумент с тире.
# 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
Ответ 6
Try:
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
Я думаю, что приведенный выше код будет работать для ваших целей, используя getopts
. Я добавил следующие три строки в ваш код, когда getopts
встречается -R
:
elif [[ $OPTARG =~ ^-. ]];then
level=1
let OPTIND=$OPTIND-1
Если встречается -R
, и первый аргумент выглядит как другой параметр getopts, уровень устанавливается на значение по умолчанию 1
, а затем переменная $OPTIND
уменьшается на единицу. В следующий раз, когда getopts
идет, чтобы схватить аргумент, он возьмет правильный аргумент, а не пропустит его.
Вот пример, основанный на коде из комментарий Яна Шампера в этом уроке:
#!/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
Когда вы обнаружите, что OPTARG von -c - это что-то, начинающееся с дефиса, тогда reset OPTIND и повторно запустите getopts (продолжите цикл while). О, конечно, это не идеально и требует некоторой уверенности. Это просто пример.
Ответ 7
Следующий код решает эту проблему, проверяя ведущую черту и, если найден, уменьшает OPTIND, чтобы вернуться к пропущенной опции для обработки. Как правило, это прекрасно работает, за исключением того, что вы не знаете, какой пользователь будет устанавливать параметры в командной строке - если параметр необязательного аргумента является последним и не содержит аргумент, то getopts захочет отключить ошибку.
Чтобы устранить проблему отсутствия последнего аргумента, массив "$ @" просто имеет пустую строку "$ @", добавленную так, что getopts будет удовлетворен тем, что она собрала еще один аргумент параметра. Чтобы исправить этот новый пустой аргумент, задана переменная, содержащая общий счет всех обрабатываемых параметров - при обработке последней опции создается вспомогательная функция под названием trim и удаляет пустую строку до используемого значения.
Это не рабочий код, он имеет только держатели мест, но вы можете легко его изменить и с небольшим вниманием он может быть полезен для создания надежной системы.
#!/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 "[email protected]";
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 "[email protected] " ## extra space, so getopts can find arguments
Ответ 8
Вы всегда можете выбрать дифференцирование с помощью строчных или прописных букв.
Однако моя идея состоит в том, чтобы вызывать getopts
дважды и 1-й раз без аргументов, игнорируя их (R
), тогда 2-й раз анализирует только эту опцию с поддержкой аргументов (R:
). Единственный трюк в том, что OPTIND
(index) необходимо изменить во время обработки, поскольку он сохраняет указатель на текущий аргумент.
Вот код:
#!/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
Вот несколько тестов для сценариев, которые работают:
$ ./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
Сценарии, которые не работают (поэтому код требует немного больше настроек):
$ ./getopts.sh -R 123 -d /foo
Dir:
Level: 123
Более подробную информацию об использовании getopts
можно найти в man bash
.
См. также: Малый учебник по учебникам в Bash Hackers Wiki
Ответ 9
Я сам столкнулся с этим и почувствовал, что ни одно из существующих решений не было действительно чистым. Немного поработав над ним и попробовав разные вещи, я обнаружил, что использование режима getopts SILENT с :) ...
, похоже, добилось цели, наряду с синхронизацией OPTIND.
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"
...
Ответ 10
Вдохновленный в @calandoa answer (единственном, который действительно работает!), Я сделал простую функцию, которая позволяет легко использовать ее несколько раз.
getopts_get_optional_argument() {
eval next_token=\${$OPTIND}
if [[ -n $next_token && $next_token != -* ]]; then
OPTIND=$((OPTIND + 1))
OPTARG=$next_token
else
OPTARG=""
fi
}
Пример использования:
while getopts "hdR" option; do
case $option in
d)
getopts_get_optional_argument [email protected]
dir=${OPTARG}
;;
R)
getopts_get_optional_argument [email protected]
level=${OPTARG:-1}
;;
h)
show_usage && exit 0
;;
\?)
show_usage && exit 1
;;
esac
done
Это дает нам практический способ получить "эту отсутствующую функцию" в getopts
:)
ПРИМЕЧАНИЕ что тем не менее параметры командной строки с необязательными аргументами явно не одобряются
Guideline 7: Option-arguments should not be optional.
но у меня нет интуитивно понятного способа реализовать мой случай без этого: у меня есть 2 режима, которые активируются либо с использованием одного флага, либо другого, и оба имеют аргумент с явным значением по умолчанию. Введение третьего флага только для устранения неоднозначности делает его плохим стилем CLI.
Я проверил это со многими комбинациями, включая все в ответе @aaron-sua, и хорошо работает.