带有 getopts 的可选选项参数

Posted

技术标签:

【中文标题】带有 getopts 的可选选项参数【英文标题】:Optional option argument with getopts 【发布时间】:2012-07-16 01:23:37 【问题描述】:
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的参数

当我输入 ./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 确实支持可选参数!从 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 是最后一个选项,后跟一些路径参数时,它可能不起作用。

P.S.:传统上,getopt.c 使用两个冒号 (::) 来指定可选参数。但是,Bash 使用的版本没有。

【讨论】:

没有意义。如果 -R 是最后一个参数,它不会被处理 没有。您错了。 ./count.sh -R -d test/ 不起作用,因为 -d 被视为 -R 的参数(这根本不是可选的)。 这个答案充其量是误导。正如@calandoa 所指出的,任何带有“可选”参数的选项只有在它是最后一个选项时才“有效”。否则,它将使用下一个选项作为其参数。例如在这种用法./count.sh -R -d test/'-R' 将'-d' 作为其参数,'-d' 不被识别为一个选项。我只是重申已经说过的话(@calandoa),因为这个不正确的答案有 20 个净赞成票。 也许以前版本的 bash(或 sh)“做正确的事”,但 bash 4.3.30(1) Debian Jessie 失败,如前一条评论所述。 这个答案在赢得投票方面是一个奇怪的案例,因为它没有回答所提出的相当具体的问题,但它确实回答了一个(我猜)更常见的问题:什么是让getopts 忽略它在不报告错误的情况下无法识别的参数的语法?这就是我来寻求答案的问题,这就是我所追求的答案。【参考方案2】:

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

【讨论】:

请看下面来自 Andreas Spindler 的回答,它是支持的 @Rohit 请注意,Andreas Spindler 的答案在大多数情况下都是错误的,如他的答案下方的 cmets 所述。 (没有“上方”或“下方”;每个访问者都有自己的排序偏好来决定答案的显示顺序。)【参考方案3】:

此解决方法定义不带参数的“R”(没有“:”),测试“-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

【讨论】:

这实际上是这里唯一有效的答案!请点赞。 受到这个答案的启发(唯一一个真正有效的!),我做了一个简单的函数,可以让它很容易被多次使用。看我的回答here eval nextopt=\$$OPTIND 是一种创造性的解决方案,但 Bash 已经有一种用于间接扩展的特殊语法:nextopt=$!OPTIND。【参考方案4】:

我同意 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

【讨论】:

【参考方案5】:

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

测试

count.sh -d 测试 -R

1 次测试

count.sh -R -d 测试

1 次测试

count.sh -d test -R 2

2 次测试

count.sh -R 2 -d 测试

2 次测试

【讨论】:

最后一次测试 (count.sh -R 2 -d test) 结果给了我1,而不是2 test (bash 5.0.3)。所有其他人都工作。这是因为$@:$OPTIND 会计算出所有其余的参数,而不仅仅是下一个。 使用@calandoa 的答案修复它使其通过所有测试。【参考方案6】:

受@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 $@
    dir=$OPTARG
    ;;
  R)
    getopts_get_optional_argument $@
    level=$OPTARG:-1
    ;;
  h)
    show_usage && exit 0
    ;;
  \?)
    show_usage && exit 1
    ;;
  esac
done

这为我们提供了一种实用的方法来获取getopts 中的“缺少的功能” :)

注意尽管如此,带有可选参数的命令行选项似乎是 discouraged explicitly

准则 7: 选项参数不应是可选的。

但是如果没有这个,我没有直观的方法来实现我的案例:我有两种模式可以通过使用一个标志或其他标志来激活,并且它们都有一个带有明确默认值的参数。引入第三个标志只是为了消除歧义,使它看起来是一种糟糕的 CLI 风格。

我已经用许多组合对此进行了测试,包括@aaron-sua 的答案中的所有组合并且效果很好。

【讨论】:

【参考方案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 "$@";
 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  

【讨论】:

【参考方案8】:

试试:

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 去抓取参数时,它将抓取正确的参数而不是跳过它。


这里是基于Jan Schampera's comment at this 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

当您发现 OPTARG von -c 是以连字符开头的内容时,请重置 OPTIND 并重新运行 getopts(继续 while 循环)。哦,当然,这并不完美,需要更多的稳健性。这只是一个例子。

【讨论】:

从玩弄 getopts 看来,这里的工作是let OPTIND=$OPTIND-1 不确定关卡跟踪发生了什么。也许你的代码有其他用途? 如果最后一个选项是“可选”OPTARG,则 wiki.bash-hackers.org 给出的示例不起作用。 $ prog.bash -a -b -c 甚至不会知道 -c 选项,而 $ prog.bash -a -c -b 会。如果它是最后一个参数,这怎么能工作?【参考方案9】:

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

但是我的想法是调用getopts 两次,第一次解析不带参数忽略它们(R),然后第二次只解析带有参数支持的选项(R:)。唯一的技巧是OPTIND(索引)在处理过程中需要更改,因为它保持指向当前参数的指针。

代码如下:

#!/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找到。

另请参阅:Small getopts tutorial 在 Bash Hackers Wiki

【讨论】:

【参考方案10】:

我自己也遇到过这个问题,觉得现有的解决方案都不是真正干净的。经过一番努力并尝试了各种方法后,我发现利用 :) ... 的 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"
...

【讨论】:

【参考方案11】:

我认为有两种方法。

首先是 calandoa 的回答,使用 OPTIND 并且没有静音模式。

其次是使用 OPTIND 和静默模式。

while getopts ":Rr:" name; do
    case $name in
        R)
            eval nextArg=\$$OPTIND
            # check option followed by nothing or other option.
            if [[ -z $nextArg || $nextArg =~ ^-.* ]]; then
                level=1
            elif [[ $nextArg =~ ^[0-9]+$ ]]; then
                level=$nextArg
                OPTIND=$((OPTIND + 1))
            else
                level=1
            fi
            ;;
        r)
            # check option followed by other option.
            if [[ $OPTARG =~ ^-.* ]]; then
                OPTIND=$((OPTIND - 1))
                level2=2
            elif [[ $OPTARG =~ ^[0-9]+$ ]]; then
                level2="$OPTARG"
            else
                level2=2
            fi
            ;;
        :)
            # check no argument
            case $OPTARG in
                r)
                    level2=2
                    ;;
            esac
    esac
done

echo "Level 1 : $level"
echo "Level 2 : $level2"

【讨论】:

【参考方案12】:

旧线程,但我想无论如何我都会分享我所做的(这也大多比这个线程更老)。我厌倦了试图让 getopt 做我想做的事,并且很沮丧地这样做了,以便支持带有可选参数的短选项和长选项。这是一段漫长的路,肯定有人会笑,但它完全按照我想要的方式工作 - 所有三种情况的过度评论示例如下:

#!/usr/bin/bash
# Begin testme.sh

shopt -s extglob;
VERSION="1.0"

function get_args()
  while test -n "$1" ; do
    case "$1" in
      -a | --all)
         # dumb single argument example
         PROCESS_ALL="yes"
         shift 1
      ;;
      -b | --buildnum)
         # requires a second argument so use check_arg() below
         check_arg $1 $2
         BUILD_NUM=$2
         shift 2
      ;;
      -c | --cache)
        # Example where argument is not required, don't use check_arg()
        if [ echo $2 | grep -q "^-" ]; then
          # no argument given, use default cache value
          CACHEDIR=~/mycachedir
          # Note: this could have been set upon entering the script
          #       and used the negative above as well
          shift 1
        else
          cache=$2
          shift 2
        fi
      ;;
      -h | --help)
        showhelp
        exit 0
      ;;
      -v | --version)
        echo -e "$(basename $0) $VERSION\n"
        exit 0
      ;;
      # Handle getopt style short args (reason for shopts above)
      -+([a-z,A-Z]))
        # split up the arguments and call recursively with trailing break
        arg="$1"
        newargs=$( echo $1 | sed 's@-@@' | \
                               sed 's/.\1\/& /g' | \
                               sed 's/[^ ]* */-&/g')
        newargs="$newargs $(echo $@ | sed "s@$arg@@")"
        get_args $newargs
        break;
      ;;
      *)
        echo -e "Invalid argument $1!\n\n"
        showhelp
        exit 1
      ;;
    esac
  done


# Super lazy, but I didn't want an if/then/else for every required arg
function check_arg()
  if [ echo "$2" | grep -q "^-" ]; then
    echo "Error:  $1 requires a valid argument."
    exit 1
  fi


function showhelp()
  echo ""
  echo "`basename $0` is a utility to..."


