ChatGPT解决这个技术问题 Extra ChatGPT

如何在 Bash 中解析命令行参数?

说,我有一个用这一行调用的脚本:

./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile

或者这个:

./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile 

什么是可接受的解析方式,以便在每种情况下(或两者的某种组合)$v$f$d 都将设置为 true 并且 $outFile 将等于 {6 }?

对于 zsh 用户,有一个很棒的内置函数 zparseopts 可以执行以下操作: zparseopts -D -E -M -- d=debug -debug=d 并且在 $debug 数组中同时包含 -d--debug 如果使用其中一个,echo $+debug[1] 将返回 0 或 1。参考:zsh.org/mla/users/2011/msg00350.html
非常好的教程:linuxcommand.org/lc3_wss0120.php。我特别喜欢“命令行选项”示例。
我为您创建了一个脚本,它被称为 - github.com/unfor19/bargs
另请参阅 Giving a bash script the option to accepts flags, like a command?,了解详细的、临时的、长短选项解析器。它不会尝试处理附加到短选项的选项参数,也不会尝试使用 = 分隔选项名称和选项值的长选项(在这两种情况下,它只是假设选项值在下一个参数中)。它也不处理短选项聚类——这个问题不需要它。
This great tutorial by Baeldung 展示了在 bash 中处理命令行参数的 4 种方法,包括:1) 位置参数 $1$2 等,2) 带有 getopts${OPTARG} 的标志,3) 遍历所有参数($@) 和 4) 使用 $#$1shift 运算符循环所有参数。

M
Mateen Ulhaq

Bash 空格分隔(例如,--option 参数)

cat >/tmp/demo-space-separated.sh <<'EOF'
#!/bin/bash

POSITIONAL_ARGS=()

while [[ $# -gt 0 ]]; do
  case $1 in
    -e|--extension)
      EXTENSION="$2"
      shift # past argument
      shift # past value
      ;;
    -s|--searchpath)
      SEARCHPATH="$2"
      shift # past argument
      shift # past value
      ;;
    --default)
      DEFAULT=YES
      shift # past argument
      ;;
    -*|--*)
      echo "Unknown option $1"
      exit 1
      ;;
    *)
      POSITIONAL_ARGS+=("$1") # save positional arg
      shift # past argument
      ;;
  esac
done

set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters

echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)

if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 "$1"
fi
EOF

chmod +x /tmp/demo-space-separated.sh

/tmp/demo-space-separated.sh -e conf -s /etc /etc/hosts

复制粘贴上面的块的输出

FILE EXTENSION  = conf
SEARCH PATH     = /etc
DEFAULT         =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34    example.com

用法

demo-space-separated.sh -e conf -s /etc /etc/hosts

Bash Equals-Separated(例如,--option=argument)

cat >/tmp/demo-equals-separated.sh <<'EOF'
#!/bin/bash

for i in "$@"; do
  case $i in
    -e=*|--extension=*)
      EXTENSION="${i#*=}"
      shift # past argument=value
      ;;
    -s=*|--searchpath=*)
      SEARCHPATH="${i#*=}"
      shift # past argument=value
      ;;
    --default)
      DEFAULT=YES
      shift # past argument with no value
      ;;
    -*|--*)
      echo "Unknown option $i"
      exit 1
      ;;
    *)
      ;;
  esac
done

echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "DEFAULT         = ${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)

if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 $1
fi
EOF

chmod +x /tmp/demo-equals-separated.sh

/tmp/demo-equals-separated.sh -e=conf -s=/etc /etc/hosts

复制粘贴上面的块的输出

FILE EXTENSION  = conf
SEARCH PATH     = /etc
DEFAULT         =
Number files in SEARCH PATH with EXTENSION: 14
Last line of file specified as non-opt/last argument:
#93.184.216.34    example.com

用法

demo-equals-separated.sh -e=conf -s=/etc /etc/hosts

为了更好地理解 ${i#*=},请在 this guide 中搜索“子字符串删除”。它在功能上等同于调用一个不必要的子流程的 `sed 's/[^=]*=//' <<< "$i"` 或调用 两个 不必要的子流程的 `echo "$i" | sed 's/[^=]*=//'`

将 bash 与 getopt[s] 一起使用

getopt(1) 限制(较旧、相对较新的 getopt 版本):

无法处理空字符串参数

无法处理带有嵌入空格的参数

较新的 getopt 版本没有这些限制。有关详细信息,请参阅这些 docs

POSIX getopts

此外,POSIX shell 和其他提供的 getopts 没有这些限制。我包含了一个简单的 getopts 示例。

cat >/tmp/demo-getopts.sh <<'EOF'
#!/bin/sh

# A POSIX variable
OPTIND=1         # Reset in case getopts has been used previously in the shell.

# Initialize our own variables:
output_file=""
verbose=0

while getopts "h?vf:" opt; do
  case "$opt" in
    h|\?)
      show_help
      exit 0
      ;;
    v)  verbose=1
      ;;
    f)  output_file=$OPTARG
      ;;
  esac
done

shift $((OPTIND-1))

[ "${1:-}" = "--" ] && shift

echo "verbose=$verbose, output_file='$output_file', Leftovers: $@"
EOF

chmod +x /tmp/demo-getopts.sh

/tmp/demo-getopts.sh -vf /etc/hosts foo bar

复制粘贴上面的块的输出

verbose=1, output_file='/etc/hosts', Leftovers: foo bar

用法

demo-getopts.sh -vf /etc/hosts foo bar

getopts 的优点是:

它更便携,并且可以在 dash 等其他 shell 中工作。它可以以典型的 Unix 方式自动处理多个单个选项,例如 -vf 文件名。

getopts 的缺点是它只能处理短选项(-h,而不是 --help)而无需额外的代码。

getopts tutorial 解释了所有语法和变量的含义。在 bash 中,还有 help getopts,它可能提供信息。


这是真的吗?根据 Wikipedia,有一个更新的 GNU 增强版本的 getopt,它包括 getopts 的所有功能,然后是一些功能。 Ubuntu 13.04 上的 man getopt 输出 getopt - parse command options (enhanced) 作为名称,所以我认为这个增强版本现在是标准的。
在你的系统上某种方式是一个非常薄弱的前提来建立“标准”的假设。
@Livven,getopt 不是 GNU 实用程序,它是 util-linux 的一部分。
如果您使用 -gt 0,请在 esac 之后删除您的 shift,将所有 shift 增加 1 并添加这种情况:*) break;; 您可以处理非可选参数。例如:pastebin.com/6DJ57HTc
getopts "h?vf:" 应该是不带问号的 getopts "hvf:"。无法识别的参数在 $opt 中存储为 ?。引自 man builtins“The colon and question mark characters may not be used as option characters.”
R
Robert Siemer

没有答案展示了增强的 getopt。并且 top-voted answer 具有误导性:它要么忽略 -⁠vfd 样式短选项(由 OP 请求),要么忽略位置参数后的选项(也由 OP 请求);它忽略了解析错误。反而:

使用来自 util-linux 或以前的 GNU glibc.1 的增强型 getopt

它与 GNU glibc 的 C 函数 getopt_long() 一起使用。

此页面上没有其他解决方案可以做到这一切:在 arguments2 中处理空格、引用字符甚至二进制(非增强的 getopt 无法做到这一点)它可以在最后处理选项: script.sh -o outFile file1 file2 -v (getopts 不这样做)允许 =-style 长选项:script.sh --outfile=fileOut --infile fileIn(如果自解析则允许两者都很长)允许组合短选项,例如 -vfd(如果自解析则实际工作) 允许触摸选项参数,例如 -oOutfile 或 -vfdoOutfile

在 arguments2 中处理空格、引用字符甚至二进制(非增强的 getopt 不能这样做)

