70

我必须在 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"
}

任何例子将不胜感激。

4

8 回答 8

101

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

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

    # ...
}

另一种解决方案可能是getopt -style 选项解析。

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

另请参阅:在 bash shell 脚本中使用 getopts 获取长短命令行选项


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

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}and 。请注意,您应该始终传递所有参数。否则,随机变量可能会泄漏到函数中。${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(),您需要获取数组

于 2012-08-26T07:41:58.617 回答
15

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

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

arg1=banana arg2=apple foo
于 2012-08-26T11:05:32.643 回答
9

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

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关键字。

于 2017-05-15T02:47:57.263 回答
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"

此代码段具有以下输出:

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

于 2012-08-26T07:51:10.233 回答
2

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

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

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'
于 2015-05-04T13:46:15.437 回答
1

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

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

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

使用Michal 回答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是一个真/假值,用于确定是否对完成建议进行排序。默认为真
  • gi_prompt是一个字符串……嗯,这是不言自明的。默认为“”。

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

笔记:

  1. 所以硬编码的shiftand $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", and"command:"也是该集合的一部分?无论你喜欢与否,Bash 都会采用后者。

于 2015-09-04T04:31:24.027 回答
0

这是一个较老的话题,但我仍然想分享下面的功能(需要 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
}
于 2014-08-19T11:47:27.283 回答
0

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

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

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

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

于 2012-08-26T07:28:54.900 回答