# Process command line arguments
get_args $@
...
# End testme.sh

我从来没有遇到过它,但我想可能存在我需要第二个参数以“-”字符开头的情况,在这种情况下,我会在调用 get_args( )。我也使用了 with 固定位置参数,在这种情况下,它们在最后,但相同的解决方案。另外,我认为便携式版本可以处理 *) 中的组合短参数,但我认为如果 bash 的要求太高,那你就靠你自己了。

【讨论】:

【参考方案13】:

目前提出的所有解决方案都将代码放在case ... in ... esac 中,但在我看来,修改getopts 命令会更自然,因此我编写了这个函数:

编辑:

现在,您可以指定可选参数的类型(请参阅使用信息)。

此外,该函数现在不再测试 $nextArg 是否“看起来像”选项 arg,而是检查 $nextArg 是否包含来自 $optstring 的字母。 这样,$optstring 中未包含的选项字母可以用作可选 arg,与 getopts' 强制 args 一样。

最新变化:

修复了 $nextArg 是否为选项 arg 的测试: 测试$nextArg 是否以破折号开头。 如果没有这个测试,包含字母的可选参数 来自$optstring 的人无法识别。 添加了正则表达式类型说明符(参见使用信息)。 已修复:0 未被识别为指定为 int 的可选 arg。 $nextArg 是 int 时的简化测试。 类型说明符::/.../: 使用perl 测试$nextArg 是否与正则表达式匹配。 这样,您就可以从(几乎 (*))Perl 正则表达式的全部功能中受益。 (*):请参阅最后一段使用信息。 已修复:不适用于多个正则表达式类型说明符: 使用 perl 而不是 grep/sed 构造,因为需要非贪婪匹配。

用法:

调用:getopts-plus optstring name "$@"

optstring:与普通的getopts 一样,但您可以通过将 :: 附加到选项字母来指定带有可选参数的选项。

但是,如果您的脚本支持使用带有可选参数作为唯一选项参数的选项的调用,然后是非选项参数,则非选项参数将被视为选项的参数。

如果你很幸运并且可选参数应该是一个整数,而非选项参数是一个字符串,反之亦然,你可以通过附加 :::i 来指定类型一个整数或 :::s 一个字符串来解决这个问题。

如果这不适用,您可以通过将 ::/.../ 附加到选项字母来为可选参数指定 Perl 正则表达式。 有关 Perl 正则表达式的介绍,请参见此处:https://perldoc.perl.org/perlretut请注意:ATM,只有 /.../ 会在 :: 之后被识别为正则表达式,即。 e.既不能使用其他分隔符,也不能使用修饰符,所以 e. G。 m#...#a 将无法被识别。 如果带有可选参数的选项后面有一个非选项参数,则只有匹配正则表达式才会被认为是可选参数。 需要明确的是:::/.../ 不是用于参数验证,而只是用于区分具有可选参数和非选项参数的选项的参数。

#!/bin/bash

