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


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

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

或者这一个:

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

什么是解析这个的可接受的方式,在每种情况下(或两者的某种组合) $v$f,和 $d 将全部设定为 true 和 $outFile 将等于 /fizz/someOtherFile ?


1349
2017-10-10 16:57


起源


对于zsh-users,有一个很棒的内置命名为zparseopts,它可以: zparseopts -D -E -M -- d=debug -debug=d 并且两者都有 -d 和 --debug 在里面 $debug 排列 echo $+debug[1] 如果使用其中一个,将返回0或1。参考: zsh.org/mla/users/2011/msg00350.html - dezza


答案:


方法#1:使用不带getopt的bash [s]

传递键 - 值对参数的两种常用方法是:

Bash Space-Separated(例如, --option argument)(没有getopt [s])

用法 ./myscript.sh -e conf -s /etc -l /usr/lib /etc/hosts

#!/bin/bash

POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"

case $key in
    -e|--extension)
    EXTENSION="$2"
    shift # past argument
    shift # past value
    ;;
    -s|--searchpath)
    SEARCHPATH="$2"
    shift # past argument
    shift # past value
    ;;
    -l|--lib)
    LIBPATH="$2"
    shift # past argument
    shift # past value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument
    ;;
    *)    # unknown option
    POSITIONAL+=("$1") # save it in an array for later
    shift # past argument
    ;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters

echo FILE EXTENSION  = "${EXTENSION}"
echo SEARCH PATH     = "${SEARCHPATH}"
echo LIBRARY PATH    = "${LIBPATH}"
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

Bash Equals-Separated(例如, --option=argument)(没有getopt [s])

用法 ./myscript.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

#!/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
    ;;
    -l=*|--lib=*)
    LIBPATH="${i#*=}"
    shift # past argument=value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument with no value
    ;;
    *)
          # unknown option
    ;;
esac
done
echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
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