它可以在最后处理选项: script.sh -o outFile file1 file2 -v (getopts 不这样做)

允许 =-style 长选项: script.sh --outfile=fileOut --infile fileIn (如果自解析,则允许两者都很长)

允许组合短选项,例如 -vfd (如果自我解析,则实际工作)

允许触摸选项参数,例如 -oOutfile 或 -vfdoOutfile

已经太老了3,以至于没有任何 GNU 系统缺少它(例如,任何 Linux 都有它)。

您可以使用以下命令测试它的存在:getopt --test → return value 4。

其他 getopt 或 shell-builtin getopt 的用途有限。

以下调用

myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile
myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile
myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd
myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile

全部返回

verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile

使用以下 myscript

#!/bin/bash
# More safety, by turning some bugs into errors.
# Without `errexit` you don’t need ! and can replace
# ${PIPESTATUS[0]} with a simple $?, but I prefer safety.
set -o errexit -o pipefail -o noclobber -o nounset

# -allow a command to fail with !’s side effect on errexit
# -use return value from ${PIPESTATUS[0]}, because ! hosed $?
! getopt --test > /dev/null 
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
    echo 'I’m sorry, `getopt --test` failed in this environment.'
    exit 1
fi

# option --output/-o requires 1 argument
LONGOPTS=debug,force,output:,verbose
OPTIONS=dfo:v

# -regarding ! and PIPESTATUS see above
# -temporarily store output to be able to check for errors
# -activate quoting/enhanced mode (e.g. by writing out “--options”)
# -pass arguments only via   -- "$@"   to separate them correctly
! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    # e.g. return value is 1
    #  then getopt has complained about wrong arguments to stdout
    exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"

d=n f=n v=n outFile=-
# now enjoy the options in order and nicely split until we see --
while true; do
    case "$1" in
        -d|--debug)
            d=y
            shift
            ;;
        -f|--force)
            f=y
            shift
            ;;
        -v|--verbose)
            v=y
            shift
            ;;
        -o|--output)
            outFile="$2"
            shift 2
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Programming error"
            exit 3
            ;;
    esac
done

# handle non-option arguments
if [[ $# -ne 1 ]]; then
    echo "$0: A single input file is required."
    exit 4
fi

echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"

1 增强的 getopt 可用于大多数“bash 系统”,包括 Cygwin;在 OS X 上尝试 brew install gnu-getoptsudo port install getopt
2 POSIX exec() 约定没有可靠的方法在命令行参数中传递二进制 NULL;这些字节过早地结束了论点
3 1997 年或更早发布的第一个版本(我只追踪到 1997 年)


谢谢你。刚刚从 en.wikipedia.org/wiki/Getopts 的功能表中确认,如果您需要对长选项的支持,并且您不在 Solaris 上,那么 getopt 是可行的方法。
我相信 getopt 的唯一警告是,它不能方便地在包装脚本中使用,其中一个可能只有几个特定于包装脚本的选项,然后将非包装脚本选项传递给包装的可执行文件,完好无损。假设我有一个名为 mygrepgrep 包装器,并且我有一个特定于 mygrep 的选项 --foo,那么我不能执行 mygrep --foo -A 2,并让 -A 2 自动传递给 grep;我需要mygrep --foo -- -A 2这里是您解决方案之上的my implementation
@bobpaul 您关于 util-linux 的陈述是错误的并且具有误导性:该软件包在 Ubuntu/Debian 上被标记为“必需”。因此,它始终被安装。 – 你在谈论哪些发行版(你说它需要故意安装)?
请注意,至少在当前 10.14.3 之前,这在 Mac 上不起作用。发布的 getopt 是 1999 年的 BSD getopt...
@jjj 脚注 1 涵盖 OS X。 – 对于 OS X 开箱即用的解决方案,请查看其他问题和答案。或者说实话:对于真正的编程,不要使用 bash。 ;-)
I
Inanc Gumus

部署.sh

#!/bin/bash

while [[ "$#" -gt 0 ]]; do
    case $1 in
        -t|--target) target="$2"; shift ;;
        -u|--uglify) uglify=1 ;;
        *) echo "Unknown parameter passed: $1"; exit 1 ;;
    esac
    shift
done

echo "Where to deploy: $target"
echo "Should uglify  : $uglify"

用法:

./deploy.sh -t dev -u

# OR:

./deploy.sh --target dev --uglify

这就是我正在做的事情。如果我想支持以布尔标志 ./script.sh --debug dev --uglify fast --verbose 结束行,则必须 while [[ "$#" > 1 ]]。示例:gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
哇!简单干净!这就是我使用它的方式:gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58
这比粘贴到每个脚本中要好得多,而不是处理源代码或让人们想知道您的功能实际从哪里开始。
警告:这允许重复的参数,最新的参数为准。例如 ./script.sh -d dev -d prod 将导致 deploy == 'prod'。反正我用过:P :) :+1:
很好的答案,tnx!我把它缩短了一点 - while (( "$#" )); do 而不是 while [[ "$#" -gt 0 ]]; do
R
Rob Bednark

digitalpeer.com 稍作修改:

用法 myscript.sh -p=my_prefix -s=dirname -l=libname

#!/bin/bash
for i in "$@"
do
case $i in
    -p=*|--prefix=*)
    PREFIX="${i#*=}"

    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    ;;
    -l=*|--lib=*)
    DIR="${i#*=}"
    ;;
    --default)
    DEFAULT=YES
    ;;
    *)
            # unknown option
    ;;
esac
done
echo PREFIX = ${PREFIX}
echo SEARCH PATH = ${SEARCHPATH}
echo DIRS = ${DIR}
echo DEFAULT = ${DEFAULT}

为了更好地理解 ${i#*=},请在 this guide 中搜索“子字符串删除”。它在功能上等同于调用一个不必要的子流程的 `sed 's/[^=]*=//' <<< "$i"` 或调用 两个 不必要的子流程的 `echo "$i" | sed 's/[^=]*=//'`


整洁的!虽然这不适用于空格分隔的参数 à la mount -t tempfs ...。可以通过 while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;; 等解决此问题
这无法处理 -vfd 样式组合的短选项。
如果您想通用地评估 --option-option 而不是每次都重复 OPTION=$i,请使用 -*=*) 作为匹配模式和 eval ${i##*-}
I
Inanc Gumus
while [ "$#" -gt 0 ]; do
  case "$1" in
    -n) name="$2"; shift 2;;
    -p) pidfile="$2"; shift 2;;
    -l) logfile="$2"; shift 2;;

    --name=*) name="${1#*=}"; shift 1;;
    --pidfile=*) pidfile="${1#*=}"; shift 1;;
    --logfile=*) logfile="${1#*=}"; shift 1;;
    --name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;;
    
    -*) echo "unknown option: $1" >&2; exit 1;;
    *) handle_argument "$1"; shift 1;;
  esac
done

这个解决方案:

处理 -n arg 和 --name=arg

最后允许参数

如果有任何拼写错误,则显示正常错误

兼容,不使用 bashisms

可读,不需要在循环中维护状态


抱歉耽搁了。在我的脚本中,handle_argument 函数接收所有非选项参数。您可以将该行替换为您想要的任何内容,可能是 *) die "unrecognized argument: $1" 或将 args 收集到变量 *) args+="$1"; shift 1;; 中。
惊人!我已经测试了几个答案,但这是唯一适用于所有情况的答案,包括许多位置参数(在标志之前和之后)
简洁的代码,但使用 -n 而没有其他 arg 由于 shift 2 上的错误导致无限循环,发出 shift 两次而不是 shift 2。建议编辑。
我进行了编辑(正在等待审核)以添加一些有用的功能,同时保持代码简洁明了。对于更高级的功能,例如单个参数中的多个单字母选项,您应该尝试 getoptgetopts
R
Rob Bednark

