【问题标题】:Is there a way to avoid positional arguments in bash?有没有办法避免 bash 中的位置参数?
【发布时间】:2012-08-21 03:11:57
【问题描述】:

我必须在 bash 中编写一个函数。该函数将接受大约 7 个参数。我知道我可以调用这样的函数:

调用带参数的函数:

function_name $arg1 $arg2

我可以在函数内部这样引用我的参数:

function_name () {
   echo "Parameter #1 is $1"
}

我的问题是,有没有更好的方法来引用函数内部的参数?我可以避免使用 $1, $2, $3, .... 的东西而只使用 $arg1, $arg2, ...吗?

是否有合适的方法或者我需要将这些参数重新分配给函数内的其他一些变量?例如:

function_name () {
   $ARG1=$1
   echo "Parameter #1 is $ARG1"
}

任何示例将不胜感激。

【问题讨论】:

  • 使用$1$ARG1有什么区别?
  • @JonLin:我可以使用更直观的名称,而不是 $1、$2。这些数字令人困惑。
  • 你真的需要七个强制参数吗?一种常见的方法是将尽可能多的选项转换为选项,并在有意义的地方提供默认值。如果您不必为如此大量的参数记住特定的位置顺序,即使大多数用户大部分时间会覆盖默认值,它也会使您的脚本更易于使用。
  • 嘿@Bhushan!我想我有一个解决方案给你。我已经发布了答案:stackoverflow.com/questions/12128296/…

标签: bash shell scripting


【解决方案1】:

这样做的常用方法是将参数分配给函数中的局部变量,即:

copy() {
    local from=${1}
    local to=${2}

    # ...
}

另一种解决方案可能是 getopt 样式的选项解析。

copy() {
    local arg from to
    while getopts 'f:t:' arg
    do
        case ${arg} in
            f) from=${OPTARG};;
            t) to=${OPTARG};;
            *) return 1 # illegal option
        esac
    done
}

copy -f /tmp/a -t /tmp/b

遗憾的是,bash 无法处理 更具可读性的长选项,即:

copy --from /tmp/a --to /tmp/b

为此,您要么需要使用外部 getopt 程序(我认为它仅在 GNU 系统上支持长选项),要么手动实现长选项解析器,即:

copy() {
    local from to

    while [[ ${1} ]]; do
        case "${1}" in
            --from)
                from=${2}
                shift
                ;;
            --to)
                to=${2}
                shift
                ;;
            *)
                echo "Unknown parameter: ${1}" >&2
                return 1
        esac

        if ! shift; then
            echo 'Missing parameter argument.' >&2
            return 1
        fi
    done
}

copy --from /tmp/a --to /tmp/b

另见:using getopts in bash shell script to get long and short command line options


你也可以偷懒,只是将“变量”作为参数传递给函数,即:

copy() {
    local "${@}"

    # ...
}

copy from=/tmp/a to=/tmp/b

您将在函数中拥有${from}${to} 作为局部变量。

请注意,以下同样的问题适用 - 如果特定变量未传递,它将从父环境继承。您可能需要添加一条“安全线”,例如:

copy() {
    local from to    # reset first
    local "${@}"

    # ...
}

确保${from}${to}在未通过时将被取消设置。


如果您对某些非常糟糕感兴趣,您还可以在调用函数时将参数分配为全局变量,即:

from=/tmp/a to=/tmp/b copy

然后你可以在copy() 函数中使用${from}${to}。请注意,您应该始终传递所有参数。否则,随机变量可能会泄漏到函数中。

from= to=/tmp/b copy   # safe
to=/tmp/b copy         # unsafe: ${from} may be declared elsewhere

如果你有 bash 4.1(我认为),你也可以尝试使用关联数组。它将允许您传递命名参数,但它很丑陋。比如:

args=( [from]=/tmp/a [to]=/tmp/b )
copy args

然后在copy() 中,您需要grab the array

