编程

Linux 中的 Bash 函数

26 2024-09-17 01:39:00

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 函数提供了极大的灵活性,并提供了一种组织复杂脚本的强大方法。