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

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

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

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

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


当前回答

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那么灵活,而且你必须在用例实例中自己做很多错误检查代码……

但这是一种选择。

其他回答

如果你不想要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功能和长选项,这是一种简单的方法。

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

内置OS X (BSD) getopt不支持长选项,但GNU版本支持:brew install GNU -getopt。然后,类似于:cp /usr/local/Cellar/gnu-getopt/1.1.6/bin/getopt /usr/local/bin/gnu-getopt。

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

短选项 长选项 不管有没有论点 非选项参数(没有“-”或“——”的参数) 顺序不重要(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解析长选项名

长选项可以被内置的标准getopts作为- " option "的"参数"解析。

这是可移植的本地POSIX shell -不需要外部程序或bashisms。

本指南将长选项作为-选项的参数实现,因此——alpha被getopts视为参数alpha为-,而——bravo=foo被参数bravo=foo视为-。true实参通过shell参数展开获取,更新$OPT和$OPTARG。

在本例中,-b和-c(以及它们的长形式——bravo和——charlie)具有强制实参。长选项的参数出现在等号之后,例如——bravo=foo(长选项的空格分隔符很难实现,参见下文)。

因为它使用了内置的getopts,所以这个解决方案支持像cmd——bravo=foo -ac FILE这样的用法(它组合了选项-a和-c,并将长选项与标准选项交织在一起),而这里的大多数其他答案要么很难做到,要么无法做到这一点。

die() { echo "$*" >&2; exit 2; }  # complain to STDERR and exit with error
needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }

while getopts ab:c:-: OPT; do
  # support long options: https://stackoverflow.com/a/28466267/519360
  if [ "$OPT" = "-" ]; then   # long option: reformulate OPT and OPTARG
    OPT="${OPTARG%%=*}"       # extract long option name
    OPTARG="${OPTARG#$OPT}"   # extract long option argument (may be empty)
    OPTARG="${OPTARG#=}"      # if long option argument, remove assigning `=`
  fi
  case "$OPT" in
    a | alpha )    alpha=true ;;
    b | bravo )    needs_arg; bravo="$OPTARG" ;;
    c | charlie )  needs_arg; charlie="$OPTARG" ;;
    ??* )          die "Illegal option --$OPT" ;;  # bad long option
    ? )            exit 2 ;;  # bad short option (error reported via getopts)
  esac
done
shift $((OPTIND-1)) # remove parsed options and args from $@ list

When the option is a dash (-), it is a long option. getopts will have parsed the actual long option into $OPTARG, e.g. --bravo=foo originally sets OPT='-' and OPTARG='bravo=foo'. The if stanza sets $OPT to the contents of $OPTARG before the first equals sign (bravo in our example) and then removes that from the beginning of $OPTARG (yielding =foo in this step, or an empty string if there is no =). Finally, we strip the argument's leading =. At this point, $OPT is either a short option (one character) or a long option (2+ characters).

The case then matches either short or long options (the pipe, |, indicates that "or" operation. A long-only option like delta ) delta=true ;; doesn't need a pipe). For short options, getopts automatically complains about options and missing arguments, so we have to replicate those manually using the needs_arg function, which fatally exits when $OPTARG is empty. The ??* condition will match any remaining long option (? matches a single character and * matches zero or more, so ??* matches 2+ characters), allowing us to issue the "Illegal option" error before exiting.

与正常的gnu风格的长选项一样,提供——将停止解析,因此- -- --bravo=4将把$alpha设置为true,但$bravo将保持不变,$1将-bravo=4。我不能说我建议用前导破折号来命名文件,但这是表示它们不是选项的方法。


小错误:如果有人给出了一个无效的单字符长选项(它也不是一个短选项),这将退出一个错误,但没有消息(这个实现假设它是一个短选项)。您可以在case之前的条件中使用一个额外的变量来跟踪它,然后在最后的case条件中测试它,但我认为这是一个太麻烦的角落情况。

大写变量名:一般情况下,建议保留全大写变量供系统使用。我将$OPT保留为全大写,以与$OPTARG保持一致,但这确实打破了这种约定。我认为这很合适,因为这是系统应该做的事情,它应该是安全的;我还没有听说过任何使用这个变量名的标准。

要抱怨长选项的非预期实参:用翻转测试模拟needs_argg来抱怨一个非预期实参:

no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }

要接受带空格分隔参数的长选项:您可以用eval "ARG_B=\"\$$OPTIND\""(或使用bash的间接展开,ARG_B="${!OPTIND}")拉入下一个参数,然后增加$OPTIND,正如这个答案的旧版本所指出的那样,但它不可靠;getopts可以过早地终止,假设参数超出了它的作用域,并且一些实现不太适合手动操作$OPTIND。

Bash内置的getopts函数可以通过在optspec中放入破折号和冒号来解析长选项:

#!/usr/bin/env bash 
optspec=":hv-:"
while getopts "$optspec" optchar; do
    case "${optchar}" in
        -)
            case "${OPTARG}" in
                loglevel)
                    val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
                    echo "Parsing option: '--${OPTARG}', value: '${val}'" >&2;
                    ;;
                loglevel=*)
                    val=${OPTARG#*=}
                    opt=${OPTARG%=$val}
                    echo "Parsing option: '--${opt}', value: '${val}'" >&2
                    ;;
                *)
                    if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                        echo "Unknown option --${OPTARG}" >&2
                    fi
                    ;;
            esac;;
        h)
            echo "usage: $0 [-v] [--loglevel[=]<value>]" >&2
            exit 2
            ;;
        v)
            echo "Parsing option: '-${optchar}'" >&2
            ;;
        *)
            if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                echo "Non-option argument: '-${OPTARG}'" >&2
            fi
            ;;
    esac
done

拷贝到当前工作目录中的可执行文件name=getopts_test.sh后,可以生成如下输出

$ ./getopts_test.sh
$ ./getopts_test.sh -f
Non-option argument: '-f'
$ ./getopts_test.sh -h
usage: code/getopts_test.sh [-v] [--loglevel[=]<value>]
$ ./getopts_test.sh --help
$ ./getopts_test.sh -v
Parsing option: '-v'
$ ./getopts_test.sh --very-bad
$ ./getopts_test.sh --loglevel
Parsing option: '--loglevel', value: ''
$ ./getopts_test.sh --loglevel 11
Parsing option: '--loglevel', value: '11'
$ ./getopts_test.sh --loglevel=11
Parsing option: '--loglevel', value: '11'

显然,getopts既不执行OPTERR检查,也不对长选项执行选项参数解析。上面的脚本片段展示了如何手动完成这一工作。基本原理也适用于Debian Almquist shell(“破折号”)。注意特殊情况:

getopts -- "-:"  ## without the option terminator "-- " bash complains about "-:"
getopts "-:"     ## this works in the Debian Almquist shell ("dash")

请注意,正如http://mywiki.wooledge.org/BashFAQ上的GreyCat指出的那样,这个技巧利用了shell的非标准行为,允许选项参数(即“-ffilename”中的文件名)连接到选项(如“-ffilename”)。POSIX标准规定它们之间必须有一个空格,在"——longoption"的情况下,空格将终止选项解析并将所有长选项转换为非选项参数。