【讨论】:

  • “本地”方法:从=/tmp/a 复制到=/tmp/b;看起来与调用 Make 目标“复制”并将两个新的环境变量(from 和 to)传递给它的方式非常相似。我真的很喜欢语法相似性。
  • @claytontstanley:是的,这就是意图;)。
  • 我对可选参数使用全局变量方法,但我在函数后命名所述变量,并在检查后在函数内清除它。例如,如果我有get_input() 的可选提示,那么我将使用gi_prompt="blah blah blah",并在函数中使用unset gi_prompt。通过命名与函数相关的所有内容,这可以防止泄漏和命名冲突,同时提供一定的灵活性。
  • 当您实现长解析器示例时,我认为您需要将 2 移到下一个参数对。
  • 我真的很喜欢 local "${@}" 解决方案的简洁性,它规定必须以 name=value 的形式传入命名参数。特别是因为您可以先重置它们,以及设置默认值,因为如果它位于函数的开头,它就会成为自文档化。
【解决方案2】:

你总是可以通过环境传递东西:

#!/bin/sh
foo() {
  echo arg1 = "$arg1"
  echo arg2 = "$arg2"
}

arg1=banana arg2=apple foo

【讨论】:

  • 这不应该是最重要的回应吗?这看起来是最干净的解决方案
【解决方案3】:

您所要做的就是在函数调用过程中命名变量。

function test() {
    echo $a
}

a='hello world' test
#prove variable didnt leak
echo $a .

这不仅仅是函数的一个特性,你可以在它自己的脚本中拥有这个函数并调用a='hello world' test.sh,它的工作原理是一样的


作为一个额外的乐趣,您可以将此方法与位置参数结合使用(假设您正在编写一个脚本,而某些用户可能不知道变量名称)。
哎呀,为什么不让它也为这些参数设置默认值呢?好吧,很简单!

function test2() {
    [[ -n "$1" ]] && local a="$1"; [[ -z "$a" ]] && local a='hi'
    [[ -n "$2" ]] && local b="$2"; [[ -z "$b" ]] && local b='bye'
    echo $a $b
}

#see the defaults
test2

#use positional as usual
test2 '' there
#use named parameter
a=well test2
#mix it up
b=one test2 nice

#prove variables didnt leak
echo $a $b .

请注意,如果 test 是它自己的脚本,则不需要使用 local 关键字。