getopt()/getopts() 是一个不错的选择。从 here 复制:

这个小脚本显示了“getopt”的简单用法:

#!/bin/bash
echo "Before getopt"
for i
do
  echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
  echo "-->$i"
done

我们所说的是允许使用 -a、-b、-c 或 -d 中的任何一个,但 -c 后跟一个参数(“c:”表示)。如果我们称其为“g”并尝试一下:

bash-2.05a$ ./g -abc foo
Before getopt
-abc
foo
After getopt
-->-a
-->-b
-->-c
-->foo
-->--

我们从两个参数开始,“getopt”分解选项并将每个选项放在自己的参数中。它还添加了“--”。


使用 $* 是对 getopt 的错误使用。 (它使用空格来处理参数。)请参阅 my answer 以了解正确用法。
为什么要让它变得更复杂?
@Matt J,如果您使用“$i”而不是$i,脚本的第一部分(对于i)将能够处理其中包含空格的参数。 getopts 似乎无法处理带空格的参数。在 for i 循环中使用 getopt 有什么好处?
b
bubla

我发现在脚本中编写可移植解析非常令人沮丧,以至于我编写了 Argbash - 一个 FOSS 代码生成器,可以为您的脚本生成参数解析代码,而且它具有一些不错的功能:

https://argbash.io


感谢您编写 argbash,我刚刚使用它,发现它运行良好。我主要选择 argbash,因为它是支持 OS X 10.11 El Capitan 上的旧 bash 3.x 的代码生成器。唯一的缺点是,与调用模块相比,代码生成器方法意味着您的主脚本中有相当多的代码。
您实际上可以使用 Argbash,它为您生成量身定制的解析库,您可以将其包含在您的脚本中,或者您可以将它放在单独的文件中并直接获取它。我添加了一个 example 来证明这一点,并且我也在文档中更明确地说明了这一点。
很高兴知道。该示例很有趣,但仍然不是很清楚-也许您可以将生成的脚本的名称更改为“parse_lib.sh”或类似名称,并显示主脚本调用它的位置(例如在包装脚本部分中,这是更复杂的用例)。
这些问题已在 argbash 的最新版本中得到解决:文档已得到改进,引入了快速入门 argbash-init 脚本,您甚至可以在 argbash.io/generate 上在线使用 argbash
R
Rob Bednark

我以较早的答案为起点来整理我旧的临时参数解析。然后我重构了以下模板代码。它处理长参数和短参数,使用 = 或空格分隔的参数,以及组合在一起的多个短参数。最后,它将所有非参数参数重新插入到 $1,$2.. 变量中。

#!/usr/bin/env bash

# NOTICE: Uncomment if your script depends on bashisms.
#if [ -z "$BASH_VERSION" ]; then bash $0 $@ ; exit $? ; fi

echo "Before"
for i ; do echo - $i ; done


# Code template for parsing command line parameters using only portable shell
# code, while handling both long and short params, handling '-f file' and
# '-f=file' style param data and also capturing non-parameters to be inserted
# back into the shell positional parameters.

while [ -n "$1" ]; do
        # Copy so we can modify it (can't modify $1)
        OPT="$1"
        # Detect argument termination
        if [ x"$OPT" = x"--" ]; then
                shift
                for OPT ; do
                        REMAINS="$REMAINS \"$OPT\""
                done
                break
        fi
        # Parse current opt
        while [ x"$OPT" != x"-" ] ; do
                case "$OPT" in
                        # Handle --flag=value opts like this
                        -c=* | --config=* )
                                CONFIGFILE="${OPT#*=}"
                                shift
                                ;;
                        # and --flag value opts like this
                        -c* | --config )
                                CONFIGFILE="$2"
                                shift
                                ;;
                        -f* | --force )
                                FORCE=true
                                ;;
                        -r* | --retry )
                                RETRY=true
                                ;;
                        # Anything unknown is recorded for later
                        * )
                                REMAINS="$REMAINS \"$OPT\""
                                break
                                ;;
                esac
                # Check for multiple short options
                # NOTICE: be sure to update this pattern to match valid options
                NEXTOPT="${OPT#-[cfr]}" # try removing single short opt
                if [ x"$OPT" != x"$NEXTOPT" ] ; then
                        OPT="-$NEXTOPT"  # multiple short opts, keep going
                else
                        break  # long form, exit inner loop
                fi
        done
        # Done with that param. move to next
        shift
done
# Set the non-parameters back into the positional parameters ($1 $2 ..)
eval set -- $REMAINS


echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'"
for i ; do echo - $i ; done

此代码无法处理带有如下参数的选项:-c1。并且使用 = 将短选项与其论点分开是不寻常的......
我在这段有用的代码中遇到了两个问题:1)“-c=foo”情况下的“shift”最终吃掉了下一个参数;和 2) 'c' 不应包含在可组合空头期权的“[cfr]”模式中。
I
Inanc Gumus
# As long as there is at least one more argument, keep looping
while [[ $# -gt 0 ]]; do
    key="$1"
    case "$key" in
        # This is a flag type option. Will catch either -f or --foo
        -f|--foo)
        FOO=1
        ;;
        # Also a flag type option. Will catch either -b or --bar
        -b|--bar)
        BAR=1
        ;;
        # This is an arg value type option. Will catch -o value or --output-file value
        -o|--output-file)
        shift # past the key and to the value
        OUTPUTFILE="$1"
        ;;
        # This is an arg=value type option. Will catch -o=value or --output-file=value
        -o=*|--output-file=*)
        # No need to shift here since the value is part of the same string
        OUTPUTFILE="${key#*=}"
        ;;
        *)
        # Do whatever you want with extra options
        echo "Unknown option '$key'"
        ;;
    esac
    # Shift after checking all the cases to get the next option
    shift
done

这允许您同时拥有空格分隔的选项/值,以及相等的定义值。

因此,您可以使用以下命令运行脚本:

./myscript --foo -b -o /fizz/file.txt

也:

./myscript -f --bar -o=/fizz/file.txt

并且两者都应该具有相同的最终结果。

优点:

允许 -arg=value 和 -arg value

适用于您可以在 bash 中使用的任何 arg 名称含义 -a 或 -arg 或 --arg 或 -arg 或其他

含义 -a 或 -arg 或 --arg 或 -arg 或其他

纯粹的狂欢。无需学习/使用 getopt 或 getopts

缺点:

不能组合 args 意味着没有 -abc。你必须做 -a -b -c

意思是没有-abc。你必须做 -a -b -c


我在这里有个问题。为什么使用 shift; OUTPUTFILE="$1" 而不是 OUTPUTFILE="$2"?也许它有一个简单的答案,但我是 bash 的新手
我相信你可以做任何一个,这真的只是个人喜好。在这种情况下,我只想在任何地方都将 $1 作为“活动”参数
p
phyatt

此示例说明如何使用 getopteval 以及 HEREDOCshift 来处理带和不带所需值的短参数和长参数。此外,switch/case 语句简洁易懂。

#!/usr/bin/env bash

# usage function
function usage()
{
   cat << HEREDOC

   Usage: $progname [--num NUM] [--time TIME_STR] [--verbose] [--dry-run]

   optional arguments:
     -h, --help           show this help message and exit
     -n, --num NUM        pass in a number
     -t, --time TIME_STR  pass in a time string
     -v, --verbose        increase the verbosity of the bash script
     --dry-run            do a dry run, dont change any files

HEREDOC
}  

# initialize variables
progname=$(basename $0)
verbose=0
dryrun=0
num_str=
time_str=

# use getopt and store the output into $OPTS
# note the use of -o for the short options, --long for the long name options
# and a : for any option that takes a parameter
OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; usage; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  # uncomment the next line to see how shift is working
  # echo "\$1:\"$1\" \$2:\"$2\""
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

