为 bash 脚本提供像命令一样接受标志的选项

Giving a bash script the option to accept flags like a command

我正在编写一个简单的 bash 脚本,我希望它能以任何顺序接受来自命令行的参数。

我浏览了 Web 并在 while 循环中编写了一个带有 case 语句的简单函数。现在,'any order' 部分有效 - 但它只获取我设置的第一个参数。我当然做错了什么,但脚本对我来说是很新的,我一直无法弄清楚 - 非常感谢您的帮助。脚本的flags部分如下:

#Parameters - source,destination,credentials,bandwidth,timeout,port,help
flags () {
        while test $# -gt 0; do
                case "" in
                        -s|--source)
                                shift
                                if test $# -gt 0; then
                                        export SOURCE=
                                else
                                        echo "No source directory specified!"
                                        exit 1
                                fi
                                ;;
                        -d|--destination)
                                shift
                                if test $# -gt 0; then
                                        export DESTINATION=
                                fi
                                ;;
                        -c|--credentials)
                                shift
                                if test $# -gt 0; then
                                        export CREDENTIALS=
                                fi
                                ;;
                        -b|--bandwidth)
                                shift
                                if test $# -gt 0; then
                                        export BANDWIDTH=
                                fi
                                ;;
                        -t|--timeout)
                                shift
                                if test $# -gt 0; then
                                        export TIMEOUT=
                                fi
                                ;;
                        -p|--port)
                                shift
                                if test $# -gt 0; then
                                        export PORT=
                                fi
                                ;;
                        -h|--help)
                                shift
                                if test $# -gt 0; then
                                        echo "Help goes here"
                                fi
                                ;;
                        -l|--compression-level)
                                shift
                                if test $# -gt 0; then
                                        export COMPRESS_LEVEL=
                                fi
                                ;;
                        *)
                                break
                                ;;
                esac
        done
}
flags "$@"
echo "source is $SOURCE, destination is $DESTINATION, credentials are $CREDENTIALS, bandwidth is $BANDWIDTH, timeout is $TIMEOUT, port is $PORT"

理想情况下,其中一些参数是强制性的,而其他参数是可选的 - 但这不是必须的。

如何修改此脚本以接受任何顺序的任何参数(最好是长格式和短格式)?

如评论中所述,在使用参数(例如凭据)后,您需要进行另一次转换。您应该在 non-existent 参数的错误报告中保持一致。如果你得到 -h--help,你应该简单地打印帮助并退出;你不应该测试更多的论点。如果需要帮助,你就给予帮助,什么都不做。您还应该将错误回显为标准错误:echo "message" >&2。您的消息应以 script/program 名称作为前缀:arg0=$(basename "[=16=]" .sh)echo "$arg0: message" >&2

将这些更改放在一起,您可能会想出这样的脚本:

#!/bin/sh

arg0=$(basename "[=10=]" .sh)
blnk=$(echo "$arg0" | sed 's/./ /g')

usage_info()
{
    echo "Usage: $arg0 [{-s|--source} source] [{-d|--destination} destination] \"
    echo "       $blnk [{-c|--credentials} credentials] [{-b|--bandwidth} bandwidth] \"
    echo "       $blnk [{-t|--timeout} timeout] [{-p|--port} port] \"
    echo "       $blnk [-h|--help] [{-l|--compression-level} level]"
}

usage()
{
    exec 1>2   # Send standard output to standard error
    usage_info
    exit 1
}

error()
{
    echo "$arg0: $*" >&2
    exit 1
}

help()
{
    usage_info
    echo
    echo "  {-s|--source} source            -- Set source directory (default: .)"
    echo "  {-d|--destination} destination  -- Set destination"
    echo "  {-c|--credentials} credentials  -- Set credentials"
    echo "  {-b|--bandwidth} bandwidth      -- Set maximum bandwidth"
    echo "  {-t|--timeout} timeout          -- Set timeout (default: 60s)"
    echo "  {-p|--port} port                -- Set port number (default: 1234)"
    echo "  {-l|--compression-level} level  -- Set compression level (default: 1)"
    echo "  {-h|--help}                     -- Print this help message and exit"
#   echo "  {-V|--version}                  -- Print version information and exit"
    exit 0
}

