Linux 中的 Bash 函数
1. 概述
当我们在 Bash 脚本中编写复杂逻辑时,可以将其组织成可重用函数。
本文中,我们将看看如何定义和使用 Bash 函数。
2. 基础
我们可以使用两种方式来定义 Bash 函数:
name () compound-command [redirections]
function name [()] compound-command [redirections]
只有括号存在时,才能省略 function
关键字。
或者,如果我们使用 function
关键字,我们也可以省略括号。
函数体可以是任何复合命令,而重定向也是可选的,并在执行函数时执行。
2.1. 定义函数
前面我们提到,同门可以使用两种方式来定义函数。我们来看看一个快速示例:
simple_function() {
for ((i=0;i<5;++i)) do
echo -n " "$i" ";
done
}
simple_function
我们仅通过调用函数名来调用函数,但我们必须在执行它之前定义它。
这个简单的例子只打印了一些数字:
0 1 2 3 4
正如我们在开始时所说的,我们可以使用 function
关键字并省略括号:
function simple_function {
# same body as before
}
当然,其结果是一样的。
请注意,我们的函数也使用了相同的名称。在这种情况下,Bash 使用我们脚本中的最后一个函数定义。
我们说过函数体可以是任何复合命令,我们甚至可以省略花括号:
function simple_for_loop()
for ((i=0;i<5;++i)) do
echo -n " "$i" ";
done
本例中的输出跟前面的一样。
然而,这只适用于执行 for
循环中指令。这是因为循环构造充当了一个复合命令。
我们还可以使用条件构造和命令组来定义函数体。
2.2. 传递输入参数
将输入传递给函数与将参数传递给 Bash 脚本没有什么不同:
function simple_inputs() {
echo "This is the first argument [$1]"
echo "This is the second argument [$2]"
echo "Calling function with $# arguments"
}
simple_inputs one 'two three'
我们来细看此例。首先,我们打印位置参数的两个输入。
然后,我们也可以使用特殊参数来打印参数总数。
我们还用引号转义第二个输入,以避免分词。
让我们看看输出:
This is the first argument [one]
This is the second argument [two three]
Calling function with 2 arguments
2.3. 获取输出
执行函数时,Bash 将其视为命令。
这意味着 return
语句只能用 0 到 255 之间的值表示数值退出状态。
如果我们没有返回退出代码,那么 Bash 将返回我们函数中最后一个命令的退出状态。
让我们计算两个数字的和:
sum=0
function simple_outputs() {
sum=$(($1+$2))
}
simple_outputs 1 2
echo "Sum is $sum"
上述代码中,我们使用了一个全局变量来保存实际结果。
作为这种方法的替代方案,我们可以依赖命令替换:
function simple_outputs() {
sum=$(($1+$2))
echo $sum
}
sum=$(simple_outputs 1 2)
echo "Sum is $sum"
请注意,现在我们正在 sub-shell 中执行函数。稍后我们将对此进行探讨。
因为函数类似于命令,所以我们可以在有实际结果的地方捕获它们的标准输出:
Sum is 3
2.4. 使用参数引用
从 Bash 4.3+ 开始,我们可以通过引用传递输入参数,然后在函数内修改其状态:
function ref_outputs() {
declare -n sum_ref=$3
sum_ref=$(($1+$2))
}
让我们深入这个例子,以便更好地理解它。
首先,我们声明了一个 nameref
变量,用来存储其第三个参数名。
第二步,我们使用该变量作为赋值操作的左侧操作数。
最后,我们通过将输入和输出都指定为位置参数来调用函数
ref_outputs 1 2 sum
echo "Sum is $sum"
然后我们就能看到同样的结果:
Sum is 3
3. 高级概念
现在我们已经了解了基础知识,让我们来看看更高级的概念和函数使用场景。
3.1. 变量和作用域
我们在前面的例子中研究了全局变量。我们还可以定义局部变量:
variable="baeldung"
function variable_scope() {
local variable="lorem"
echo "Variable inside function variable_scope : [$variable]"
}
variable_scope
echo "Variable outside function variable_scope : [$variable]"
局部变量在调用作用域中隐藏同名变量:
Variable inside function variable_scope : [lorem]
Variable outside function variable_scope : [baeldung]
让我们把事情进一步复杂化,并在前面的函数中调用另一个函数:
variable="baeldung"
function variable_scope2() {
echo "Variable inside function variable_scope2 : [$variable]"
}
function variable_scope() {
local variable="lorem"
echo "Variable inside function variable_scope : [$variable]"
variable_scope2
}
variable_scope
其输出有些惊喜:
Variable inside function variable_scope : [lorem]
Variable inside function variable_scope2 : [lorem]
即使我们在 variable_scope
函数中定义了局部变量,它仍然在第二个函数中是可见的:
这称为动态作用域,它影响了如何在嵌套的子作用域中查看变量。
3.2. Sub-Shell
还记得我们在上面提到了 sub-shell 吧。sub-shell 是一种特殊类型的命令组,它允许我们从当前 shell 生成新的执行环境。
由于函数体可以用任何命令组界定,我们可以直接在 sub-shell 中执行我们的逻辑:
sum=0
function simple_subshell() (
sum_ref=$(($1+$2))
)
simple_subshell
echo "Sum is $sum"
请注意,我们现在使用括号来界定函数体,而不是花括号。
当我们运行该示例时,我们注意到全局变量并没有改变:
Sum is 0
现在我们来实时使用参数引用:
sum=0
function simple_subshell() (
declare -n sum_ref=$3
sum_ref=$(($1+$2))
)
simple_subshell 1 2 sum
echo "Sum is $sum"
我们获得了同样的结果。这是因为,当生成的执行环境完成时,变量赋值将被忽略。
而我们可以使用命令替换来检索 sub-shell 的标准输出流,正如我们在前面的代码片段中看到的那样。
一般来说,sub-shell 的目的是允许并行处理任务。
3.3. 重定向
一开始,我们也提到函数定义语法也允许重定向。
让我们看看一个简单的例子,我们逐行读取文件并打印其内容:
function redirection_in() {
while read input;
do
echo "$input"
done
} < infile
redirection_in
在这段代码中,我们将测试文件的内容直接重定向到函数的标准输入。
read
命令从标准输入中获取每一行。
当运行该函数时,输出包含汽车厂商清单以及模型和生产年份:
Honda Insight 2010
Honda Element 2006
Chevrolet Avalanche 2002
我们可以将函数的标准输出重定向到文件:
function redirection_out() {
declared -a output=("baeldung" "lorem" "ipsum")
for element in "${output[@]}"
do
echo "$element"
done
} > outfile
redirection_out
本例中,该输出文件 outfile
在单独的行中包含我们的三个元素:
baeldung
lorem
ipsum
但是重定向到其他命令和从其他命令重定向呢?为此,我们可以使用进程替换:
function redirection_in_ps() {
read
while read -a input;
do
echo "${input[2]} ${input[8]}"
done
} < <(ls -ll /)
redirection_in_ps
此示例从根目录(/)读取文件夹及其所有者。让我们仔细看看会发生什么:
root bin
root boot
root dev
root etc
# some more folders
ls
命令的输出通过进程替换被解释为文件。
然后,此输出被重定向到函数的标准输入,函数将对其进行进一步处理。
当我们想将函数的标准输出重定向到命令时,我们只需在第 7 行反转进程替换运算符:
function redirection_out_ps() {
declare -a output=("baeldung" "lorem" "ipsum" "caracg")
for element in "${output[@]}"
do
echo "$element"
done
} > >(grep "g")
redirection_out_ps
这样,我们可以将 grep
的标准输入视为一个文件,并将我们的函数输出重定向zhi它。
此段代码仅打印包含字母 g
的行:
baeldung
caracg
3.4. 递归
我们还可以在 Bash 函数中使用递归。让我们来探索计算第 n 个斐波那契数:
function fibonnaci_recursion() {
argument=$1
if [[ "$argument" -eq 0 ]] || [[ "$argument" -eq 1 ]]; then
echo $argument
else
first=$(fibonnaci_recursion $(($argument-1)))
second=$(fibonnaci_recursion $(($argument-2)))
echo $(( $first + $second ))
fi
}
让我们仔细看看,以便更好地理解它。函数的第一部分处理第一个和第二个斐波那契数。
对于所有其他数字,我们递归调用函数来计算前面的两个数字。
我们使用算术展开从输入参数中减去 1 和 2,并再次命令替换以保留结果。
让我们看看这是如何计算第 7 和第 15 个斐波那契数的:
echo $(fibonnaci_recursion 7)
echo $(fibonnaci_recursion 15)
13
610
虽然对于第一次调用,事情运行顺利,但我们可以很快观察到第二次调用的执行变得相当缓慢。
虽然递归在 Bash 函数中是可能行的,但通常最好避免使用。
我们还可以通过设置 FUNNEST
内置变量来限制函数的嵌套调用次数:
FUNCNEST=5
echo $(fibonnaci_recursion 7)
echo $(fibonnaci_recursion 15)
fibonnaci_recursion: maximum function nesting level exceeded (5)
fibonnaci_recursion: maximum function nesting level exceeded (5)
# some more errors
4. 总结
本文中,我们探讨了使用 Bash 函数的实操。我们可以使用不同的构造来定义函数体,并使用多种方式检索输出。
声明局部变量是可能的,但动态作用域会影响函数查看变量的方式。我们还可以在函数中重定向文件和其他命令,甚至使用递归。
总的来说,Bash 函数提供了极大的灵活性,并提供了一种组织复杂脚本的强大方法。