有没有办法避免bash中的位置参数?

时间:2012-08-26 07:11:57

标签: bash shell scripting

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

使用参数调用函数:

function_name $arg1 $arg2

我可以在函数中引用我的参数:

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

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

是否有适当的方法或是否需要将这些参数重新分配给函数内的其他变量? E.g:

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

任何例子都会非常感激。

8 个答案:

答案 0 :(得分:63)

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

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

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

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

答案 1 :(得分:8)

您可以随时通过环境传递信息:

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

arg1=banana arg2=apple foo

答案 2 :(得分:3)

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"

此代码段具有以下输出:

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命名空间。

答案 3 :(得分:2)

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

function test() {
    echo $a
}

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

enter image description here

这不仅仅是函数的一个特性,你可以在它自己的脚本中使用该函数并调用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 .

enter image description here

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

答案 4 :(得分:1)

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

我开发的方法允许您访问传递给函数的参数,如下所示:

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'

答案 5 :(得分:1)

我个人希望看到某种语法,如

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

但是因为那不是一件事,而且我看到了很多关于全局变量的引用(并非没有范围界定和命名冲突的警告),我将分享我的方法。

使用Michal's answer中的copy功能:

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是一个true / false值,用于确定完成建议是否已排序。默认为true
  • gi_prompt是一个字符串......好吧,这是不言自明的。默认为“”。

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

注释:

  1. 因此,硬编码的shift$1$2等等是不可能的。

  2. E.g。是"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都会假设后者。

答案 6 :(得分:0)

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

现在,如果您只想回显参数,可以迭代它们:

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

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

答案 7 :(得分:0)

这是一个较旧的主题,但我还是想分享下面的功能(需要bash 4)。它解析命名参数并在脚本环境中设置变量。只需确保您拥有所需参数的合理默认值即可。最后的出口声明也可能只是一个评估。结合使用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
}