我希望使用shell脚本调用长形式和短形式的命令行选项。

我知道可以使用getopts,但是像在Perl中一样,我不能用shell做同样的事情。

关于如何做到这一点的任何想法,这样我就可以使用如下选项:

./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/

在上面,这两个命令对我的shell意味着同样的事情,但使用getopts,我还不能实现这些?


当前回答

公认的答案很好地指出了bash内置getopts的所有缺点。答案以:

因此,虽然可以编写更多的代码来解决长选项支持不足的问题,但工作量要大得多,并且在一定程度上违背了使用getopt解析器来简化代码的目的。

尽管我原则上同意这种说法,但我觉得我们在各种脚本中实现这个特性的次数证明,我们应该花一些精力来创建一个“标准化”的、经过良好测试的解决方案。

因此,我通过在纯bash中实现getopts_long“升级”了内置在getopts中的bash,没有外部依赖。该函数的使用与内置的getopts 100%兼容。

通过在脚本中包含getopts_long(在GitHub上托管),可以简单地实现原始问题的答案:

source "${PATH_TO}/getopts_long.bash"

while getopts_long ':c: copyfile:' OPTKEY; do
    case ${OPTKEY} in
        'c'|'copyfile')
            echo 'file supplied -- ${OPTARG}'
            ;;
        '?')
            echo "INVALID OPTION -- ${OPTARG}" >&2
            exit 1
            ;;
        ':')
            echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2
            exit 1
            ;;
        *)
            echo "Misconfigured OPTSPEC or uncaught option -- ${OPTKEY}" >&2
            exit 1
            ;;
    esac
done

shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift

其他回答

这需要一些时间,但我想要全部:

短选项 长选项 不管有没有论点 非选项参数(没有“-”或“——”的参数) 顺序不重要(script.sh /file -V或script.sh -V /file) 抓住错误用法 在不同的脚本中使用它作为模块,而不需要更改多行代码

最后,我提出了下面的解决方案,它使用getopt来捕获错误并将非选项移动到列表的末尾,然后再使用getopts来解析短选项和长选项。

所有的选项都会自动解析,将它们的长选项名作为变量名(请看例子):

# create string of short options
opt_short=$(printf "%s" "${!options[@]}")

# create string of long options
opt_long="$(printf ",%s" "${options[@]}")"

# catch wrong options and move non-options to the end of the string
args=$(getopt -l "$opt_long" "$opt_short" "$@" 2> >(sed -e 's/^/stderr/g')) || echo -n "Error: " && echo "$args" | grep -oP "(?<=^stderr).*" && exit 1

# create new array of options
mapfile -t args < <(xargs -n1 <<< "$(echo "$args" | sed -E "s/(--[^ ]+) '/\1='/g")" )

# overwrite $@ (options)
set -- "${args[@]}"

# parse options ([h]=help sets the variable "$opt_help" and [V]="" sets the variable "$opt_V")
while getopts "$opt_short-:" opt; do

  echo "$opt:$OPTARG"

  # long option
  if [[ "$opt" == "-" ]]; then

    # extract long option name
    opt="${OPTARG%%=*}"

    # extract long option argument (may be empty)
    OPTARG="${OPTARG#"$opt"}"

    # remove "=" from long option argument
    OPTARG="${OPTARG#=}"

    # set variable name
    opt=opt_$opt

  # short option without argument uses long option name as variable name
  elif [[ "${options[$opt]+x}" ]] && [[ "${options[$opt]}" ]]; then
    opt=opt_${options[$opt]} 

  # short option with argument uses long option name as variable name
  elif [[ "${options[$opt:]+x}" ]] && [[ "${options[$opt:]}" ]]; then
    opt=opt_${options[$opt:]} 

  # short option without long option name uses short option name as variable name
  else
    opt=opt_$opt
  fi

  # remove double colon
  opt="${opt%:}" 

  # options without arguments are set to 1
  [[ ! $OPTARG ]] && OPTARG=1 

  # replace hyphen against underscore
  opt="${opt//-/_}"

  # set variable variables (replaces hyphen against underscore)
  printf -v "$opt" '%s' "$OPTARG" 

done

现在,我只需要定义所需的选项名称和源代码脚本:

# import options module
declare -A options=( [h]=help [f:]=file: [V]=verbose [0]=long_only: [s]="" )
source "/usr/local/bin/inc/options.sh";

# display help text
if [[ $opt_help ]]; then
  echo "help text"
  exit
fi

# output
echo "opt_help:$opt_help"
echo "opt_file:$opt_file"
echo "opt_verbose:$opt_verbose"
echo "opt_long_only:$opt_long_only"
echo "opt_short_only:$opt_s"
echo "opt_path:$1"
echo "opt_mail:$2"