if (( $verbose > 0 )); then

   # print out all the parameters we read in
   cat <<EOM
   num=$num_str
   time=$time_str
   verbose=$verbose
   dryrun=$dryrun
EOM
fi

# The rest of your script below

上面脚本中最重要的几行是:

OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@")
if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; exit 1 ; fi
eval set -- "$OPTS"

while true; do
  case "$1" in
    -h | --help ) usage; exit; ;;
    -n | --num ) num_str="$2"; shift 2 ;;
    -t | --time ) time_str="$2"; shift 2 ;;
    --dry-run ) dryrun=1; shift ;;
    -v | --verbose ) verbose=$((verbose + 1)); shift ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
done

简短、中肯、易读,并且几乎可以处理所有事情(恕我直言)。

希望对某人有所帮助。


这是最好的答案之一。
j
jchook

扩展@bruno-bronosky 的答案,我添加了一个“预处理器”来处理一些常见的格式:

将 --longopt=val 扩展为 --longopt val

将 -xyz 扩展为 -x -y -z

支持 -- 表示标志的结束

显示意外选项的错误

紧凑且易于阅读的选项开关

#!/bin/bash

# Report usage
usage() {
  echo "Usage:"
  echo "$(basename "$0") [options] [--] [file1, ...]"
}

invalid() {
  echo "ERROR: Unrecognized argument: $1" >&2
  usage
  exit 1
}

# Pre-process options to:
# - expand -xyz into -x -y -z
# - expand --longopt=arg into --longopt arg
ARGV=()
END_OF_OPT=
while [[ $# -gt 0 ]]; do
  arg="$1"; shift
  case "${END_OF_OPT}${arg}" in
    --) ARGV+=("$arg"); END_OF_OPT=1 ;;
    --*=*)ARGV+=("${arg%%=*}" "${arg#*=}") ;;
    --*) ARGV+=("$arg") ;;
    -*) for i in $(seq 2 ${#arg}); do ARGV+=("-${arg:i-1:1}"); done ;;
    *) ARGV+=("$arg") ;;
  esac
done

# Apply pre-processed options
set -- "${ARGV[@]}"

# Parse options
END_OF_OPT=
POSITIONAL=()
while [[ $# -gt 0 ]]; do
  case "${END_OF_OPT}${1}" in
    -h|--help)      usage; exit 0 ;;
    -p|--password)  shift; PASSWORD="$1" ;;
    -u|--username)  shift; USERNAME="$1" ;;
    -n|--name)      shift; names+=("$1") ;;
    -q|--quiet)     QUIET=1 ;;
    -C|--copy)      COPY=1 ;;
    -N|--notify)    NOTIFY=1 ;;
    --stdin)        READ_STDIN=1 ;;
    --)             END_OF_OPT=1 ;;
    -*)             invalid "$1" ;;
    *)              POSITIONAL+=("$1") ;;
  esac
  shift
done

# Restore positional parameters
set -- "${POSITIONAL[@]}"

这看起来很棒 - 但想知道 END_OF_OPT=1 是否真的需要在此行:--*) ARGV+=("$arg"); END_OF_OPT=1 ;;。如果留在其中,如果它包含在 --quiet 之后(或任何其他长样式布尔选项),它将无法解析 --username=fred。例如,script.sh --quiet --username=fredUnrecognized argument: --username=fred 失败(尽管 script.sh --quiet --username fred 工作正常)。我在我的脚本中取出了那个 END_OF_OPT=1,现在它可以工作了,但不确定这是否会破坏我不知道的其他情况。
I
Inanc Gumus

如果您正在制作可与其他实用程序互换的脚本,那么以下灵活性可能会很有用。

任何一个:

command -x=myfilename.ext --another_switch 

或者:

command -x myfilename.ext --another_switch

这是代码:

STD_IN=0

prefix=""
key=""
value=""
for keyValue in "$@"
do
  case "${prefix}${keyValue}" in
    -i=*|--input_filename=*)  key="-i";     value="${keyValue#*=}";; 
    -ss=*|--seek_from=*)      key="-ss";    value="${keyValue#*=}";;
    -t=*|--play_seconds=*)    key="-t";     value="${keyValue#*=}";;
    -|--stdin)                key="-";      value=1;;
    *)                                      value=$keyValue;;
  esac
  case $key in
    -i) MOVIE=$(resolveMovie "${value}");  prefix=""; key="";;
    -ss) SEEK_FROM="${value}";          prefix=""; key="";;
    -t)  PLAY_SECONDS="${value}";           prefix=""; key="";;
    -)   STD_IN=${value};                   prefix=""; key="";; 
    *)   prefix="${keyValue}=";;
  esac
done

A
Alek

我认为这个使用起来很简单:

#!/bin/bash
#

readopt='getopts $opts opt;rc=$?;[ "$rc$opt" = "0?" ]&&exit 1;[ $rc = 0 ]||{ shift $[OPTIND-1];false; }'

opts=vfdo:

# Enumerating options
while eval "$readopt"
do
    echo OPT:$opt ${OPTARG+OPTARG:$OPTARG}
done

# Enumerating arguments
for arg
do
    echo ARG:$arg
done

调用示例:

./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile
OPT:v 
OPT:d 
OPT:o OPTARG:/fizz/someOtherFile
OPT:f 
ARG:./foo/bar/someFile

我都读了,这本是我最喜欢的。我不喜欢使用 -a=1 作为 argc 样式。我更喜欢首先放置主要选项 -options,然后是具有单间距 -o option 的特殊选项。我正在寻找阅读 argvs 的最简单与更好的方法。
它工作得非常好,但是如果您将参数传递给非 a: 选项,则以下所有选项都将被视为参数。您可以使用自己的脚本检查此行 ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFile。 -d 选项未设置为 d:
O
Oleksii Chekulaiev

我给你函数 parse_params,它将从命令行解析参数。

这是一个纯 Bash 解决方案,没有额外的实用程序。不污染全局范围。毫不费力地为您返回简单易用的变量,您可以在其上构建进一步的逻辑。参数前的破折号数量无关紧要(--all 等于 -all 等于 all=all)

下面的脚本是一个复制粘贴工作演示。请参阅 show_use 函数以了解如何使用 parse_params

限制:

不支持空格分隔的参数 (-d 1) 参数名称将丢失破折号,因此 --any-param 和 -anyparam 是等效的 eval $(parse_params "$@") 必须在 bash 函数中使用(它在全局中不起作用范围)

#!/bin/bash

