【问题标题】:Is mixing getopts with positional parameters possible?是否可以将 getopts 与位置参数混合?
【发布时间】:2012-07-29 09:21:27
【问题描述】:

我想设计一个 shell 脚本作为几个脚本的包装器。我想使用getoptsmyshell.sh 指定参数,并将其余参数以相同的顺序传递给指定的脚本。

如果myshell.sh 像这样执行:

myshell.sh -h hostname -s test.sh -d waittime param1 param2 param3

myshell.sh param1 param2 -h hostname param3 -d waittime -s test.sh

myshell.sh param1 -h hostname -d waittime -s test.sh param2 param3

以上都应该可以调用为

test.sh param1 param2 param3

是否可以利用 myshell.sh 中的选项参数并将剩余参数发布到底层脚本?

【问题讨论】:

  • 你想做什么?您想将test.sh param1 param2 param3 呼叫到myshell.sh 吗?
  • 对不起,如果问题不清楚。是的。我想让我的脚本能够处理位置参数和 getopt 值的混合。 getopt 中剩余的所有内容都应与底层 shell 脚本一起传递。
  • 只有第一行符合 unix 标准(见下文)用于选项处理。否则将需要做更多的工作才能正确和维护。

标签: bash shell parameters


【解决方案1】:

想做类似OP的事情,找到了我需要的相关资料herehere

基本上如果你想做类似的事情:

script.sh [options] ARG1 ARG2

然后像这样得到你的选择:

while getopts "h:u:p:d:" flag; do
case "$flag" in
    h) HOSTNAME=$OPTARG;;
    u) USERNAME=$OPTARG;;
    p) PASSWORD=$OPTARG;;
    d) DATABASE=$OPTARG;;
esac
done

然后你可以像这样得到你的位置参数:

ARG1=${@:$OPTIND:1}
ARG2=${@:$OPTIND+1:1}

更多信息和细节可通过上面的链接获得。

希望有帮助!!

【讨论】:

  • 但这不允许位置参数在标志之前,这是 OP 问题的一部分。处理位置 /after/ 标志的另一种标准方法是在 esac 之后执行 shift $((OPTIND-1)) ,然后正常处理位置。
  • @Chinasaur 当我使用ARG1=${@:$OPTIND:1} 之后,它对我的​​script.sh -h localhost abcscript.sh abc -h localhost = 在ARG1 中都有效,我在这两种情况下都有abc 的价值。您能否更具体地说明什么不起作用(我误解了什么)?谢谢!
  • 我无法获得ARG1。它是空的。
  • 另一种常见模式是通过 getopts 关闭所有 args 进程,然后以 $1、$2 处理剩余的进程:while getopts "h:u:p:d:" flag; do .... done; shift $(($OPTIND - 1)); echo $1; // Positional arg1 echo $2; // Positional arg2
  • @Chinasaur 至少在 C (man 3 getopt) 中,getopt 移动了参数:By default, getopt() permutes the contents of argv as it scans, so that eventually all the nonoptions are at the end. 我猜man 1 getopts 的行为方式相同,尽管我什么都没看到快速浏览一下手册页即可了解它。
【解决方案2】:

混合选择和参数:

ARGS=""
echo "options :"
while [ $# -gt 0 ]
do
    unset OPTIND
    unset OPTARG
    while getopts as:c:  options
    do
    case $options in
            a)  echo "option a  no optarg"
                    ;;
            s)  serveur="$OPTARG"
                    echo "option s = $serveur"
                    ;;
            c)  cible="$OPTARG"
                    echo "option c = $cible"
                    ;;
        esac
   done
   shift $((OPTIND-1))
   ARGS="${ARGS} $1 "
   shift
done

echo "ARGS : $ARGS"
exit 1

结果:

bash test.sh  -a  arg1 arg2 -s serveur -c cible  arg3
options :
option a  no optarg
option s = serveur
option c = cible
ARGS :  arg1  arg2  arg3

【讨论】:

  • 对于 ARGS 变量,如果没有参数,这会产生一个更清晰的空字符串(即没有空格):ARGS="${ARGS:+${ARGS} }${1}"
  • 请使用构造declare -a ARGS ..... ARGS+=($1) 向列表中添加值。这种方式尊重空间。
  • 你如何通过位置索引访问 ARGS 的元素?例如,如果我想将 ARGS 的第一个元素分配给另一个变量,我该怎么做?
【解决方案3】:

myshell.sh:

#!/bin/bash

script_args=()
while [ $OPTIND -le "$#" ]
do
    if getopts h:d:s: option
    then
        case $option
        in
            h) host_name="$OPTARG";;
            d) wait_time="$OPTARG";;
            s) script="$OPTARG";;
        esac
    else
        script_args+=("${!OPTIND}")
        ((OPTIND++))
    fi