# Invocation: getopts-plus optstring name "$@"\
# \
# optstring: Like normal getopts, but you may specify options with optional argument
# by appending :: to the option letter.\
# \
# However, if your script supports an invocation with an option with optional
# argument as the only option argument, followed by a non-option argument,
# the non-option argument will be considered to be the argument for the option.\
# \
# If you're lucky and the optional argument is expected to be an integer, whereas
# the non-option argument is a string or vice versa, you may specify the type by
# appending :::i for an integer or :::s for a string to solve that issue.\
# \
# If that doesn't apply, you may specify a Perl regexp for the optional arg by appending
# ::/.../ to the option letter.\
# See here for an introduction to Perl regexps: https://perldoc.perl.org/perlretut
# Please note: ATM, only /.../ will be recognised as a regexp after ::,\
# i. e. neither other delimiters, nor modifiers may be used, so e. g. m#...#a will
# not be recognised.\
# If there is a non-option argument after the option with optional argument, it will
# be considered to be the optional argument only if it matches the regexp.\
# To be clear: ::/.../ is not meant for argument validation but solely to discriminate
# between arguments for options with optional argument and non-option arguments.
function getopts-plus

    local optstring=$1
    local -n name=$2

    shift 2

    local optionalArgSuffixRE='::(?::[si]|/.*?/)?'
    local optionalArgTypeCaptureRE=':::([si])|::(/.*?/)'

    # If we pass 'opt' for 'name' (as I always do when using getopts) and there is
    # also a local variable 'opt', the "outer" 'opt' will always be empty.
    # I don't understand why a local variable interferes with caller's variable with
    # same name in this case; however, we can easily circumvent this.
    local opt_

    # Extract options with optional arg

    local -A isOptWithOptionalArg

    while read opt_; do
        # Using an associative array as set
        isOptWithOptionalArg[$opt_]=1
    done <<<$(perlGetCaptures "$optstring" "([a-zA-Z])$optionalArgSuffixRE")

    # Extract all option letters (used to weed out possible optional args that are option args)
    local optLetters=$(perlGetCaptures "$optstring" "([a-zA-Z])(?:$optionalArgSuffixRE|:)?")

    # Save original optstring, then remove our suffix(es)
    local optstringOrg=$optstring
    optstring=$(perl -pe "s#$optionalArgSuffixRE##g" <<<$optstring)

    getopts $optstring name "$@" || return # Return value is getopts' exit value.

    # If current option is an option with optional arg and if an arg has been provided,
    # check if that arg is not an option and if it isn't, check if that arg matches(*)
    # the specified type, if any, and if it does or no type has been specified,
    # assign it to OPTARG and inc OPTIND.
    #
    # (*) We detect an int because it's easy, but we assume a string if it's not an int
    # because detecting a string would be complicated.
    # So it sounds strange to call it a match if we know that the optional arg is specified
    # to be a string, but merely that the provided arg is not an int, but in this context,
    # "not an int" is equivalent to "string". At least I think so, but I might be wrong.

    if ((isOptWithOptionalArg[$name])) && [[ $!OPTIND ]]; then
        local nextArg=$!OPTIND foundOpt=0

        # Test if $nextArg is an option arg
        if [[ $nextArg == -* ]]; then
            # Check if $nextArg contains a letter from $optLetters.
            # This way, an option not contained in $optstring can be
            # used as optional arg, as with getopts' mandatory args.

            local i

            # Start at char 1 to skip the leading dash
            for ((i = 1; i < $#nextArg; i++)); do
                while read opt_; do
                    [[ $nextArg:i:1 == $opt_ ]] && foundOpt=1 && break 2
                done <<<$optLetters
            done

            ((foundOpt)) && return
        fi

        # Extract type of optional arg if specified
        local optArgType=$(perlGetCaptures "$optstringOrg" "$name(?:$optionalArgTypeCaptureRE)" '$1$2')

        local nextArgIsOptArg=0

        case $optArgType in
            /*/) # Check if $nextArg matches regexp
                perlMatch "$nextArg" "$optArgType" && nextArgIsOptArg=1
                ;;
            [si]) # Check if $nextArg is an int
                local nextArgIsInt=0

                [[ $nextArg =~ ^[0-9]+$ ]] && nextArgIsInt=1

                # Test if specified type and arg type match (see (*) above).
                # N.B.: We need command groups since && and || between commands have same precedence.
                 [[ $optArgType == i ]] && ((nextArgIsInt)) ||  [[ $optArgType == s ]] && ((! nextArgIsInt)); ;  && nextArgIsOptArg=1
                ;;
            '') # No type or regexp specified => Assume $nextArg is optional arg.
                nextArgIsOptArg=1
                ;;
        esac

        if ((nextArgIsOptArg)); then
            OPTARG=$nextArg && ((OPTIND++))
        fi
    fi


# Uses perl to match \<string\> against \<regexp\>.\
# Returns with code 0 on a match and 1 otherwise.
function perlMatch # Args: <string> <regexp>

    perl -e 'q('"$1"') =~ '"$2"' and exit 0; exit 1;'


# Uses perl to match \<string\> against \<regexp\>
# and prints each capture on a separate line.\
# If \<regexp\> contains more than one capture group,
# you must specify the \<line format\> which is an
# arbitrary Perl string containing your desired backrefs.\
# By default, merely $1 will be printed.
function perlGetCaptures # Args: <string> <regexp> [<line format>]

    local lineFmt=$3:-\$1

    # Matching repeatedly with g option gives one set of captures at a time.
    perl -e 'while (q('"$1"') =~ m#'"$2"'#g)  print(qq('"$lineFmt"') . "\n"); '

如果你不需要它们,函数体内没有 cmets 的相同脚本:

#!/bin/bash

# Invocation: getopts-plus optstring name "$@"\
# \
# optstring: Like normal getopts, but you may specify options with optional argument
# by appending :: to the option letter.\
# \
# However, if your script supports an invocation with an option with optional
# argument as the only option argument, followed by a non-option argument,
# the non-option argument will be considered to be the argument for the option.\
# \
# If you're lucky and the optional argument is expected to be an integer, whereas
# the non-option argument is a string or vice versa, you may specify the type by
# appending :::i for an integer or :::s for a string to solve that issue.\
# \
# If that doesn't apply, you may specify a Perl regexp for the optional arg by appending
# ::/.../ to the option letter.\
# See here for an introduction to Perl regexps: https://perldoc.perl.org/perlretut
# Please note: ATM, only /.../ will be recognised as a regexp after ::,\
# i. e. neither other delimiters, nor modifiers may be used, so e. g. m#...#a will
# not be recognised.\
# If there is a non-option argument after the option with optional argument, it will
# be considered to be the optional argument only if it matches the regexp.\
# To be clear: ::/.../ is not meant for argument validation but solely to discriminate
# between arguments for options with optional argument and non-option arguments.
function getopts-plus

    local optstring=$1
    local -n name=$2

    shift 2

    local optionalArgSuffixRE='::(?::[si]|/.*?/)?'
    local optionalArgTypeCaptureRE=':::([si])|::(/.*?/)'

    local opt_

    local -A isOptWithOptionalArg

    while read opt_; do
        isOptWithOptionalArg[$opt_]=1
    done <<<$(perlGetCaptures "$optstring" "([a-zA-Z])$optionalArgSuffixRE")

    local optLetters=$(perlGetCaptures "$optstring" "([a-zA-Z])(?:$optionalArgSuffixRE|:)?")

    local optstringOrg=$optstring
    optstring=$(perl -pe "s#$optionalArgSuffixRE##g" <<<$optstring)

    getopts $optstring name "$@" || return

    if ((isOptWithOptionalArg[$name])) && [[ $!OPTIND ]]; then
        local nextArg=$!OPTIND foundOpt=0

        if [[ $nextArg == -* ]]; then
            local i

            for ((i = 1; i < $#nextArg; i++)); do
                while read opt_; do
                    [[ $nextArg:i:1 == $opt_ ]] && foundOpt=1 && break 2
                done <<<$optLetters
            done

            ((foundOpt)) && return
        fi

        local optArgType=$(perlGetCaptures "$optstringOrg" "$name(?:$optionalArgTypeCaptureRE)" '$1$2')

        local nextArgIsOptArg=0

        case $optArgType in
            /*/)
                perlMatch "$nextArg" "$optArgType" && nextArgIsOptArg=1
                ;;
            [si])
                local nextArgIsInt=0

                [[ $nextArg =~ ^[0-9]+$ ]] && nextArgIsInt=1

                 [[ $optArgType == i ]] && ((nextArgIsInt)) ||  [[ $optArgType == s ]] && ((! nextArgIsInt)); ;  && nextArgIsOptArg=1
                ;;
            '')
                nextArgIsOptArg=1
                ;;
        esac

        if ((nextArgIsOptArg)); then
            OPTARG=$nextArg && ((OPTIND++))
        fi
    fi