# Universal Bash parameter parsing
# Parse equal sign separated params into named local variables
# Standalone named parameter value will equal its param name (--force creates variable $force=="force")
# Parses multi-valued named params into an array (--path=path1 --path=path2 creates ${path[*]} array)
# Puts un-named params as-is into ${ARGV[*]} array
# Additionally puts all named params as-is into ${ARGN[*]} array
# Additionally puts all standalone "option" params as-is into ${ARGO[*]} array
# @author Oleksii Chekulaiev
# @version v1.4.1 (Jul-27-2018)
parse_params ()
{
    local existing_named
    local ARGV=() # un-named params
    local ARGN=() # named params
    local ARGO=() # options (--params)
    echo "local ARGV=(); local ARGN=(); local ARGO=();"
    while [[ "$1" != "" ]]; do
        # Escape asterisk to prevent bash asterisk expansion, and quotes to prevent string breakage
        _escaped=${1/\*/\'\"*\"\'}
        _escaped=${_escaped//\'/\\\'}
        _escaped=${_escaped//\"/\\\"}
        # If equals delimited named parameter
        nonspace="[^[:space:]]"
        if [[ "$1" =~ ^${nonspace}${nonspace}*=..* ]]; then
            # Add to named parameters array
            echo "ARGN+=('$_escaped');"
            # key is part before first =
            local _key=$(echo "$1" | cut -d = -f 1)
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # val is everything after key and = (protect from param==value error)
            local _val="${1/$_key=}"
            # remove dashes from key name
            _key=${_key//\-}
            # skip when key is empty
            # search for existing parameter name
            if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then
                # if name already exists then it's a multi-value named parameter
                # re-declare it as an array if needed
                if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then
                    echo "$_key=(\"\$$_key\");"
                fi
                # append new value
                echo "$_key+=('$_val');"
            else
                # single-value named parameter
                echo "local $_key='$_val';"
                existing_named=" $_key"
            fi
        # If standalone named parameter
        elif [[ "$1" =~ ^\-${nonspace}+ ]]; then
            # remove dashes
            local _key=${1//\-}
            # Just add as non-named when key is empty or contains space
            if [[ "$_key" == "" || "$_key" =~ " " ]]; then
                echo "ARGV+=('$_escaped');"
                shift
                continue
            fi
            # Add to options array
            echo "ARGO+=('$_escaped');"
            echo "local $_key=\"$_key\";"
        # non-named parameter
        else
            # Escape asterisk to prevent bash asterisk expansion
            _escaped=${1/\*/\'\"*\"\'}
            echo "ARGV+=('$_escaped');"
        fi
        shift
    done
}

#--------------------------- DEMO OF THE USAGE -------------------------------

show_use ()
{
    eval $(parse_params "$@")
    # --
    echo "${ARGV[0]}" # print first unnamed param
    echo "${ARGV[1]}" # print second unnamed param
    echo "${ARGN[0]}" # print first named param
    echo "${ARG0[0]}" # print first option param (--force)
    echo "$anyparam"  # print --anyparam value
    echo "$k"         # print k=5 value
    echo "${multivalue[0]}" # print first value of multi-value
    echo "${multivalue[1]}" # print second value of multi-value
    [[ "$force" == "force" ]] && echo "\$force is set so let the force be with you"
}

show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2

要使用演示解析进入 bash 脚本的参数,您只需执行 show_use "$@"
基本上我发现 github.com/renatosilva/easyoptions 以同样的方式做同样的事情,但比这个函数大一点。
H
Hive

如果 #1 已安装 getopts 并且 #2 您打算在同一平台上运行它,则 getopts 效果很好。 OSX 和 Linux(例如)在这方面表现不同。

这是一个支持等于、非等于和布尔标志的(非 getopts)解决方案。例如,您可以以这种方式运行您的脚本:

./script --arg1=value1 --arg2 value2 --shouldClean

# parse the arguments.
COUNTER=0
ARGS=("$@")
while [ $COUNTER -lt $# ]
do
    arg=${ARGS[$COUNTER]}
    let COUNTER=COUNTER+1
    nextArg=${ARGS[$COUNTER]}

    if [[ $skipNext -eq 1 ]]; then
        echo "Skipping"
        skipNext=0
        continue
    fi

    argKey=""
    argVal=""
    if [[ "$arg" =~ ^\- ]]; then
        # if the format is: -key=value
        if [[ "$arg" =~ \= ]]; then
            argVal=$(echo "$arg" | cut -d'=' -f2)
            argKey=$(echo "$arg" | cut -d'=' -f1)
            skipNext=0

        # if the format is: -key value
        elif [[ ! "$nextArg" =~ ^\- ]]; then
            argKey="$arg"
            argVal="$nextArg"
            skipNext=1

        # if the format is: -key (a boolean flag)
        elif [[ "$nextArg" =~ ^\- ]] || [[ -z "$nextArg" ]]; then
            argKey="$arg"
            argVal=""
            skipNext=0
        fi
    # if the format has not flag, just a value.
    else
        argKey=""
        argVal="$arg"
        skipNext=0
    fi

    case "$argKey" in 
        --source-scmurl)
            SOURCE_URL="$argVal"
        ;;
        --dest-scmurl)
            DEST_URL="$argVal"
        ;;
        --version-num)
            VERSION_NUM="$argVal"
        ;;
        -c|--clean)
            CLEAN_BEFORE_START="1"
        ;;
        -h|--help|-help|--h)
            showUsage
            exit
        ;;
    esac
done

K
Koichi Nakashima

另一个选项解析器(生成器)

一个优雅的 shell 脚本选项解析器(完全支持所有 POSIX shell)https://github.com/ko1nksm/getoptions(更新:2021-05-02 发布的 v3.3.0)

getoptions 是一个用 POSIX 兼容的 shell 脚本编写的新选项解析器(生成器),并于 2020 年 8 月发布。它适用于那些希望在 shell 脚本中支持 POSIX / GNU 样式选项语法的人。

支持的语法有 -a+a-abc-vvv-p VALUE-pVALUE--flag--no-flag--with-flag--without-flag--param VALUE、{12 },--option[=VALUE]--no-option --

它支持子命令、验证、缩写选项和自动帮助生成。并且适用于所有 POSIX shell(dash 0.5.4+、bash 2.03+、ksh88+、mksh R28+、zsh 3.1.9+、yash 2.29+、busybox ash 1.1.3+ 等)。

#!/bin/sh

VERSION="0.1"

parser_definition() {
  setup   REST help:usage -- "Usage: example.sh [options]... [arguments]..." ''
  msg -- 'Options:'
  flag    FLAG    -f --flag                 -- "takes no arguments"
  param   PARAM   -p --param                -- "takes one argument"
  option  OPTION  -o --option on:"default"  -- "takes one optional argument"
  disp    :usage  -h --help
  disp    VERSION    --version
}

eval "$(getoptions parser_definition) exit 1"

echo "FLAG: $FLAG, PARAM: $PARAM, OPTION: $OPTION"
printf '%s\n' "$@" # rest arguments

它解析以下参数:

example.sh -f --flag -p VALUE --param VALUE -o --option -oVALUE --option=VALUE 1 2 3

并自动生成帮助。

$ example.sh --help

Usage: example.sh [options]... [arguments]...

Options:
  -f, --flag                  takes no arguments
  -p, --param PARAM           takes one argument
  -o, --option[=OPTION]       takes one optional argument
  -h, --help
      --version

它也是一个选项解析器生成器,生成以下简单的选项解析代码。如果您使用生成的代码,则不需要 getoptions实现真正的可移植性和零依赖。

FLAG=''
PARAM=''
OPTION=''
REST=''
getoptions_parse() {
  OPTIND=$(($#+1))
  while OPTARG= && [ $# -gt 0 ]; do
    case $1 in
      --?*=*) OPTARG=$1; shift
        eval 'set -- "${OPTARG%%\=*}" "${OPTARG#*\=}"' ${1+'"$@"'}
        ;;
      --no-*|--without-*) unset OPTARG ;;
      -[po]?*) OPTARG=$1; shift
        eval 'set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}"' ${1+'"$@"'}
        ;;
      -[fh]?*) OPTARG=$1; shift
        eval 'set -- "${OPTARG%"${OPTARG#??}"}" -"${OPTARG#??}"' ${1+'"$@"'}
        OPTARG= ;;
    esac
    case $1 in
      '-f'|'--flag')
        [ "${OPTARG:-}" ] && OPTARG=${OPTARG#*\=} && set "noarg" "$1" && break
        eval '[ ${OPTARG+x} ] &&:' && OPTARG='1' || OPTARG=''
        FLAG="$OPTARG"
        ;;
      '-p'|'--param')
        [ $# -le 1 ] && set "required" "$1" && break
        OPTARG=$2
        PARAM="$OPTARG"
        shift ;;
      '-o'|'--option')
        set -- "$1" "$@"
        [ ${OPTARG+x} ] && {
          case $1 in --no-*|--without-*) set "noarg" "${1%%\=*}"; break; esac
          [ "${OPTARG:-}" ] && { shift; OPTARG=$2; } || OPTARG='default'
        } || OPTARG=''
        OPTION="$OPTARG"
        shift ;;
      '-h'|'--help')
        usage
        exit 0 ;;
      '--version')
        echo "${VERSION}"
        exit 0 ;;
      --)
        shift
        while [ $# -gt 0 ]; do
          REST="${REST} \"\${$(($OPTIND-$#))}\""
          shift
        done
        break ;;
      [-]?*) set "unknown" "$1"; break ;;
      *)
        REST="${REST} \"\${$(($OPTIND-$#))}\""
    esac
    shift
  done
  [ $# -eq 0 ] && { OPTIND=1; unset OPTARG; return 0; }
  case $1 in
    unknown) set "Unrecognized option: $2" "$@" ;;
    noarg) set "Does not allow an argument: $2" "$@" ;;
    required) set "Requires an argument: $2" "$@" ;;
    pattern:*) set "Does not match the pattern (${1#*:}): $2" "$@" ;;
    notcmd) set "Not a command: $2" "$@" ;;
    *) set "Validation error ($1): $2" "$@"
  esac
  echo "$1" >&2
  exit 1
}
usage() {
cat<<'GETOPTIONSHERE'
Usage: example.sh [options]... [arguments]...

Options:
  -f, --flag                  takes no arguments
  -p, --param PARAM           takes one argument
  -o, --option[=OPTION]       takes one optional argument
  -h, --help
      --version
GETOPTIONSHERE
}