flags()
{
    while test $# -gt 0
    do
        case "" in
        (-s|--source)
            shift
            [ $# = 0 ] && error "No source directory specified"
            export SOURCE=""
            shift;;
        (-d|--destination)
            shift
            [ $# = 0 ] && error "No destination specified"
            export DESTINATION=""
            shift;;
        (-c|--credentials)
            shift
            [ $# = 0 ] && error "No credentials specified"
            export CREDENTIALS=""
            shift;;
        (-b|--bandwidth)
            shift
            [ $# = 0 ] && error "No bandwidth specified"
            export BANDWIDTH=""
            shift;;
        (-t|--timeout)
            shift
            [ $# = 0 ] && error "No timeout specified"
            export TIMEOUT=""
            shift;;
        (-p|--port)
            shift
            [ $# = 0 ] && error "No port specified"
            export PORT=""
            shift;;
        (-l|--compression-level)
            shift
            [ $# = 0 ] && error "No compression level specified"
            export COMPRESS_LEVEL=""
            shift;;
        (-h|--help)
            help;;
#       (-V|--version)
#           version_info;;
        (*) usage;;
        esac
    done
}

flags "$@"

echo "source is $SOURCE"
echo "destination is $DESTINATION"
echo "credentials are $CREDENTIALS"
echo "bandwidth is $BANDWIDTH"
echo "timeout is $TIMEOUT"
echo "port is $PORT"

示例运行(脚本名称:flags53.sh):

$ sh flags53.sh -c XYZ -d PQR -s 123 -l 4 -t 99 -b 12 -p 56789
source is 123
destination is PQR
credentials are XYZ
bandwidth is 12
timeout is 99
port is 56789
$ sh flags53.sh -c XYZ --destination PQR -s 123 -l 4 --timeout 99 -b 12 --port 56789
source is 123
destination is PQR
credentials are XYZ
bandwidth is 12
timeout is 99
port is 56789
$ sh flags53.sh -c XYZ -h
Usage: flags53 [{-s|--source} source] [{-d|--destination} destination] \
               [{-c|--credentials} credentials] [{-b|--bandwidth} bandwidth] \
               [{-t|--timeout} timeout] [{-p|--port} port] \
               [-h|--help] [{-l|--compression-level} level]

  {-s|--source} source            -- Set source directory (default: .)
  {-d|--destination} destination  -- Set destination
  {-c|--credentials} credentials  -- Set credentials
  {-b|--bandwidth} bandwidth      -- Set maximum bandwidth
  {-t|--timeout} timeout          -- Set timeout (default: 60s)
  {-p|--port} port                -- Set port number (default: 1234)
  {-l|--compression-level} level  -- Set compression level (default: 1)
  {-h|--help}                     -- Print this help message and exit
$

请注意,请求的帮助可以转到标准输出而不是标准错误,尽管将帮助发送到标准错误并不是严重的犯罪行为。帮助获取有关每个选项含义的用法消息和额外信息。注意默认值(并设置它们)也是一个好主意。可能没有必要导出设置——您可以简单地设置变量而无需显式 export。在调用 flags 函数之前,或者在 flags 函数开始时,您应该真正将变量设置为默认值。这避免了意外继承导出的值(环境变量)。当然,除非你想接受环境变量,但你的名字可能应该被赋予一个适合脚本名称的系统前缀。大多数程序应该有 --version-V 选项(对 'verbose' 使用 -v,而不是版本)。如果该命令不接受任何 non-option(文件名)参数,请在解析循环后添加检查并抱怨不需要的参数。如果该命令必须至少有一个 non-option 参数,请检查它。不要在接收到 -- 作为参数时报告错误;终止检查循环并将任何剩余参数视为 non-option 个参数。

一个遗留问题——函数中的移位影响函数的参数列表,而不是全局“$@”。你必须弄清楚如何从这个骨架中处理它。我想我可能会创建一个 $OPTIND 的类似物,它报告有多少参数要转移到 non-option 参数。 flags 函数中的代码应该跟踪它移动了多少个参数。

这导致修改后的代码:

#!/bin/sh

arg0=$(basename "[=12=]" .sh)
blnk=$(echo "$arg0" | sed 's/./ /g')

usage_info()
{
    echo "Usage: $arg0 [{-s|--source} source] [{-d|--destination} destination] \"
    echo "       $blnk [{-c|--credentials} credentials] [{-b|--bandwidth} bandwidth] \"
    echo "       $blnk [{-t|--timeout} timeout] [{-p|--port} port] \"
    echo "       $blnk [-h|--help] [{-l|--compression-level} level]"
}

usage()
{
    exec 1>2   # Send standard output to standard error
    usage_info
    exit 1
}

error()
{
    echo "$arg0: $*" >&2
    exit 1
}

help()
{
    usage_info
    echo
    echo "  {-s|--source} source            -- Set source directory (default: .)"
    echo "  {-d|--destination} destination  -- Set destination"
    echo "  {-c|--credentials} credentials  -- Set credentials"
    echo "  {-b|--bandwidth} bandwidth      -- Set maximum bandwidth"
    echo "  {-t|--timeout} timeout          -- Set timeout (default: 60s)"
    echo "  {-p|--port} port                -- Set port number (default: 1234)"
    echo "  {-l|--compression-level} level  -- Set compression level (default: 1)"
    echo "  {-h|--help}                     -- Print this help message and exit"
#   echo "  {-V|--version}                  -- Print version information and exit"
    exit 0
}

flags()
{
    OPTCOUNT=0
    while test $# -gt 0
    do
        case "" in
        (-s|--source)
            shift
            [ $# = 0 ] && error "No source directory specified"
            export SOURCE=""
            shift
            OPTCOUNT=$(($OPTCOUNT + 2));;
        (-d|--destination)
            shift
            [ $# = 0 ] && error "No destination specified"
            export DESTINATION=
            shift
            OPTCOUNT=$(($OPTCOUNT + 2));;
        (-c|--credentials)
            shift
            [ $# = 0 ] && error "No credentials specified"
            export CREDENTIALS=
            shift
            OPTCOUNT=$(($OPTCOUNT + 2));;
        (-b|--bandwidth)
            shift
            [ $# = 0 ] && error "No bandwidth specified"
            export BANDWIDTH=
            shift
            OPTCOUNT=$(($OPTCOUNT + 2));;
        (-t|--timeout)
            shift
            [ $# = 0 ] && error "No timeout specified"
            export TIMEOUT=""
            shift
            OPTCOUNT=$(($OPTCOUNT + 2));;
        (-p|--port)
            shift
            [ $# = 0 ] && error "No port specified"
            export PORT=
            shift
            OPTCOUNT=$(($OPTCOUNT + 2));;
        (-l|--compression-level)
            shift
            [ $# = 0 ] && error "No compression level specified"
            export COMPRESS_LEVEL=""
            shift
            OPTCOUNT=$(($OPTCOUNT + 2));;
        (-h|--help)
            help;;
#       (-V|--version)
#           version_info;;
        (--)
            shift
            OPTCOUNT=$(($OPTCOUNT + 1))
            break;;
        (*) usage;;
        esac
    done
    echo "DEBUG-1: [$*]" >&2
    echo "OPTCOUNT=$OPTCOUNT" >&2
}

flags "$@"
echo "DEBUG-2: [$*]" >&2
echo "OPTCOUNT=$OPTCOUNT" >&2
shift $OPTCOUNT
echo "DEBUG-3: [$*]" >&2


echo "source is $SOURCE"
echo "destination is $DESTINATION"
echo "credentials are $CREDENTIALS"
echo "bandwidth is $BANDWIDTH"
echo "timeout is $TIMEOUT"
echo "port is $PORT"

如果您想试验一下,还有其他写算术的方法。不过不要使用 expr