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

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

任何例子都可以。

47827 次浏览

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

现在,如果你只想回传参数,你可以对它们进行迭代:

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

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

常用的方法是将参数赋给函数中的局部变量,例如:

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


# ...
}

另一种解决方案可能是 选择风格的选项解析。

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

然后您可以在 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()中,你需要 拿到数组

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

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

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


arg1=banana arg2=apple foo

这是一个较老的主题,但我仍然想分享下面的函数(需要 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
}

我有个办法。 通过一些技巧,您实际上可以将命名参数和数组一起传递给函数。

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

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 无穷大框架,下面的代码就是为此目的开发的。

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'

我个人希望看到类似

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是一个 true/false 值,用于确定是否对完成建议进行了排序
  • 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 都会假设是后者。

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

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