l
leogama

另一个 Shell 参数解析器(ASAP)

POSIX 兼容,没有 getopt(s)

我受到相对简单的 answer by @bronson 的启发,并试图尝试改进它(不增加太多复杂性)。结果如下:

使用任何 -n [arg]、-abn [arg]、--name [arg] 和 --name=arg 样式的选项;

参数可以以任何顺序出现,循环后只有位置参数留在 $@ 中;

使用 -- 强制将剩余的参数视为位置参数;

检测无效选项和缺失参数;

不依赖于 getopt(s) 或外部工具(一个功能使用简单的 sed 命令);

便携,紧凑,可读性强,具有独立功能。

# Convenience functions.
usage_error () { echo >&2 "$(basename $0):  $1"; exit 2; }
assert_argument () { test "$1" != "$EOL" || usage_error "$2 requires an argument"; }

# One loop, nothing more.
if [ "$#" != 0 ]; then
  EOL=$(printf '\1\3\3\7')
  set -- "$@" "$EOL"
  while [ "$1" != "$EOL" ]; do
    opt="$1"; shift
    case "$opt" in

      # Your options go here.
      -f|--flag) flag='true';;
      -n|--name) assert_argument "$1" "$opt"; name="$1"; shift;;

      # Arguments processing. You may remove any unneeded line after the 1st.
      -|''|[!-]*) set -- "$@" "$opt";;                                          # positional argument, rotate to the end
      --*=*)      set -- "${opt%%=*}" "${opt#*=}" "$@";;                        # convert '--name=arg' to '--name' 'arg'
      -[!-]?*)    set -- $(echo "${opt#-}" | sed 's/\(.\)/ -\1/g') "$@";;       # convert '-abc' to '-a' '-b' '-c'
      --)         while [ "$1" != "$EOL" ]; do set -- "$@" "$1"; shift; done;;  # process remaining arguments as positional
      -*)         usage_error "unknown option: '$opt'";;                        # catch misspelled options
      *)          usage_error "this should NEVER happen ($opt)";;               # sanity test for previous patterns

    esac
  done
  shift  # $EOL
fi

# Do something cool with "$@"... \o/

注意: 我知道... 二进制模式 0x01030307 的参数可能会破坏逻辑。但是,如果有人在命令行中传递这样的参数,他们应该得到它。


很好的聪明方法。我从现在开始使用它,直到找到更好的方法或发现错误 ;-)
非常适合我以任何顺序混合位置和可选参数的用例,谢谢。
@leogama 是的,我在我的脚本中使用了这段代码,整体效果很好!继续努力👍👍
@leogama 你的 $EOL 不是二进制序列。它只是一个长度为 12 的可打印字符串(包含四个反斜杠)。您的意思是使用 printf 而不是 echo?我不知道这是否会影响 POSIX 合规性。
@PedroA 太好了!来自 GNU echo info page:“POSIX 不需要支持任何选项,并表示如果任何 STRING 包含反斜杠或第一个参数是'-n','echo' 的行为是实现定义的。便携式程序可以使用如果他们需要省略尾随换行符或输出控制字符或反斜杠,则使用 'printf' 命令。”在 POSIX 中至少有 printf ...
T
Thanh Trung

我想提交我的项目:https://github.com/flyingangel/argparser

source argparser.sh
parse_args "$@"

就那么简单。环境将填充与参数同名的变量


a
akostadinov

这就是我在函数中所做的,以避免破坏 getopts 在堆栈中更高位置同时运行:

function waitForWeb () {
   local OPTIND=1 OPTARG OPTION
   local host=localhost port=8080 proto=http
   while getopts "h:p:r:" OPTION; do
      case "$OPTION" in
      h)
         host="$OPTARG"
         ;;
      p)
         port="$OPTARG"
         ;;
      r)
         proto="$OPTARG"
         ;;
      esac
   done
...
}

g
galmok

我想提供我的选项解析版本,它允许以下内容:

-s p1
--stage p1
-w somefolder
--workfolder somefolder
-sw p1 somefolder
-e=hello

也允许这样做(可能是不需要的):

-s--workfolder p1 somefolder
-se=hello p1
-swe=hello p1 somefolder

您必须在使用前决定是否将 = 用于选项。这是为了保持代码干净(ish)。

while [[ $# > 0 ]]
do
    key="$1"
    while [[ ${key+x} ]]
    do
        case $key in
            -s*|--stage)
                STAGE="$2"
                shift # option has parameter
                ;;
            -w*|--workfolder)
                workfolder="$2"
                shift # option has parameter
                ;;
            -e=*)
                EXAMPLE="${key#*=}"
                break # option has been fully handled
                ;;
            *)
                # unknown option
                echo Unknown option: $key #1>&2
                exit 10 # either this: my preferred way to handle unknown options
                break # or this: do this to signal the option has been handled (if exit isn't used)
                ;;
        esac
        # prepare for next option in this key, if any
        [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}"
    done
    shift # option(s) fully processed, proceed to next input argument
done

${key+x} 上的“+x”是什么意思?
这是一个测试“key”是否存在。再往下我取消设置键,这打破了内部的while循环。
t
tmoschou

有几种方法可以解析 cmdline args(例如 GNU getopt(不可移植)vs BSD(MacOS)getopt vs getopts) - 都有问题。这个解决方案

是便携的!

零依赖,仅依赖 bash 内置

允许短期和长期选择

处理空格或同时在选项和参数之间使用 = 分隔符

支持串联短选项样式 -vxf

处理带有可选参数的选项(例如 --color 与 --color=always),

正确检测并报告未知选项

支持 -- 表示选项结束,以及

与相同功能集的替代方案相比,不需要代码膨胀。即简洁,因此更易于维护

示例:任何

# flag
-f
--foo

# option with required argument
-b"Hello World"
-b "Hello World"
--bar "Hello World"
--bar="Hello World"

# option with optional argument
--baz
--baz="Optional Hello"

#!/usr/bin/env bash

usage() {
  cat - >&2 <<EOF
NAME
    program-name.sh - Brief description
 
SYNOPSIS
    program-name.sh [-h|--help]
    program-name.sh [-f|--foo]
                    [-b|--bar <arg>]
                    [--baz[=<arg>]]
                    [--]
                    FILE ...

REQUIRED ARGUMENTS
  FILE ...
          input files

OPTIONS
  -h, --help
          Prints this and exits

  -f, --foo
          A flag option
      
  -b, --bar <arg>
          Option requiring an argument <arg>

  --baz[=<arg>]
          Option that has an optional argument <arg>. If <arg>
          is not specified, defaults to 'DEFAULT'
  --     
          Specify end of options; useful if the first non option
          argument starts with a hyphen

EOF
}