# Uses perl to match \<string\> against \<regexp\>.\
# Returns with code 0 on a match and 1 otherwise.
function perlMatch # Args: <string> <regexp>

    perl -e 'q('"$1"') =~ '"$2"' and exit 0; exit 1;'


# Uses perl to match \<string\> against \<regexp\>
# and prints each capture on a separate line.\
# If \<regexp\> contains more than one capture group,
# you must specify the \<line format\> which is an
# arbitrary Perl string containing your desired backrefs.\
# By default, merely $1 will be printed.
function perlGetCaptures # Args: <string> <regexp> [<line format>]

    local lineFmt=$3:-\$1

    perl -e 'while (q('"$1"') =~ m#'"$2"'#g)  print(qq('"$lineFmt"') . "\n"); '

一些使用最新版本的测试:

-g 的可选 arg 类型指定为整数,未传递 int 但后跟非选项字符串 arg。

$ . ./getopts-plus.sh
$ while getopts-plus 'b:c::de::f::g:::ia' opt -ab 99 -c 11 -def 55 -g "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done

opt == 'a'

OPTARG == ''

-------------------------

opt == 'b'

OPTARG == '99'

-------------------------

opt == 'c'

OPTARG == '11'

-------------------------

opt == 'd'

OPTARG == ''

-------------------------

