如何使用getopts忽略无效参数并继续解析?

5
我有以下简单的代码:
#!/usr/bin/env bash
while getopts :f arg; do
  case $arg in
    f) echo Option $arg specified. ;;
    *) echo Unknown option: $OPTARG. ;;
  esac
done

它适用于简单的场景,例如:

和它一起工作。

$ ./test.sh -f
Option f specified.
$ ./test.sh -a -f
Unknown option: a.
Option f specified.

然而,它对以下内容无效:
$ ./test.sh foo -f

$ ./test.sh -a abc -f
Unknown option: a.

如何修复上面的代码示例以支持无效参数?


为了支持无效参数,以识别-f已被指定为未知参数,因此-a abc -f仍将识别-f,但似乎getopts一旦发现无效参数就会停止处理。 - kenorb
它停在第一个非选项参数(即 abc)处。例如,-a -a -a -a -f 将四次打印未知。 - Etan Reisner
在我看来,它停止的行为相当奇怪。我实际上需要它来编写更复杂的脚本,其中涉及多次调用 getopts(但我不想让它比现在更复杂)。因此,第一次调用处理所有已知选项,后续调用仅检查特定参数,但它们失败了,因为它们无法识别之前的参数(而我在后续调用中并不关心)。 - kenorb
是的,我要么一次性解析它们,要么就这样做。记住,在case语句中,你不需要实际上处理任何其他选项,你可以完全忽略它们,只处理你关心的那些。 - Etan Reisner
我刚刚遇到了这种行为。我认为它必须被归类为getopts中的一个bug,而且可能是一个危险的bug。也许你可以提交一个bug报告? - Łukasz Grabowski
显示剩余3条评论
3个回答

5

看起来 getopts 一旦发现未知的非选项参数 (abc),就会简单地退出循环。

我找到了以下解决方法,通过将 getopts 循环包装在另一个循环中:

#!/usr/bin/env bash
while :; do
  while getopts :f arg; do
    case $arg in
      f)
        echo Option $arg specified.
        ;;
      *)
        echo Unknown option: $OPTARG.
        ;;
    esac
  done
  ((OPTIND++)) 
  [ $OPTIND -gt $# ] && break
done

当达到最大参数时,跳过无效参数并结束循环。

输出:

$ ./test.sh abc -f
Option f specified.
$ ./test.sh -a abc -f
Unknown option: a.
Option f specified.

-a 不是问题,而是非选项值。从示例中删除 fooabc,它们就可以正常工作(与您最初的示例一样)。这就是 getopts 的功能。 - Etan Reisner
你是否真的在这些示例中关心 abc 值?因为这个双重循环意味着如果你真的关心,你将不得不再次手动遍历参数并手动查找它(通过自己模拟 getopts 逻辑)。 - Etan Reisner
@EtanReisner 我不在意那些未知的参数,比如 abc,因为我可以在脚本的其他地方再次阅读它。 - kenorb

3
这个主题帮助我找到了答案,以便在尝试检测命令行上是否存在特定参数时进行操作。 我以不同的方式实现它,所以想分享我的解决方案。 代码中包含注释,这将有助于理解此实现。 还包括一些已注释的行,用于调试目的。
###############################################################################
#
# Convenience method to test if a command line option is present.
#
# Parameters:
#   $1 - command line argument to match against
#   $2 - command line parameters
#
# Example:
#   is_cmd_line_option_present "v" "$@" 
#       check if the -v option has been provided on the command line
#
###############################################################################
function is_cmd_line_option_present()
{
    _iclop_option="$1"
    # remove $1 from the arguments (via the shift command) to this method before searching for it from the actual command line
    shift
    # Default the return value to zero
    _iclop_return=0

    # Don't need to increment OPTIND each time, as the getopts call does that for us
    for (( OPTIND=1; OPTIND <= $#; ))
    do
        # Use getopts to parse each command line argument, and test for a match
        if getopts ":${_iclop_option}" _iclop_option_var; then
            if [ "${_iclop_option_var}" == "${_iclop_option}" ]; then
                _iclop_return=1
             # else
                # (>&2 echo -e "[Std Err]: is_cmd_line_option_present - Option discarded _iclop_option_var: [${_iclop_option_var}]")
            fi
         else
            # (>&2 echo -e "[Std Err]: is_cmd_line_option_present - Unknown Option Parameter _iclop_option_var: [${_iclop_option_var}]")
            # Need to increment the option indicator when an option is found that isn't listed as an expected option, as getopts won't do this for us.
            ((OPTIND++))
        fi 
    done

    # (>&2 echo -e "[Std Err]: is_cmd_line_option_present end - _iclop_return: [${_iclop_return}]")
    return $_iclop_return;
}

1
以下是一种非常局限的解决方法,它有自己的问题,但至少在我的使用情况下有效。我怀疑OP的问题提供了一个最小的示例来展示问题,所以它可能不适用于“真正”的问题。
params=0
 while getopts :f arg; do
  params=1
  case $arg in
    f) echo Option $arg specified. ;;
    *) echo Unknown option: $OPTARG. ;;
  esac
done
if [[ ! $@ == '' ]] && ((params == 0 )); then
    echo "wrong arguments"
    exit 1
fi

更通用的方法是在while循环中递增params,然后检查params是否等于$@中形式为“ -[aA-zZ]”的子字符串数量。 - Łukasz Grabowski
我喜欢这个比“循环嵌套”选项更好,而且它相当简单明了。 - Ogre Psalm33

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接