在调用脚本时,可以完全随机地传递所有选项和非选项:

#             $opt_file     $1        $2       $opt_V  $opt_long_only $opt_s
# /demo.sh --file=file.txt /dir info@example.com -V --long_only=yes -s
opt_help:1
opt_file:file.txt
opt_verbose:1
opt_long_only:yes
opt_short_only:1
opt_path=/dir
opt_mail:info@example.com

笔记

在选项数组中,在选项名称后添加:以启用参数。 如果没有给出较长的选项名,则变量名将是$opt_X,其中X是较短的选项名。 如果您希望使用较长的选项名而不定义较短的选项名,则将数组索引设置为一个数字,如上面示例中使用[0]=long_only所做的那样。当然,每个数组下标必须是唯一的。

使用的技术

不使用临时文件捕获stderr 将字符串转换为数组 使用:来解析getopt参数 使用getopts解析长选项名

如果你不想要getopt依赖,你可以这样做:

while test $# -gt 0
do
  case $1 in

  # Normal option processing
    -h | --help)
      # usage and help
      ;;
    -v | --version)
      # version info
      ;;
  # ...

  # Special cases
    --)
      break
      ;;
    --*)
      # error unknown (long) option $1
      ;;
    -?)
      # error unknown (short) option $1
      ;;

  # FUN STUFF HERE:
  # Split apart combined short options
    -*)
      split=$1
      shift
      set -- $(echo "$split" | cut -c 2- | sed 's/./-& /g') "$@"
      continue
      ;;

  # Done with options
    *)
      break
      ;;
  esac

  # for testing purposes:
  echo "$1"

  shift
done

当然,这样你就不能使用长样式选项。如果你想添加缩短的版本(例如——verbos而不是——verbose),那么你需要手动添加这些。

但是如果您希望获得getopts功能和长选项,这是一种简单的方法。

我也把这个片段作为一个主旨。

可以考虑以下三种实现方式:

Bash内置的getopts。这不支持带有双破折号前缀的长选项名。它只支持单字符选项。 BSD UNIX实现的独立getopt命令(这是MacOS使用的)。这也不支持长选项。 独立getopt的GNU实现。GNU getopt(3)(由Linux上的命令行getopt(1)使用)支持解析长选项。


其他一些答案给出了使用bash内置getopts模拟长选项的解决方案。该解决方案实际上生成了一个字符为“-”的短选项。所以你得到“——”作为标志。然后,后面的所有内容都变成OPTARG,并使用嵌套的case测试OPTARG。

这很聪明,但也需要注意:

getopts不能强制执行opt规范。如果用户提供了无效的选项,它不能返回错误。在解析OPTARG时,您必须自己进行错误检查。 OPTARG用于长选项名称,当长选项本身有参数时,这会使使用复杂化。你最终不得不自己编写代码作为一个额外的案例。

因此,虽然可以编写更多的代码来解决长选项支持不足的问题,但工作量要大得多,并且在一定程度上违背了使用getopt解析器来简化代码的目的。

内置的getopts不能做到这一点。有一个外部的getopt(1)程序可以做到这一点,但是您只能在Linux上从util-linux包中获得它。它附带一个示例脚本getop -parse.bash。

还有一个getopts_long被写成shell函数。

Getopts“可以用来”解析长选项,只要你不希望它们有参数……

以下是如何:

$ cat > longopt
while getopts 'e:-:' OPT; do
  case $OPT in
    e) echo echo: $OPTARG;;
    -) #long option
       case $OPTARG in
         long-option) echo long option;;
         *) echo long option: $OPTARG;;
       esac;;
  esac
done

$ bash longopt -e asd --long-option --long1 --long2 -e test
echo: asd
long option
long option: long1
long option: long2
echo: test

如果尝试使用OPTIND获取长选项的参数,getopts将把它作为第一个不可选的位置参数,并停止解析任何其他参数。 在这种情况下,您最好使用简单的case语句手动处理它。

这将“总是”工作:

$ cat >longopt2
while (($#)); do
    OPT=$1
    shift
    case $OPT in
        --*) case ${OPT:2} in
            long1) echo long1 option;;
            complex) echo comples with argument $1; shift;;
        esac;;

        -*) case ${OPT:1} in
            a) echo short option a;;
            b) echo short option b with parameter $1; shift;;
        esac;;
    esac
done


$ bash longopt2 --complex abc -a --long -b test
comples with argument abc
short option a
short option b with parameter test

尽管它不像getopts那么灵活,而且你必须在用例实例中自己做很多错误检查代码……

但这是一种选择。