Bash 函数

警告
本文最后更新于 2023-10-22,文中内容可能已过时。

Bash 函数

Bash 函数和 Bash 脚本的各个方面都非常相似,其底层可能是同一个东西。

简介

函数(function)是可以重复使用的代码片段,有利于代码的复用。它与别名(alias)的区别是,别名只适合封装简单的单个命令,函数则可以封装复杂的多行命令。

我们可以将带有参数的命令设置一个别名,alias,所以别名也可以做简单的函数,比如查找特定文件之类的

函数总是在当前 Shell 执行,这是跟脚本的一个重大区别,Bash 会新建一个子 Shell 执行脚本。

如果函数与脚本同名,函数会优先执行。但是,函数的优先级不如别名,即如果函数与别名同名,那么别名优先执行。

shell 环境下同名的时候的优先级:别名 > 函数 > 脚本

Bash 函数定义的语法有两种。

1
2
3
4
5
6
7
8
9
# 第一种
fn() {
  # codes
}

# 第二种
function fn() {
  # codes
}

上面代码中,fn是自定义的函数名,括号里是不写参数的,函数代码就写在大括号之中。上面这两种写法是等价的。不过一般推荐第二种,带有 function 关键字,可读性好一些

我们在交互式 shell 中创建函数的时候,输入完function test_fun() {之后直接回车,不会直接执行,而是像 Here 文档一样,出现>,直到输入},关于 Here 文档,请看《转义和引号》

删除一个函数,可以使用unset命令。关于 unset 命令,请看《Bash 变量》的删除变量小节

1
unset -f functionName

查看当前 Shell 已经定义的所有函数,可以使用declare命令。关于 declare 命令,请看《Bash 变量》的declare 命令小节

1
$ declare -f

上面的declare命令不仅会输出函数名,还会输出所有定义。输出顺序是按照函数名的字母表顺序。由于会输出很多内容,最好通过管道命令配合moreless使用。

declare命令还支持查看单个函数的定义。

1
$ declare -f functionName

declare -F可以输出所有已经定义的函数名,不含函数体。

1
$ declare -

简单实践如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ function test_fun(){
> echo "hello from test_fun"
> }
$ declare -f
test_fun () 
{ 
    echo "hello from test_fun"
}
$ declare -F
declare -f test_fun
$ unset -f test_fun
$ declare -F
$ declare -fdeclare 命令

调用时,就直接写函数名,参数跟在函数名后面。这种传参的方式跟脚本传参的默认方式一样

函数体里面的$1表示函数调用时的第一个参数。其他几个变量比如$@$#,其含义也跟在脚本中一样

简单实践如下:

1
2
3
4
5
$ function test_echo(){
> echo params is $@
> }
$ test_echo 111 222 333
params is 111 222 333

参数变量

函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。

  • $1~$9:函数的第一个到第 9 个的参数。
  • $0:函数所在的脚本名。
  • $#:函数的参数总数。
  • $@:函数的全部参数,参数之间使用空格分隔。
  • $*:函数的全部参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。

如果函数的参数多于 9 个,那么第 10 个参数可以用${10}的形式引用,以此类推。

此外注意,脚本传参是做不到传递数组变量的,只能做到将所有的数组元素挨个传入,然后再在脚本中组合成一个数组。

shift命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1),使得后面的参数向前一位,即$2变成$1$3变成$2$4变成$3,以此类推。

shift命令可以接受一个整数作为参数,指定所要移除的参数个数,默认为1

此外,在方法中也可以使用 getopts 命令或者 getopt 命令,以-或者--的格式传入参数,跟脚本的传参方式基本无异。而且调用起来比脚本调用起来更加简洁

具体请看《Bash 脚本》的脚本参数小节。

简单实践如下:

创建function.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/usr/bin/env bash

# 脚本日志文件
log_file_path="$(pwd)/logs.txt"

# 若日志不存在,则创建日志文件
if [ -e "$log_file_path"  ];then
    touch $log_file_path
fi

# 清空日志文件
> $log_file_path