done

"$script" "${script_args[@]}"

test.sh:

#!/bin/bash
echo "$0 $@"

测试 OP 的案例:

$ PATH+=:.  # Use the cases as written without prepending ./ to the scripts
$ myshell.sh -h hostname -s test.sh -d waittime param1 param2 param3
./test.sh param1 param2 param3
$ myshell.sh param1 param2 -h hostname param3 -d waittime -s test.sh
./test.sh param1 param2 param3
$ myshell.sh param1 -h hostname -d waittime -s test.sh param2 param3
./test.sh param1 param2 param3

发生了什么:

getopts 在遇到位置参数时会失败。如果将其用作循环条件,则只要位置参数出现在选项之前,循环就会提前中断,就像在两个测试用例中所做的那样。

因此,只有在处理完所有参数后,此循环才会中断。如果getopts 无法识别某些东西,我们只是假设它是一个位置参数,并在手动递增getopts 的计数器的同时将其填充到一个数组中。

可能的改进:

正如所写,子脚本不能接受选项(只有位置参数),因为包装脚本中的getopts 会吃掉这些选项并打印错误消息,同时将任何参数视为位置参数:

$ myshell.sh param1 param2 -h hostname -d waittime -s test.sh -a opt1 param3
./myshell.sh: illegal option -- a
./test.sh param1 param2 opt1 param3

如果我们知道子脚本只能接受位置参数,那么myshell.sh 应该可能会在无法识别的选项上停止。这可以像在case 块的末尾添加一个默认的最后一个案例一样简单:

            \?) exit 1;;
$ myshell.sh param1 param2 -h hostname -d waittime -s test.sh -a opt1 param3
./myshell.sh: illegal option -- a

如果子脚本需要接受选项(只要它们不与myshell.sh 中的选项冲突),我们可以通过在选项字符串前加一个冒号将getopts 切换为静默错误报告:

    if getopts :h:d:s: option

然后我们将使用默认的最后一种情况将任何无法识别的选项填充到script_args

            \?) script_args+=("-$OPTARG");;
$ myshell.sh param1 param2 -h hostname -d waittime -s test.sh -a opt1 param3
./test.sh param1 param2 -a opt1 param3

【讨论】:

  • 这很好用并且可以回答原帖。为什么点赞这么少?
  • @conny 这是最近的
【解决方案4】:

getopts 不会解析 param1-n 选项的组合。

最好将 param1-3 像其他选项一样放入选项中。

此外,您还可以使用现有的库,例如 shflags。它非常智能且易于使用。

最后一种方法是编写自己的函数来解析参数而不使用 getopts,只需通过 case 构造迭代所有参数。这是最难的方法,但它是完全符合您的期望的唯一方法。

【讨论】:

  • 谢谢。我这里选择的方式是先解析getopts参数,再解析shift并将参数传递给底层脚本。
【解决方案5】:

我想出了一种方法,可以将 getopts 扩展到真正混合选项和位置参数。这个想法是在调用getopts 和将找到的任何位置参数分配给n1n2n3 等之间交替:

parse_args() {
    _parse_args 1 "$@"
}

_parse_args() {
    local n="$1"
    shift

    local options_func="$1"
    shift

    local OPTIND
    "$options_func" "$@"
    shift $(( OPTIND - 1 ))

    if [ $# -gt 0 ]; then
        eval test -n \${n$n+x}
        if [ $? -eq 0 ]; then
            eval n$n="\$1"
        fi

        shift
        _parse_args $(( n + 1 )) "$options_func" "$@"
    fi
}

那么在 OP 的情况下,您可以像这样使用它:

main() {
    local n1='' n2='' n3=''
    local duration hostname script

    parse_args parse_main_options "$@"

    echo "n1 = $n1"
    echo "n2 = $n2"
    echo "n3 = $n3"
    echo "duration = $duration"
    echo "hostname = $hostname"
    echo "script   = $script"
}

parse_main_options() {
    while getopts d:h:s: opt; do
        case "$opt" in
            d) duration="$OPTARG" ;;
            h) hostname="$OPTARG" ;;
            s) script="$OPTARG"   ;;
        esac
    done
}

main "$@"

运行它会显示输出:

$ myshell.sh param1 param2 -h hostname param3 -d waittime -s test.sh
n1 = param1
n2 = param2
n3 = param3
duration = waittime
hostname = hostname
script   = test.sh

只是一个概念证明,但也许对某人有用。

注意:如果一个使用 parse_args 的函数调用另一个使用 parse_args 的函数并且外部函数声明例如local n4='',但内部函数没有并且 4 个或更多位置参数被传递给内部函数