【讨论】:

    【解决方案4】:

    Shell 函数可以完全访问在其调用范围内可用的任何变量,除了 用于在函数本身内用作局部变量的那些变量名。此外,函数内设置的任何非局部变量在函数被调用后在外部可用。考虑以下示例:

    A=aaa
    B=bbb
    
    echo "A=$A B=$B C=$C"
    
    example() {
        echo "example(): A=$A B=$B C=$C"
    
        A=AAA
        local B=BBB
        C=CCC
    
        echo "example(): A=$A B=$B C=$C"
    }
    
    example
    
    echo "A=$A B=$B C=$C"
    

    这个 sn-p 有以下输出:

    A=aaa B=bbb C=
    example(): A=aaa B=bbb C=
    example(): A=AAA B=BBB C=CCC
    A=AAA B=bbb C=CCC
    

    这种方法的明显缺点是函数不再是自包含的,并且在函数外部设置变量可能会产生意想不到的副作用。如果您想将数据传递给函数而不首先将其分配给变量,这也会使事情变得更加困难,因为该函数不再使用位置参数。

    最常见的处理方法是使用局部变量作为参数和函数内的任何临时变量:

    example() {
       local A="$1" B="$2" C="$3" TMP="/tmp"
    
       ...
    }
    

    这避免了函数局部变量污染 shell 命名空间。

    【讨论】:

      【解决方案5】:

      我想我有一个适合你的解决方案。 通过一些技巧,您实际上可以将命名参数与数组一起传递给函数。

      我开发的方法允许你像这样访问传递给函数的参数:

      testPassingParams() {
      
          @var hello
          l=4 @array anArrayWithFourElements
          l=2 @array anotherArrayWithTwo
          @var anotherSingle
          @reference table   # references only work in bash >=4.3
          @params anArrayOfVariedSize
      
          test "$hello" = "$1" && echo correct
          #
          test "${anArrayWithFourElements[0]}" = "$2" && echo correct
          test "${anArrayWithFourElements[1]}" = "$3" && echo correct
          test "${anArrayWithFourElements[2]}" = "$4" && echo correct
          # etc...
          #
          test "${anotherArrayWithTwo[0]}" = "$6" && echo correct
          test "${anotherArrayWithTwo[1]}" = "$7" && echo correct
          #
          test "$anotherSingle" = "$8" && echo correct
          #
          test "${table[test]}" = "works"
          table[inside]="adding a new value"
          #
          # I'm using * just in this example:
          test "${anArrayOfVariedSize[*]}" = "${*:10}" && echo correct
      }
      
      fourElements=( a1 a2 "a3 with spaces" a4 )
      twoElements=( b1 b2 )
      declare -A assocArray
      assocArray[test]="works"
      
      testPassingParams "first" "${fourElements[@]}" "${twoElements[@]}" "single with spaces" assocArray "and more... " "even more..."
      
      test "${assocArray[inside]}" = "adding a new value"
      

      换句话说,您不仅可以通过参数名称调用参数(这构成了更具可读性的核心),您实际上还可以传递数组(以及对变量的引用——尽管此功能仅在 bash 4.3 中有效)!另外,映射变量都在本地范围内,就像 $1 (和其他)一样。

      使这项工作的代码非常轻巧,并且可以在 bash 3 和 bash 4 中工作(这是我测试过的唯一版本)。如果您对更多类似这样的技巧感兴趣,这些技巧可以使使用 bash 进行开发变得更好、更容易,您可以查看我的 Bash Infinity Framework,下面的代码就是为此目的而开发的。

      Function.AssignParamLocally() {
          local commandWithArgs=( $1 )
          local command="${commandWithArgs[0]}"
      
          shift
      
          if [[ "$command" == "trap" || "$command" == "l="* || "$command" == "_type="* ]]
          then
              paramNo+=-1
              return 0
          fi
      
          if [[ "$command" != "local" ]]
          then
              assignNormalCodeStarted=true
          fi
      
          local varDeclaration="${commandWithArgs[1]}"
          if [[ $varDeclaration == '-n' ]]
          then
              varDeclaration="${commandWithArgs[2]}"
          fi
          local varName="${varDeclaration%%=*}"
      
          # var value is only important if making an object later on from it
          local varValue="${varDeclaration#*=}"
      
          if [[ ! -z $assignVarType ]]
          then
              local previousParamNo=$(expr $paramNo - 1)
      
              if [[ "$assignVarType" == "array" ]]
              then
                  # passing array:
                  execute="$assignVarName=( \"\${@:$previousParamNo:$assignArrLength}\" )"
                  eval "$execute"
                  paramNo+=$(expr $assignArrLength - 1)
      
                  unset assignArrLength
              elif [[ "$assignVarType" == "params" ]]
              then
                  execute="$assignVarName=( \"\${@:$previousParamNo}\" )"
                  eval "$execute"
              elif [[ "$assignVarType" == "reference" ]]
              then
                  execute="$assignVarName=\"\$$previousParamNo\""
                  eval "$execute"
              elif [[ ! -z "${!previousParamNo}" ]]
              then
                  execute="$assignVarName=\"\$$previousParamNo\""
                  eval "$execute"
              fi
          fi
      
          assignVarType="$__capture_type"
          assignVarName="$varName"
          assignArrLength="$__capture_arrLength"
      }
      
      Function.CaptureParams() {
          __capture_type="$_type"
          __capture_arrLength="$l"
      }
      
      alias @trapAssign='Function.CaptureParams; trap "declare -i \"paramNo+=1\"; Function.AssignParamLocally \"\$BASH_COMMAND\" \"\$@\"; [[ \$assignNormalCodeStarted = true ]] && trap - DEBUG && unset assignVarType && unset assignVarName && unset assignNormalCodeStarted && unset paramNo" DEBUG; '
      alias @param='@trapAssign local'
      alias @reference='_type=reference @trapAssign local -n'
      alias @var='_type=var @param'
      alias @params='_type=params @param'
      alias @array='_type=array @param'
      

      【讨论】:

        【解决方案6】:

        我个人希望看到类似的语法

        func(a b){
            echo $a
            echo $b
        }
        

        但由于这不是问题,而且我看到很多对全局变量的引用(并非没有范围和命名冲突的警告),所以我将分享我的方法。

        使用来自Michal's answercopy 函数:

        copy(){
            cp $from $to
        }
        from=/tmp/a
        to=/tmp/b
        copy
        

        这很糟糕,因为fromto 是非常宽泛的词,任何数量的函数都可以使用它。您可能很快就会遇到命名冲突或手上的“泄漏”。

        letter(){
            echo "From: $from"
            echo "To:   $to"
            echo
            echo "$1"
        }
        
        to=Emily
        letter "Hello Emily, you're fired for missing two days of work."
        
        # Result:
        #   From: /tmp/a
        #   To:   Emily
        
        #   Hello Emily, you're fired for missing two days of work.
        

        所以我的方法是“命名空间”它们。我以函数命名变量,并在函数完成后将其删除。当然,我只将它用于具有默认值的可选值。否则,我只使用位置参数。

        copy(){
            if [[ $copy_from ]] && [[ $copy_to ]]; then
                cp $copy_from $copy_to
                unset copy_from copy_to
            fi
        }
        copy_from=/tmp/a
        copy_to=/tmp/b
        copy # Copies /tmp/a to /tmp/b
        copy # Does nothing, as it ought to
        letter "Emily, you're 'not' re-hired for the 'not' bribe ;)"
        # From: (no /tmp/a here!)
        # To:
        
        # Emily, you're 'not' re-hired for the 'not' bribe ;)
        

        我会成为一个糟糕的老板......


        实际上,我的函数名称比“复制”或“字母”更复杂。

        我记忆中最近的例子是get_input(),它有gi_no_sortgi_prompt

        • gi_no_sort 是一个真/假值,用于确定是否对完成建议进行排序。默认为真
        • gi_prompt 是一个字符串……嗯,这是不言自明的。默认为“”。

        函数采用的实际参数是上述输入提示的“完成建议”的来源,并且由于所述列表取自函数中的$@,因此“命名参数”是可选的[1 ],并且没有明显的方法来区分表示完成的字符串和布尔/提示消息,或者实际上是 bash 中以空格分隔的任何内容[2] ;上述解决方案最终为我节省了很多的麻烦。

        注释:

        1. 所以硬编码的shift$1$2等是不可能的。

        2. 例如"0 Enter a command: {1..9} $(ls)"0"Enter a command:"1 2 3 4 5 6 7 8 9 <directory contents> 的集合吗?或者"0""Enter""a""command:" 也是该集合的一部分?不管你喜不喜欢,Bash 都会采用后者。

        【讨论】:

          【解决方案7】:

          参数作为单个项目的元组发送到函数,因此它们没有名称,只有位置。这允许一些有趣的可能性,如下所示,但这确实意味着你被困在 1 美元上。 $2 等关于是否将它们映射到更好的名称,问题归结为函数有多大,以及它会使阅读代码更加清晰。如果它很复杂,那么映射有意义的名称($BatchID、$FirstName、$SourceFilePath)是个好主意。不过,对于简单的东西,它可能没有必要。如果您使用 $arg1 之类的名称,我当然不会打扰。

          现在,如果你只是想回显参数,你可以遍历它们:

          for $arg in "$@"
          do
            echo "$arg"
          done
          

          只是一个有趣的事实;除非您正在处理列表,否则您可能对更有用的东西感兴趣

          【讨论】:

            【解决方案8】:

            这是一个较老的话题,但我仍然想分享下面的功能(需要 bash 4)。它解析命名参数并在脚本环境中设置变量。只需确保您需要的所有参数都具有合理的默认值。最后的导出语句也可能只是一个 eval。与 shift 结合使用可以很好地扩展现有脚本,这些脚本已经采用了一些位置参数并且您不想更改语法,但仍然增加了一些灵活性。

            parseOptions()
            {
              args=("$@")
              for opt in "${args[@]}"; do
                if [[ ! "${opt}" =~ .*=.* ]]; then
                  echo "badly formatted option \"${opt}\" should be: option=value, stopping..."
                  return 1
                fi
                local var="${opt%%=*}"
                local value="${opt#*=}"
                export ${var}="${value}"
              done
              return 0
            }
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2019-09-07
              • 1970-01-01
              • 2012-06-20
              • 1970-01-01
              • 1970-01-01
              • 2019-05-29
              • 2011-09-13
              • 2021-12-15
              相关资源
              最近更新 更多