如何在 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 $outFile方式是什么?

答案

更新:距我开始回答已有 5 年了。感谢您提供大量修改 / 评论 / 建议。为了节省维护时间,我将代码块修改为 100%复制粘贴就绪。请不要发表 “如果您将 X 更改为 Y ... 该怎么办” 之类的评论。取而代之的是,复制粘贴代码块,查看输出,进行更改,重新运行脚本,并注释 “我将 X 更改为 Y,然后……”,我没有时间测试您的想法并告诉您它们是否可行。


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

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

Bash 分隔的空格(例如--option argument )(没有 getopt [s])

用法demo-space-separated.sh -e conf -s /etc -l /usr/lib /etc/hosts

cat >/tmp/demo-space-separated.sh <<'EOF'
#!/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
EOF

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

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

复制粘贴以上块的输出:

FILE EXTENSION  = conf
SEARCH PATH     = /etc
LIBRARY PATH    = /usr/lib
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

Bash 等于分隔(例如--option=argument )(没有 getopt [s])

用法demo-equals-separated.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

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
    ;;
    -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 "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 -l=/usr/lib /etc/hosts

复制粘贴以上块的输出:

FILE EXTENSION  = conf
SEARCH PATH     = /etc
LIBRARY PATH    = /usr/lib
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

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

方法 2:将 bash 与 getopt [s] 一起使用

来自: http : //mywiki.wooledge.org/BashFAQ/035#getopts

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

  • 无法处理为空字符串的参数
  • 无法处理带有嵌入式空格的参数

最新的getopt版本没有这些限制。

另外,POSIX shell(和其他)提供了getopts ,而没有这些限制。我提供了一个简单的getopts示例。

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

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

getopts的优点是:

  1. 它更加便携,并且可以在dash等其他外壳中使用。
  2. 它可以以典型的 Unix 方式自动处理多个选项,例如-vf filename

getopts的缺点在于,它只能处理简短的选项( -h ,而不是--help ),而无需其他代码。

有一个getopts 教程 ,它解释了所有语法和变量的含义。在 bash 中,还help getopts ,它可能会提供参考。

没有答案提到增强的 getopt投票最多的答案是令人误解的:它要么忽略-⁠vfd样式短选项(OP 要求),要么忽略位置参数后的选项(OP 也要求);并且它忽略了解析错误。代替:

  • 使用来自 util-linux 或以前的 GNU glibc 的增强型getopt1 个
  • 它与 GNU glibc 的 C 函数getopt_long()使用。
  • 具有所有有用的区别功能(其他功能则没有):
    • 在参数2 中处理空格,引用字符甚至二进制文件(非增强型getopt无法做到这一点)
    • 它可以在最后处理选项: script.sh -o outFile file1 file2 -vgetopts不会这样做)
    • script.sh --outfile=fileOut --infile fileIn = -style long 选项: script.sh --outfile=fileOut --infile fileIn (如果自解析,则两者都很长)
    • 允许组合的简短选项,例如-vfd (如果进行自我解析, -vfd实际工作)
    • 允许触摸选项参数,例如-oOutfile-vfdoOutfile
  • 已经太老了3以至于没有 GNU 系统会缺少此功能(例如,任何 Linux 都拥有它)。
  • 您可以使用以下命令测试其存在: getopt --test →返回值 4。
  • 其他getopt或 shell 内置的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

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

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

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

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

来自: 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/[^=]*=//'`调用了两个不必要的子进程。

更简洁的方式

script.sh

#!/bin/bash

while [[ "$#" -gt 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

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” 将选项分开,并将每个选项置于自己的参数中。它还添加了 “-”。

这是我的计划,冒着添加另一个示例来忽略的风险。

  • 处理-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

我这个问题迟到了 4 年,但想退回给我。我以较早的答案为起点来整理旧的即席参数解析。然后,我重构了以下模板代码。它使用 = 或以空格分隔的参数来处理长参数和短参数,以及组合在一起的多个短参数。最后,它将所有非参数参数重新插入到 $ 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

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

https://argbash.io

我的答案主要基于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 或 - arg 或其他
  • 纯扑无需学习 / 使用 getopt 或 getopts

缺点:

  • 不能合并参数

    • 表示没有 - abc。您必须执行 - a -b -c

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

我认为这个简单易用:

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