【讨论】:

    【解决方案6】:

    只是混搭了一个 quickie,它可以轻松处理选项和位置参数的混合(在 $@ 中只留下位置参数):

    #!/bin/bash
    while [ ${#} -gt 0 ];do OPTERR=0;OPTIND=1;getopts "p:o:hvu" arg;case "$arg" in
            p) echo "Path:   [$OPTARG]" ;;
            o) echo "Output: [$OPTARG]" ;;
            h) echo "Help"              ;;
            v) echo "Version"           ;;
        \?) SET+=("$1")                                           ;;
        *) echo "Coding error: '-$arg' is not handled by case">&2 ;;
    esac;shift;[ "" != "$OPTARG" ] && shift;done
    [ ${#SET[@]} -gt 0 ] && set "" "${SET[@]}" && shift
    
    echo -e "=========\nLeftover (positional) parameters (count=$#) are:"
    for i in `seq $#`;do echo -e "\t$i> [${!i}]";done
    

    样本输出:

    [root@hots:~]$ ./test.sh 'aa bb' -h -v -u -q 'cc dd' -p 'ee ff' 'gg hh' -o ooo
    Help
    Version
    Coding error: '-u' is not handled by case
    Path:   [ee ff]
    Output: [ooo]
    =========
    Leftover (positional) parameters (count=4) are:
            1> [aa bb]
            2> [-q]
            3> [cc dd]
            4> [gg hh]
    [root@hots:~]$
    

    【讨论】:

    • 这不允许在单个“-abc”参数中组合多个选项。
    【解决方案7】:

    您可以直接实现自己的 bash 参数解析器,而不是使用 getopts。以此为例。它可以同时处理名称和位置参数。

    #!/bin/bash
    
    function parse_command_line() {
        local named_options;
        local parsed_positional_arguments;
    
        yes_to_all_questions="";
        parsed_positional_arguments=0;
    
        named_options=(
                "-y" "--yes"
                "-n" "--no"
                "-h" "--help"
                "-s" "--skip"
                "-v" "--version"
            );
    
        function validateduplicateoptions() {
            local item;
            local variabletoset;
            local namedargument;
            local argumentvalue;
    
            variabletoset="${1}";
            namedargument="${2}";
            argumentvalue="${3}";
    
            if [[ -z "${namedargument}" ]]; then
                printf "Error: Missing command line option for named argument '%s', got '%s'...\\n" "${variabletoset}" "${argumentvalue}";
                exit 1;
            fi;
    
            for item in "${named_options[@]}";
            do
                if [[ "${item}" == "${argumentvalue}" ]]; then
                    printf "Warning: Named argument '%s' got possible invalid option '%s'...\\n" "${namedargument}" "${argumentvalue}";
                    exit 1;
                fi;
            done;
    
            if [[ -n "${!variabletoset}" ]]; then
                printf "Warning: Overriding the named argument '%s=%s' with '%s'...\\n" "${namedargument}" "${!variabletoset}" "${argumentvalue}";
            else
                printf "Setting '%s' named argument '%s=%s'...\\n" "${thing_name}" "${namedargument}" "${argumentvalue}";
            fi;
            eval "${variabletoset}='${argumentvalue}'";
        }
    
        # https://stackoverflow.com/questions/2210349/test-whether-string-is-a-valid-integer
        function validateintegeroption() {
            local namedargument;
            local argumentvalue;
    
            namedargument="${1}";
            argumentvalue="${2}";
    
            if [[ -z "${2}" ]];
            then
                argumentvalue="${1}";
            fi;
    
            if [[ -n "$(printf "%s" "${argumentvalue}" | sed s/[0-9]//g)" ]];
            then
                if [[ -z "${2}" ]];
                then
                    printf "Error: The %s positional argument requires a integer, but it got '%s'...\\n" "${parsed_positional_arguments}" "${argumentvalue}";
                else
                    printf "Error: The named argument '%s' requires a integer, but it got '%s'...\\n" "${namedargument}" "${argumentvalue}";
                fi;
                exit 1;
            fi;
        }
    
        function validateposisionaloption() {
            local variabletoset;
            local argumentvalue;
    
            variabletoset="${1}";
            argumentvalue="${2}";
    
            if [[ -n "${!variabletoset}" ]]; then
                printf "Warning: Overriding the %s positional argument '%s=%s' with '%s'...\\n" "${parsed_positional_arguments}" "${variabletoset}" "${!variabletoset}" "${argumentvalue}";
            else
                printf "Setting the %s positional argument '%s=%s'...\\n" "${parsed_positional_arguments}" "${variabletoset}" "${argumentvalue}";
            fi;
            eval "${variabletoset}='${argumentvalue}'";
        }
    
        while [[ "${#}" -gt 0 ]];
        do
            case ${1} in
                -y|--yes)
                    yes_to_all_questions="${1}";
                    printf "Named argument '%s' for yes to all questions was triggered.\\n" "${1}";
                    ;;
    
                -n|--no)
                    yes_to_all_questions="${1}";
                    printf "Named argument '%s' for no to all questions was triggered.\\n" "${1}";
                    ;;
    
                -h|--help)
                    printf "Print help here\\n";
                    exit 0;
                    ;;
    
                -s|--skip)
                    validateintegeroption "${1}" "${2}";
                    validateduplicateoptions g_installation_model_skip_commands "${1}" "${2}";
                    shift;
                    ;;
    
                -v|--version)
                    validateduplicateoptions branch_or_tag "${1}" "${2}";
                    shift;
                    ;;
    
                *)
                    parsed_positional_arguments=$((parsed_positional_arguments+1));
    
                    case ${parsed_positional_arguments} in
                        1)
                            validateposisionaloption branch_or_tag "${1}";
                            ;;
    
                        2)
                            validateintegeroption "${1}";
                            validateposisionaloption g_installation_model_skip_commands "${1}";
                            ;;
    
                        *)
                            printf "ERROR: Extra positional command line argument '%s' found.\\n" "${1}";
                            exit 1;
                            ;;
                    esac;
                    ;;
            esac;
            shift;
        done;
    
        if [[ -z "${g_installation_model_skip_commands}" ]];
        then
            g_installation_model_skip_commands="0";
        fi;
    }
    

    您可以将此函数称为:

    #!/bin/bash
    source ./function_file.sh;
    parse_command_line "${@}";
    

    使用示例:

    ./test.sh as 22 -s 3
    Setting the 1 positional argument 'branch_or_tag=as'...
    Setting the 2 positional argument 'skip_commands=22'...
    Warning: Overriding the named argument '-s=22' with '3'...
    

    参考文献:

    1. example_installation_model.sh.md
    2. Checking for the correct number of arguments
    3. https://unix.stackexchange.com/questions/129391/passing-named-arguments-to-shell-scripts
    4. An example of how to use getopts in bash

    【讨论】:

      【解决方案8】:

      unix 选项处理有一些标准,在 shell 编程中,getopts 是执行它们的最佳方式。几乎所有现代语言(perl、python)在getopts 上都有一个变体。

      这只是一个简单的例子:

      command [ options ] [--] [ words ]
      
      1. 每个选项都必须以破折号- 开头,并且必须由单个字符组成。

      2. GNU 项目引入了长选项,以两个破折号-- 开头, 后跟一个完整的词,--long_option。 AST KSH 项目有一个 getopts,它也支持长选项,以单个破折号开头的长选项,-,如find(1)

      3. 选项可能需要也可能不需要参数。

      4. 任何不以破折号开头的单词,-,都将结束选项处理。

      5. 必须跳过字符串--,并将结束选项处理。

      6. 任何剩余的参数都保留为位置参数。

      The Open Group 在Utility Argument Syntax 上有一个部分

      Eric Raymond 的 Unix 编程艺术 has a chapter 关于选项字母的传统 unix 选择及其含义。

      【讨论】:

        【解决方案9】:

        你可以试试这个技巧:在带有 optargs 的 while 循环之后,只需使用这个 sn-p

        #shift away all the options so that only positional agruments
        #remain in $@
        
        for (( i=0; i<OPTIND-1; i++)); do
            shift
        done
        
        POSITIONAL="$@"
        

        但是,这种方法有一个错误:

          第一个位置参数之后的所有选项都由 getopts 输入并被视为位置参数 - 事件那些正确的选项(参见示例输出:-m 和 -c 在位置参数中)

        也许它有更多的错误......

        看整个例子:

        while getopts :abc opt; do
            case $opt in
                a)
                echo found: -a
                ;;
                b)
                echo found: -b
                ;;
                c)
                echo found: -c
                ;;
                \?) echo found bad option: -$OPTARG
                ;;
            esac
        done
        
        #OPTIND-1 now points to the first arguments not beginning with -
        
        #shift away all the options so that only positional agruments
        #remain in $@
        
        for (( i=0; i<OPTIND-1; i++)); do
            shift
        done
        
        POSITIONAL="$@"
        
        echo "positional: $POSITIONAL"
        

        输出:

        [root@host ~]# ./abc.sh -abc -de -fgh -bca haha blabla -m -c
        found: -a
        found: -b
        found: -c
        found bad option: -d
        found bad option: -e
        found bad option: -f
        found bad option: -g
        found bad option: -h
        found: -b
        found: -c
        found: -a
        positional: haha blabla -m -c
        

        【讨论】:

          猜你喜欢
          • 2021-11-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-12-18
          • 2022-10-04
          • 2010-12-31
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多