为了更好地理解 ${i#*=} 在中搜索“子串删除” 本指南。它在功能上等同于 `sed 's/[^=]*=//' <<< "$i"` 它调用一个不必要的子进程或 `echo "$i" | sed 's/[^=]*=//'` 哪个叫  不必要的子过程。

使用getopt [s]

从: http://mywiki.wooledge.org/BashFAQ/035#getopts

getopt(1)限制 (年龄较大,相对较新 getopt 版本):

  • 无法处理空字符串的参数
  • 无法处理嵌入空格的参数

更近 getopt 版本没有这些限制。

此外,POSIX shell(和其他)提供 getopts 没有这些限制。这是一个简单的 getopts 例:

#!/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: $@"

# End of file

的优点 getopts 是:

  1. 它更便携,并且可以在其他外壳中工作 dash
  2. 它可以处理多个单一选项 -vf filename 以典型的Unix方式,自动。

缺点 getopts 是它只能处理短期权利(-h不是 --help)没有额外的代码。

有一个 getopts教程 这解释了所有语法和变量的含义。在bash中,也有 help getopts,这可能是提供信息的。


1948
2018-01-07 20:01



这是真的吗?根据 维基百科 有一个较新的GNU增强版本 getopt 其中包括的所有功能 getopts 然后还有一些。 man getopt 在Ubuntu 13.04输出 getopt - parse command options (enhanced) 作为名称,所以我认为这个增强版本现在是标准的。 - Livven
在你的系统上某种某种方式是一个非常弱的前提,基于“标准”的假设。 - szablica
@Livven,那 getopt 它不是GNU实用程序,它是其中的一部分 util-linux。 - Stephane Chazelas
如果你使用 -gt 0,删除你的 shift 之后 esac,增加所有 shift 由1并添加此案例: *) break;; 你可以处理非选项参数。例如: pastebin.com/6DJ57HTc - Nicolas Mongrain-Lacombe
你不回应 –default。在第一个例子中,我注意到如果 –default 是最后一个参数,它不被处理(被认为是非选择),除非 while [[ $# -gt 1 ]] 被设为 while [[ $# -gt 0 ]] - kolydart


没有回答提到 增强了getopt。而且 最高投票的答案 有误导性: 它忽略了 -⁠vfd 样式短选项(由OP请求),位置参数后的选项(也由OP请求)并忽略解析错误。代替:

  • 使用增强 getopt 来自util-linux或以前的GNU glibc1
  • 它适用于 getopt_long() GNU glibc的C函数。
  • 具有 所有 有用的区别特征(其他没有它们):
    • 处理空格,引用字符甚至参数中的二进制文件2
    • 它可以在最后处理选项: script.sh -o outFile file1 file2 -v
    • 允许 =式长选项: script.sh --outfile=fileOut --infile fileIn
  • 已经太老了3 没有GNU系统缺少这个(例如任何Linux都有它)。
  • 您可以使用以下方法测试其存在: getopt --test →返回值4。
  • 其他 getopt 或者是shell-builtin getopts 用途有限。

以下电话

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
# saner programming env: these switches turn some bugs into errors
set -o errexit -o pipefail -o noclobber -o nounset

! getopt --test > /dev/null 
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
    echo "I’m sorry, `getopt --test` failed in this environment."
    exit 1
fi

OPTIONS=dfo:v
LONGOPTS=debug,force,output:,verbose

# -use ! and PIPESTATUS to get exit code with errexit set
# -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 大多数“bash系统”都可以使用增强的getopt,包括Cygwin;在OS X上尝试 brew安装gnu-getopt
2 POSIX exec() 约定没有可靠的方法在命令行参数中传递二进制NULL;那些字节过早地结束了论证
3 1997年或之前发布的第一个版本(我只追溯到1997年)


333
2018-04-20 17:47



谢谢你。刚从功能表中确认 en.wikipedia.org/wiki/Getopts,如果您需要长期选项的支持,并且您不在Solaris上, getopt 是要走的路。 - johncip
我相信唯一的警告 getopt 是它不能使用 便利地 在包装器脚本中,其中一个可能没有特定于包装器脚本的选项,然后将非包装脚本选项传递给包装的可执行文件,完整无缺。假设我有一个 grep 包装器叫 mygrep 我有一个选择 --foo 具体到 mygrep那我就做不到了 mygrep --foo -A 2,并有 -A 2 自动传递给 grep;一世 需要 去做 mygrep --foo -- -A 2。 这是 我的实施 在您的解决方案之上。 - Kaushal Modi
@bobpaul你关于util-linux的陈述也是错误的和误导性的:软件包在Ubuntu / Debian上标记为“必需”。因此,它始终安装。 - 你在谈论哪些发行版(你说它需要有意安装)? - Robert Siemer
@Jason, getopt 会为你失败。无需亲自检查。 - Robert Siemer
@AlexYursha @Marcos @bobpaul改进的脚本使用 errexit 又名 set -e 现在相当优雅。 - Robert Siemer


来自: 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#*=} 在中搜索“子串删除” 本指南。它在功能上等同于 `sed 's/[^=]*=//' <<< "$i"` 它调用一个不必要的子进程或 `echo "$i" | sed 's/[^=]*=//'` 哪个叫  不必要的子过程。


104
2017-11-13 10:31



整齐!虽然这不适用于以空格分隔的参数 mount -t tempfs ...。人们可以通过类似的东西解决这个问题 while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;; 等等 - Tobias Kienzler
这无法处理 -vfd 风格结合短期选择。 - Robert Siemer
链接坏了! - bekur


getopt()/getopts() 是个不错的选择。偷了 这里

这个迷你脚本中显示了“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”打破了选择和   把每个人都放在自己的论点中。它也是   添加 ” - ”。


101
2017-10-10 17:03



运用 $* 是破碎的用法 getopt。 (它用空格来填充参数。)参见 我的答案 正确使用。 - Robert Siemer
你为什么要把它变得更复杂? - SDsolar
@Matt J,脚本的第一部分(对于i)如果使用“$ i”而不是$ i,则能够处理带有空格的参数。 getopts似乎无法处理带空格的参数。使用getopt而不是for i循环有什么好处? - thebunnyrules


冒着添加另一个忽略的例子的风险,这是我的方案。

  • 手柄 -n arg 和 --name=arg
  • 最后允许参数
  • 如果有任何拼写错误,则会显示正确的错误
  • 兼容,不使用bashisms
  • 可读,不需要在循环中维护状态

希望它对某人有用。

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

61
2017-07-15 23:43



什么是“handle_argument”函数? - rhombidodecahedron
抱歉耽搁了。在我的脚本中,handle_argument函数接收所有非选项参数。你可以用你喜欢的任何东西替换那条线 *) die "unrecognized argument: $1" 或者将args收集到变量中 *) args+="$1"; shift 1;;。 - bronson
惊人!我已经测试了几个答案,但这是唯一适用于所有情况的答案,包括许多位置参数(标志前后) - Guilherme Garnier