# 永久地将错误输出重定向到标准输出
exec 2>&1

# 永久地将标准输出重定向到日志文件中
exec 1> $log_file_path

# 记录日志的方法
function log_message(){
    echo $(date '+%Y-%m-%d %H:%M:%S') $0:  $@ 
}

# 测试 shift
function params_manip(){
    for params in $@;do
       log_message $1
       shift
    done
}

# 调用方法
params_manip 11 222 333 44 55

# 测试 getopts 解析参数
function test_func_params(){
while getopts 'sn:v:' OPTION; do
  case "$OPTION" in
    s)
      echo "switch on"
      ;;
    n)
      name="$OPTARG"
      echo "name is $name"
      ;;
    v)
      value="$OPTARG"
      echo "value is $value"
      ;;
    ?)
      echo "script usage: $(basename $0) [-s] [-n somevalue] [-v somevalue]" >&2
      ;;
  esac
done

}

# 调用方法
test_func_params -s -n xiashuo -v 10

赋权后执行

1
2
3
4
5
6
7
8
9
$ ./function.sh && cat logs.txt
2023-09-08 14:47:51 ./function.sh: 11
2023-09-08 14:47:51 ./function.sh: 222
2023-09-08 14:47:51 ./function.sh: 333
2023-09-08 14:47:51 ./function.sh: 44
2023-09-08 14:47:51 ./function.sh: 55
switch on
name is xiashuo
value is 10

return 命令

return命令用于从函数返回一个退出码,这个退出码的范围为 0-255。函数执行到这条命令,就不再往下执行了,直接返回了。

1
2
3
function func_return_value {
  return 10
}

函数将退出值返回给调用者。如果命令行直接执行函数,下一个命令可以用$?拿到退出码的值。

1
2
3
$ func_return_value
$ echo "Value returned by function is: $?"
Value returned by function is: 10

return后面不跟参数,只用于返回也是可以的。此时会返回函数体中最后一个命令的退出码。

1
2
3
4
function name {
  commands
  return
}

具体请看《Bash 脚本》的return 命令小节

如果函数想返回除退出码以外的内容,则可以在函数体中通过 echo 语句中输出,然后用子命令拓展符号将调用方法的语句包起来,例如$(func param1 param2),这样就可以的获取函数中所有的 echo 语句返回的值,默认以 IFS 变量分割,甚至还可以通过()包起来,成为数组。

参考:Return value in a Bash function - Stack Overflow

关于子命令拓展,请看《Bash 的模式拓展》的子命令拓展小节

关于数组,请看《Bash 数组》小节

简单实践如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ function test_return(){
> echo 111
> echo 222
> echo 333
> }
$ value=$(test_return)
$ echo $value
111 222 333
$ value_arr=($(test_return))
$ echo ${value_arr[@]}
111 222 333
$ echo ${#value_arr[@]}
3

传递参数到子脚本和方法中的时候无法传递数组类型,只能传递字符串类型,其实也就证明 Bash 中不存在单独的数组类型,只有字符串一个类型。

既然 Bash 没有数据结构,没有原始数据类型和对象类型这些东西,自然也就不存在什么值传递和引用传递的区别,

而这,这是 bash 的局限性。

全局变量和局部变量

Bash 函数体内直接声明的变量,属于全局变量,(在同一脚本中)方法外,甚至别的方法内部都可以读取。

而且函数体内不仅可以声明全局变量,还可以修改全局变量。

函数里面可以用local命令声明局部变量。方法中的局部变量只在方法内部可以使用。

简单实践如下:

创建function_var.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env bash

name="aaa"

function change_var(){
   name="bbb"
   age=12
}

function change_var2(){
    age=22
    local gender="male"
}

change_var
change_var2

echo name: $name
echo age: $age
echo gender: $gender

赋权后执行:

1
2
3
4
$ ./function_var.sh 
name: bbb
age: 22
gender:

local命令声明的$gender变量,只在函数体内有效,函数体外没有定义。

再次感觉方法调用起来比脚本调用起来更加简洁

0%