fatal() {
    for i; do
        echo -e "${i}" >&2
    done
    exit 1
}

# For long option processing
next_arg() {
    if [[ $OPTARG == *=* ]]; then
        # for cases like '--opt=arg'
        OPTARG="${OPTARG#*=}"
    else
        # for cases like '--opt arg'
        OPTARG="${args[$OPTIND]}"
        OPTIND=$((OPTIND + 1))
    fi
}

# ':' means preceding option character expects one argument, except
# first ':' which make getopts run in silent mode. We handle errors with
# wildcard case catch. Long options are considered as the '-' character
optspec=":hfb:-:"
args=("" "$@")  # dummy first element so $1 and $args[1] are aligned
while getopts "$optspec" optchar; do
    case "$optchar" in
        h) usage; exit 0 ;;
        f) foo=1 ;;
        b) bar="$OPTARG" ;;
        -) # long option processing
            case "$OPTARG" in
                help)
                    usage; exit 0 ;;
                foo)
                    foo=1 ;;
                bar|bar=*) next_arg
                    bar="$OPTARG" ;;
                baz)
                    baz=DEFAULT ;;
                baz=*) next_arg
                    baz="$OPTARG" ;;
                -) break ;;
                *) fatal "Unknown option '--${OPTARG}'" "see '${0} --help' for usage" ;;
            esac
            ;;
        *) fatal "Unknown option: '-${OPTARG}'" "See '${0} --help' for usage" ;;
    esac
done

shift $((OPTIND-1))

if [ "$#" -eq 0 ]; then
    fatal "Expected at least one required argument FILE" \
    "See '${0} --help' for usage"
fi

echo "foo=$foo, bar=$bar, baz=$baz, files=${@}"

佚名

保留未处理参数的解决方案。包括演示。

这是我的解决方案。它非常灵活,不像其他人,不应该需要外部包并干净地处理剩余的参数。

用法是:./myscript -flag flagvariable -otherflag flagvar2

您所要做的就是编辑validflags 行。它在前面加上一个连字符并搜索所有参数。然后它将下一个参数定义为标志名称,例如

./myscript -flag flagvariable -otherflag flagvar2
echo $flag $otherflag
flagvariable flagvar2

主要代码(简短版本,详细示例,下面还有一个错误版本):

#!/usr/bin/env bash
#shebang.io
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
    for flag in $validflags
    do
        sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers

带有内置 echo 演示的详细版本:

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
echo "all args
$@"
validflags="rate time number"
count=1
for arg in $@
do
    match=0
    argval=$1
#   argval=$(echo $@ | cut -d ' ' -f$count)
    for flag in $validflags
    do
            sflag="-"$flag
        if [ "$argval" == "$sflag" ]
        then
            declare $flag=$2
            match=1
        fi
    done
        if [ "$match" == "1" ]
    then
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done

#Cleanup then restore the leftovers
echo "pre final clear args:
$@"
shift $#
echo "post final clear args:
$@"
set -- $leftovers
echo "all post set args:
$@"
echo arg1: $1 arg2: $2

echo leftovers: $leftovers
echo rate $rate time $time number $number

最后一个,如果传递了一个无效的参数,这个错误就会出错。

#!/usr/bin/env bash
#shebang.io
rate=30
time=30
number=30
validflags="rate time number"
count=1
for arg in $@
do
    argval=$1
    match=0
        if [ "${argval:0:1}" == "-" ]
    then
        for flag in $validflags
        do
                sflag="-"$flag
            if [ "$argval" == "$sflag" ]
            then
                declare $flag=$2
                match=1
            fi
        done
        if [ "$match" == "0" ]
        then
            echo "Bad argument: $argval"
            exit 1
        fi
        shift 2
    else
        leftovers=$(echo $leftovers $argval)
        shift
    fi
    count=$(($count+1))
done
#Cleanup then restore the leftovers
shift $#
set -- $leftovers
echo rate $rate time $time number $number
echo leftovers: $leftovers

优点:它做了什么,它处理得很好。它保留了许多其他解决方案没有的未使用的参数。它还允许调用变量而无需在脚本中手动定义。如果没有给出相应的参数,它还允许预填充变量。 (参见详细示例)。

缺点:无法解析单个复杂的 arg 字符串,例如 -xcvf 将作为单个参数处理。不过,您可以稍微轻松地将其他代码写入我的代码中以添加此功能。


s
schily

请注意,getopt(1) 是 AT&T 的一个短暂错误。

getopt 是在 1984 年创建的,但在 1986 年已经被埋没了,因为它并不是真正可用的。

getopt 非常过时的一个证据是 getopt(1) 手册页仍然提到 "$*" 而不是 "$@",它是在 1986 年与内置的 getopts(1) shell 按顺序添加到 Bourne Shell 中的处理内部有空格的参数。

顺便说一句:如果您对解析 shell 脚本中的长选项感兴趣,可能有兴趣知道 libc (Solaris) 的 getopt(3) 实现和 ksh93 都添加了一个统一的长选项实现,该实现支持长选项作为别名短选项。这会导致 ksh93Bourne Shell 通过 getopts 为长选项实现统一接口。

从 Bourne Shell 手册页中获取的长选项示例:

getopts "f:(file)(input-file)o:(output-file)" OPTX "$@"

显示在 Bourne Shell 和 ksh93 中可以使用多长时间的选项别名。

请参阅最近的 Bourne Shell 的手册页:

http://schillix.sourceforge.net/man/man1/bosh.1.html

以及来自 OpenSolaris 的 getopt(3) 手册页:

http://schillix.sourceforge.net/man/man3c/getopt.3c.html

最后,使用 getopt(1) 手册页来验证过时的 $*:

http://schillix.sourceforge.net/man/man1/getopt.1.html


C
CIsForCookies

基于此处的其他答案,这是我的版本:

#!/bin/bash
set -e

function parse() {
    for arg in "$@"; do # transform long options to short ones
        shift
        case "$arg" in
            "--name") set -- "$@" "-n" ;;
            "--verbose") set -- "$@" "-v" ;;
            *) set -- "$@" "$arg"
        esac
    done

    while getopts "n:v" optname  # left to ":" are flags that expect a value, right to the ":" are flags that expect nothing
    do
        case "$optname" in
            "n") name=${OPTARG} ;;
            "v") verbose=true ;;
        esac
    done
    shift "$((OPTIND-1))" # shift out all the already processed options
}


parse "$@"
echo "hello $name"
if [ ! -z $verbose ]; then echo 'nice to meet you!'; fi

用法:

$ ./parse.sh
hello
$ ./parse.sh -n YOUR_NAME
hello YOUR_NAME
$ ./parse.sh -n YOUR_NAME -v
hello YOUR_NAME
nice to meet you!
$ ./parse.sh -v -n YOUR_NAME
hello YOUR_NAME
nice to meet you!
$ ./parse.sh -v
hello 
nice to meet you!

M
Mark Fox

混合位置和基于标志的参数

--param=arg(等于分隔)

在位置参数之间自由混合标志:

./script.sh dumbo 127.0.0.1 --environment=production -q -d
./script.sh dumbo --environment=production 127.0.0.1 --quiet -d