opt == 'e'

OPTARG == ''

-------------------------

opt == 'f'

OPTARG == '55'

-------------------------

opt == 'g'

OPTARG == '' <-- Empty because "hello you" is not an int

与上面类似,但使用 int arg。

$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ia' opt -ab 99 -c 11 -def 55 -g 7 "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done

opt == 'a'

OPTARG == ''

-------------------------

opt == 'b'

OPTARG == '99'

-------------------------

opt == 'c'

OPTARG == '11'

-------------------------

opt == 'd'

OPTARG == ''

-------------------------

opt == 'e'

OPTARG == ''

-------------------------

opt == 'f'

OPTARG == '55'

-------------------------

opt == 'g'

OPTARG == '7' <-- The passed int

添加了可选选项-h 和正则表达式/^(a|b|ab|ba)$/,没有传递参数。

$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/a' opt -ab 99 -c 11 -def 55 -gh "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done

opt == 'a'

OPTARG == ''

-------------------------

opt == 'b'

OPTARG == '99'

-------------------------

opt == 'c'

OPTARG == '11'

-------------------------

opt == 'd'

OPTARG == ''

-------------------------

opt == 'e'

OPTARG == ''

-------------------------

opt == 'f'

OPTARG == '55'

-------------------------

opt == 'g'

OPTARG == ''

-------------------------

opt == 'h'

OPTARG == '' <-- Empty because "hello you" does not match the regexp

与上面类似,但 arg 与正则表达式匹配。

$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/a' opt -ab 99 -c 11 -def 55 -gh ab "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done

opt == 'a'

OPTARG == ''

-------------------------

opt == 'b'

OPTARG == '99'

-------------------------

opt == 'c'

OPTARG == '11'

-------------------------

opt == 'd'

OPTARG == ''

-------------------------

opt == 'e'

OPTARG == ''

-------------------------

opt == 'f'

OPTARG == '55'

-------------------------

opt == 'g'

OPTARG == ''

-------------------------

opt == 'h'

OPTARG == 'ab' <-- The arg that matches the regexp

添加了另一个正则表达式类型的可选选项-i 和正则表达式/^\w+$/(使用Perl 标记\w,表示字母数字或下划线),没有传递任何参数。

$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/ai::/^\w+$/' opt -ab 99 -c 11 -def 55 -gh ab -i "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
[23:10:49]

opt == 'a'

OPTARG == ''

-------------------------

opt == 'b'

OPTARG == '99'

-------------------------

opt == 'c'

OPTARG == '11'

-------------------------

opt == 'd'

OPTARG == ''

-------------------------

opt == 'e'

OPTARG == ''

-------------------------

opt == 'f'

OPTARG == '55'

-------------------------

opt == 'g'

OPTARG == ''

-------------------------

opt == 'h'

OPTARG == 'ab'

-------------------------

opt == 'i'

OPTARG == '' <-- Empty because "hello you" contains a space.

与上面类似,但 arg 与正则表达式匹配。

$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/ai::/^\w+$/' opt -ab 99 -c 11 -def 55 -gh ab -i foo_Bar_1 "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
[23:15:23]

opt == 'a'

OPTARG == ''

-------------------------

opt == 'b'

OPTARG == '99'

-------------------------

opt == 'c'

OPTARG == '11'

-------------------------

opt == 'd'

OPTARG == ''

-------------------------

opt == 'e'

OPTARG == ''

-------------------------

opt == 'f'

OPTARG == '55'

-------------------------

opt == 'g'

OPTARG == ''

-------------------------

opt == 'h'

OPTARG == 'ab'

-------------------------

opt == 'i'

OPTARG == 'foo_Bar_1' <-- Matched because it contains only alphanumeric chars and underscores.

【讨论】:

以上是关于带有 getopts 的可选选项参数的主要内容,如果未能解决你的问题,请参考以下文章

Getopt可选参数?

shell 脚本参数解析之 getopt getopts

[记录]Shell中的getopts和getopt用法

如何在你的 shell 中使用互斥标志并添加一个可选参数标志(使用 getopts)

在 Perl 中使用 Getopt 时如何对参数进行分组?

getopt 不将可选参数解析为参数