可选的参数与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 
  • level指-R的参数,dir指-d的参数

  • 当我input./count.sh -R 1 -d test/它工作正常

  • 当我input./count.sh -d test/ -R 1它正常工作

  • 但是我想在input./count.sh -d test/ -R./count.sh -R -d test/

这意味着我希望-R有一个默认值,命令的顺序可以更灵活。

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 

错误。 其实getopts支持可选参数! 从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. 

当手册页显示“无声”时,表示无提示错误报告。 要启用它, optstring的第一个字符需要是冒号:

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

由于Bash的getopt无法识别--要结束选项列表,当-R是最后一个选项时,它可能不起作用,然后是一些path参数。

PS:传统上, getopt.c使用两个冒号( :: :)来指定一个可选的参数。 但是,Bash使用的版本不会。

我同意tripleee,getopts不支持可选参数处理。

我已经解决的妥协解决scheme是使用相同选项标志的大写/小写组合来区分带有参数的选项和不带有参数的选项。

例:

 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 

这其实很简单。 只需在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 -dtesting

testing

count.sh -dtesting-R

1testing

count.sh -R -dtesting

1testing

count.sh -d test -R 2

2testing

count.sh -R 2 -dtesting

2testing

下面的代码通过检查一个前导破折号来解决这个问题,如果发现递减,OPTIND指向跳过的选项进行处理。 这通常工作正常,除了你不知道用户将命令行放置选项的顺序 – 如果您的可选参数选项是最后一个,并没有提供一个参数getopts会想出错。

为了解决最后一个参数丢失的问题,“$ @”数组只添加了一个空string“$ @”,这样getopts就会满足于它已经吞噬了另一个选项参数。 为了修复这个新的空参数,设置一个variables来保存所有要处理的选项的总数 – 当最后一个选项被处理时,调用一个名为trim的辅助函数,并在使用该值之前删除空string。

这不是工作代码,它只有占位符,但是你可以很容易地修改它,并且有一点小心,它可以build立一个强大的系统是有用的。

 #!/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. ## Eg "'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. 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 

尝试:

 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参数,则将level设置为默认值1 ,然后将$OPTINDvariables减1。 下一次getopts去争取一个参数,它会抓住正确的参数而不是跳过它。


以下是基于Jan Schampera在本教程中的评论的代码:

 #!/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是以连字符开头的,然后重置OPTIND并重新运行getopts(继续while循环)。 哦,当然,这不是完美的,需要更强大的一些。 这只是一个例子。

您始终可以决定使用小写还是大写来区分选项。

然而,我的想法是调用getopts两次和第一次parsing没有参数忽略它们( R )然后第二次parsing只有该选项与参数支持( R: 。 唯一的技巧是OPTIND (index)需要在处理过程中被改变,因为它保持指向当前参数的指针。

这里是代码:

 #!/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 

这里有几个testingscheme的工作原理:

 $ ./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 

情况不起作用(所以代码需要一点点调整):

 $ ./getopts.sh -R 123 -d /foo Dir: Level: 123 

有关getopts用法的更多信息可以在man bashfind。

另请参阅:Bash Hackers Wiki上的小型getopts教程

这个解决方法定义了没有参数(':')的'R',testing了'-R'(pipe理命令行上的最后一个选项)之后的任何参数,并testing现有的参数是否以破折号开始。

 # 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 

我发现它与主要的冒号工作的争论。 主要的冒号让脚本对缺less的论点保持沉默。