可以通过一种相当简洁的方法来完成:

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   param=${!pointer}
   if [[ $param != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      case $param in
         # paramter-flags with arguments
         -e=*|--environment=*) environment="${param#*=}";;
                  --another=*) another="${param#*=}";;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + 1)):$#} \
         || set -- ${@:((pointer + 1)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2

--param arg(空格分隔)

不混合 --flag=value--flag value 样式通常更清楚。

./script.sh dumbo 127.0.0.1 --environment production -q -d

这读起来有点冒险,但仍然有效

./script.sh dumbo --environment production 127.0.0.1 --quiet -d

资源

# process flags
pointer=1
while [[ $pointer -le $# ]]; do
   if [[ ${!pointer} != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer
   else
      param=${!pointer}
      ((pointer_plus = pointer + 1))
      slice_len=1

      case $param in
         # paramter-flags with arguments
         -e|--environment) environment=${!pointer_plus}; ((slice_len++));;
                --another) another=${!pointer_plus}; ((slice_len++));;

         # binary flags
         -q|--quiet) quiet=true;;
                 -d) debug=true;;
      esac

      # splice out pointer frame from positional list
      [[ $pointer -gt 1 ]] \
         && set -- ${@:1:((pointer - 1))} ${@:((pointer + $slice_len)):$#} \
         || set -- ${@:((pointer + $slice_len)):$#};
   fi
done

# positional remain
node_name=$1
ip_address=$2

E
Emeric Verschuur

我编写了一个 bash 助手来编写一个不错的 bash 工具

项目主页:https://gitlab.mbedsys.org/mbedsys/bashopts

例子:

#!/bin/bash -ei

# load the library
. bashopts.sh

# Enable backtrace dusplay on error
trap 'bashopts_exit_handle' ERR

# Initialize the library
bashopts_setup -n "$0" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc"

# Declare the options
bashopts_declare -n first_name -l first -o f -d "First name" -t string -i -s -r
bashopts_declare -n last_name -l last -o l -d "Last name" -t string -i -s -r
bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name"
bashopts_declare -n age -l number -d "Age" -t number
bashopts_declare -n email_list -t string -m add -l email -d "Email adress"

# Parse arguments
bashopts_parse_args "$@"

# Process argument
bashopts_process_args

会提供帮助:

NAME:
    ./example.sh - This is myapp tool description displayed on help message

USAGE:
    [options and commands] [-- [extra args]]

OPTIONS:
    -h,--help                          Display this help
    -n,--non-interactive true          Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false)
    -f,--first "John"                  First name - [$first_name] (type:string, default:"")
    -l,--last "Smith"                  Last name - [$last_name] (type:string, default:"")
    --display-name "John Smith"        Display name - [$display_name] (type:string, default:"$first_name $last_name")
    --number 0                         Age - [$age] (type:number, default:0)
    --email                            Email adress - [$email_list] (type:string, default:"")

请享用 :)


我在 Mac OS X 上得到这个:``` lib/bashopts.sh: line 138: declare: -A: invalid option declare: usage: declare [-afFirtx] [-p] [name[=value] ...] lib/bashopts.sh:138 中的错误。 'declare -x -A bashopts_optprop_name' 以状态 2 退出调用树:1:lib/controller.sh:4 source(...) 以状态 1 退出```
您需要 Bash 版本 4 才能使用它。在 Mac 上,默认版本是 3。您可以使用 home brew 安装 bash 4。
a
a_z

这是我的方法 - 使用正则表达式。

没有getopts

它处理短参数块 -qwerty

它处理短参数 -q -w -e

它处理长选项--qwerty

您可以将属性传递给短选项或长选项(如果您使用的是短选项块,则属性附加到最后一个选项)

您可以使用空格或 = 来提供属性,但属性匹配直到遇到连字符+空格“分隔符”,所以在 --q=qwe ty qwe ty 是一个属性

它处理上述所有内容的混合,因此 -oa -op attr ibute --option=att ribute --op-tion 属性 --option att-ribute 是有效的

脚本:

#!/usr/bin/env sh

help_menu() {
  echo "Usage:

  ${0##*/} [-h][-l FILENAME][-d]

Options:

  -h, --help
    display this help and exit

  -l, --logfile=FILENAME
    filename

  -d, --debug
    enable debug
  "
}

parse_options() {
  case $opt in
    h|help)
      help_menu
      exit
     ;;
    l|logfile)
      logfile=${attr}
      ;;
    d|debug)
      debug=true
      ;;
    *)
      echo "Unknown option: ${opt}\nRun ${0##*/} -h for help.">&2
      exit 1
  esac
}
options=$@

until [ "$options" = "" ]; do
  if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then
    if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute]
      opt=${BASH_REMATCH[3]}
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute]
      pile=${BASH_REMATCH[4]}
      while (( ${#pile} > 1 )); do
        opt=${pile:0:1}
        attr=""
        pile=${pile/${pile:0:1}/}
        parse_options
      done
      opt=$pile
      attr=${BASH_REMATCH[7]}
      options=${BASH_REMATCH[9]}
    else # leftovers that don't match
      opt=${BASH_REMATCH[10]}
      options=""
    fi
    parse_options
  fi
done

像这个。也许只需添加 -e 参数以与新行相呼应。
J
John

假设我们创建了一个名为 test_args.sh 的 shell 脚本,如下所示

#!/bin/sh
until [ $# -eq 0 ]
do
  name=${1:1}; shift;
  if [[ -z "$1" || $1 == -* ]] ; then eval "export $name=true"; else eval "export $name=$1"; shift; fi  
done
echo "year=$year month=$month day=$day flag=$flag"

在我们运行以下命令后:

sh test_args.sh  -year 2017 -flag  -month 12 -day 22 

输出将是:

year=2017 month=12 day=22 flag=true

这采用与 Noah's answer 相同的方法,但安全检查/保护措施较少。这允许我们将任意参数写入脚本的环境,我很确定您在这里使用 eval 可能允许命令注入。
M
Mihir Luthra

我想分享我为解析选项所做的事情。这里的答案没有满足我的一些需求,所以我不得不想出这个:https://github.com/MihirLuthra/bash_option_parser

这支持:

子选项解析

选项的别名

可选参数

可变参数

打印使用和错误

假设我们有一个名为 fruit 的命令,用法如下:

fruit <fruit-name> ...
   [-e|—-eat|—-chew]
   [-c|--cut <how> <why>]
   <command> [<args>] 

-e 不带参数
-c 带两个参数,即如何剪切以及为什么要剪切
fruit 本身至少需要一个参数。
<command> 用于像 apple 这样的子选项, orange 等(类似于具有子选项 commitpush 等的 git

所以要解析它:

parse_options \
    'fruit'                         '1 ...'  \
    '-e'     , '--eat' , '--chew'   '0'      \
    '-c'     , '--cut'              '1 1'    \
    'apple'                         'S'      \
    'orange'                        'S'      \
    ';' \
    "$@"

现在,如果有任何使用错误,可以使用 option_parser_error_msg 打印如下:

retval=$?

if [ $retval -ne 0 ]; then
    # this will manage error messages if
    # insufficient or extra args are supplied

    option_parser_error_msg "$retval"

    # This will print the usage
    print_usage 'fruit'
    exit 1
fi

现在检查是否通过了某些选项,

if [ -n "${OPTIONS[-c]}" ]
then
    echo "-c was passed"

    # args can be accessed in a 2D-array-like format
    echo "Arg1 to -c = ${ARGS[-c,0]}"
    echo "Arg2 to -c = ${ARGS[-c,1]}"

fi

子选项解析也可以通过将 $shift_count 传递给 parse_options_detailed 来完成,这使得它在将 args 移动到子选项的 args 后开始解析。在此 example 中进行了演示。

repository 中的自述文件和示例中提供了详细说明。


V
Volodymyr M. Lisivka

使用来自 bash-modules 的模块“参数”

例子:

#!/bin/bash
. import.sh log arguments

NAME="world"

parse_arguments "-n|--name)NAME;S" -- "$@" || {
  error "Cannot parse command line."
  exit 1
}

info "Hello, $NAME!"