Contents

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 使用我们脚本中的最后一个函数定义。 由于我们说 body 可以是任何复合命令,我们甚至可以省略花括号:

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 中执行我们的函数。稍后我们将对此进行探讨。

因为函数类似于 commands,所以我们可以在我们获得实际结果的地方捕获它们的标准输出:

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="blogdemo"
function variable_scope() {
    local variable="Ann"
    echo "Variable inside function variable_scope : [$variable]"
}
variable_scope
echo "Variable outside function variable_scope : [$variable]"

局部变量从调用范围中隐藏具有相同名称的变量:

Variable inside function variable_scope : [Ann]
Variable outside function variable_scope : [blogdemo]

让我们把事情进一步复杂化,并从我们之前的函数中调用另一个函数:

variable="blogdemo"
function variable_scope2() {
    echo "Variable inside function variable_scope2 : [$variable]"
}
function variable_scope() {
    local variable="Ann"
    echo "Variable inside function variable_scope : [$variable]"
    variable_scope2
}
variable_scope

输出有点令人惊讶:

Variable inside function variable_scope : [Ann]
Variable inside function variable_scope2 : [Ann]

即使我们在variable_scope函数中声明了一个局部变量,它在我们的第二个函数中仍然可见。

这称为动态作用域,影响变量在嵌套子作用域中的显示方式。

3.2. 子shell

请记住,我们在上面提到了子外壳。子 shell 是一种特殊类型的命令组,它允许我们从当前 shell 生成一个新的执行环境

由于函数的主体可以用任何命令组分隔,我们可以直接在子 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"

我们得到相同的结果。那是因为当我们生成的执行环境完成时变量赋值被解除

然而,我们可以使用命令替换来检索子 shell 的标准输出流,正如我们在前面的片段中看到的那样。

通常,子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=("blogdemo" "Ann" "Bob")
    for element in "${output[@]}"
        do
            echo "$element"
        done
} > outfile
redirection_out

在这种情况下,输出文件outfile在单独的行中包含我们的三个元素:

blogdemo
Ann
Bob

但是重定向到其他命令和从其他命令重定向呢?为此,我们可以使用进程替换

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=("blogdemo" "Ann" "Bob" "Candy")
    for element in "${output[@]}"
        do
            echo "$element"
        done
} > >(grep "g")
redirection_out_ps

这样,我们可以将grep 的标准输入视为一个文件,并将我们的函数输出重定向到它。

此代码段仅打印包含字母g的行:

blogdemo
Candy

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 函数可以进行递归,但我们通常最好避免使用它。

我们还可以通过设置FUNCNEST内置变量来限制函数的嵌套调用次数

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