更简洁的方式

script.sh

#!/bin/bash

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

echo "Should deploy? $deploy"
echo "Should uglify? $uglify"

用法:

./script.sh -d dev -u

# OR:

./script.sh --deploy dev --uglify

41
2017-11-20 12:28



这就是我在做的事情。不得不 while [[ "$#" > 1 ]] 如果我想支持用布尔标志结束该行 ./script.sh --debug dev --uglify fast --verbose。例: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58 - hfossli
我发了一个编辑请求。我刚刚测试了它,它完美无缺。 - hfossli
哇!简单干净!这就是我使用它的方式: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58 - hfossli
这就像我见过的最优雅的代码片段。 +1 - Harindaka
这很好,很简单。的KudoZ - jitter


我对这个问题迟了大约4年,但是想要回馈。我使用早期的答案作为整理旧的adhoc param解析的起点。然后我重构了以下模板代码。它使用=或空格分隔的参数处理长和短参数,以及组合在一起的多个短参数。最后,它将任何非参数参数重新插入到$ 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

36
2017-07-01 01:20



此代码无法使用如下参数处理选项: -c1。并使用 = 从他们的论点中分离出短期选择是不寻常的...... - Robert Siemer
我用这个有用的代码块遇到了两个问题:1)“-c = foo”情况下的“shift”最终会吃下一个参数; 2)'c'不应包括在可组合短期权的“[cfr]”模式中。 - sfnd


我的答案主要基于 Bruno Bronosky的答案,但我把他的两个纯粹的bash实现混合成了一个我经常使用的实现。

# 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值

  • 适用于您可以在bash中使用的任何arg名称

    • 含义-a或-arg或--arg或-a-r-g或其他
  • 纯粹的bash。无需学习/使用getopt或getopts

缺点:

  • 无法组合args

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

这些是我能想到的唯一优点/缺点


21
2017-09-08 18:59





我发现在脚本中编写可移植解析的问题让我写的很令人沮丧 Argbash  - 一个FOSS代码生成器,可以为您的脚本生成参数解析代码,还有一些很好的功能:

https://argbash.io


21
2017-07-10 22:40



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


我认为这个很简单,可以使用:

#!/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

12
2018-03-01 15:15



我读了所有,这是我的首选。我不喜欢用 -a=1 作为argc风格。我更喜欢先放置主选项-options,然后放入单一间距的特殊选项 -o option。我正在寻找最简单,更好的方式来阅读argvs。 - erm3nda
它工作得很好但是如果你将一个参数传递给非a:选项,所有以下选项都将被视为参数。你可以查看这一行 ./myscript -v -d fail -o /fizz/someOtherFile -f ./foo/bar/someFile 用你自己的脚本。 -d选项未设置为d: - erm3nda


扩展@guneysus的优秀答案,这是一个调整,让用户可以使用他们喜欢的任何语法,例如

command -x=myfilename.ext --another_switch 

VS

command -x myfilename.ext --another_switch

也就是说,等号可以用空格代替。

这种“模糊解释”可能不符合您的喜好,但如果您正在制作可与其他实用程序互换的脚本(就像我的情况一样,它必须与ffmpeg一起使用),灵活性很有用。

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

11
2018-06-09 13:46