Shell 脚本编程(高级篇)

高级篇

一、处理用户输入

1. 读取脚本参数

Bash Shell 将命令行中传递给脚本的参数赋值给一组特殊的变量,叫做位置变量(positional parameters)。位置变量用 $number 的形式表示。
$0 表示脚本文件的名称,$1 表示脚本收到的第一个参数,$2 表示第二个参数,以此类推,直到 $9 表示第九个参数。
从第十个参数起,使用 ${number} 的形式。即第十个参数表示为 ${10}

示例程序:

1
2
3
4
5
6
7
8
9
10
11
$ cat add.sh
#!/bin/bash

total=$[ $1 + $2 ]
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The total value is $total"
$ ./add.sh 2 5
The first parameter is 2
The second parameter is 5
The total value is 7

2. 参数检查

在 Shell 脚本中使用命令行参数时,一般需要先对传入的参数进行检查。如脚本执行时没有接收到预想的参数,往往会在执行过程中报出错误。如:

1
2
3
4
5
$ ./add.sh 2
./add.sh: line 3: 2 + : syntax error: operand expected (error token is " ")
The first parameter is 2
The second parameter is
The total value is

所以参数检查是写脚本时很有必要的步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat check_parameter.sh
#!/bin/bash

if [ -n "$1" ]
then
echo Hello $1, glad to meet you.
else
echo "Sorry, you did not identify youself"
fi
$ ./check_parameter.sh starky
Hello starky, glad to meet you.
$ ./check_parameter.sh
Sorry, you did not identify youself

3. 特殊参数变量
参数计数

上面有提到,应该在使用命令行参数前先检查其是否符合要求。对于接收多个参数的脚本,有时候需要获取命令行中输入的参数数量。
Shell 脚本中的 $# 变量保存了该脚本执行时接收到的参数的数量。如:

1
2
3
4
5
6
7
8
9
10
$ cat count_parameters.sh
#!/bin/bash

echo There were $# parameters supplied.
$ ./count_parameters.sh
There were 0 parameters supplied.
$ ./count_parameters.sh test
There were 1 parameters supplied.
$ ./count_parameters.sh test test
There were 2 parameters supplied.

通过 $# 变量的使用,可以将之前的 add.sh 脚本优化为以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cat add2.sh
#!/bin/bash

if [ $# -ne 2 ]
then
echo Usage: add2.sh a b
else
total=$[ $1 + $2 ]
echo The total is $total
fi

$ ./add2.sh
Usage: add2.sh a b
$ ./add2.sh 2
Usage: add2.sh a b
$ ./add2.sh 2 4
The total is 6

获取所有参数

某些情况下需要获取命令行提供的所有参数。除了通过 $# 变量使用循环,还可以直接使用另外两个特殊的变量。

$*$@ 变量都可以包含命令行输入的所有参数。
其中 $* 变量接收所有参数并将它们保存在一个单一的字符串中。
$@ 变量接收所有参数并将它们保存在分开的字符串中。
$* 变量将收到的所有参数作为整体的一个参数对待,而 $@ 变量将收到的所有参数作为不同的多个对象,可以使用 for 命令进行遍历。

这两个变量的区别可以通过以下脚本来区分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ cat iterate_parameters.sh
#!/bin/bash

count=1
for param in "$*"
do
echo "\$* Parameter $count = $param"
count=$[ $count + 1 ]
done

echo
count=1
for param in "$@"
do
echo "\$@ Parameter $count = $param"
count=$[ count + 1 ]
done
$ ./iterate_parameters.sh rich barbara katie jessica
$* Parameter 1 = rich barbara katie jessica

$@ Parameter 1 = rich
$@ Parameter 2 = barbara
$@ Parameter 3 = katie
$@ Parameter 4 = jessica

4. shift 命令

shift 命令可以用来操作命令行参数,对它们进行整体的移位
默认情况下,shift 命令会将所有的命令行参数整体的向左移动一个位置。
$3 变量的值移动到 $2$2 变量的值移动到 $1。如:

1
2
3
4
5
6
7
8
9
$ cat shift.sh
#!/bin/bash

echo "The original parameters: $*"
shift 2
echo "Here's the new parameters: $*"
$ ./shift.sh 1 2 3 4 5
The original parameters: 1 2 3 4 5
Here's the new parameters: 3 4 5

二、函数

1. 函数定义

Bash Shell 脚本中的函数可以用以下两种格式定义:

1
2
3
function name {
commands
}


1
2
3
name() {
commands
}

具体示例如下:

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
$ cat func1.sh
#!/bin/bash

function func1 {
echo "This is an example of a function"
}

count=1
while [ $count -le 5 ]
do
func1
count=$[ $count + 1 ]
done

echo "This is the end of the loop"
func1
echo "Now this is the end of the script"
$ ./func1.sh
This is an example of a function
This is an example of a function
This is an example of a function
This is an example of a function
This is an example of a function
This is the end of the loop
This is an example of a function
Now this is the end of the script

2. 函数返回值
退出状态

默认情况下,某个函数的完成状态(即退出码)即是该函数中最后一条命令的退出码。
在该函数执行后,可以使用 $? 变量获取其退出状态码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cat exit_status.sh
#!/bin/bash

func1() {
echo "trying to display a non-existent file"
ls -l badfile
}

echo "testing the function: "
func1
echo "The exit status is: $?"
$ ./exit_status.sh
testing the function:
trying to display a non-existent file
ls: badfile: No such file or directory
The exit status is: 1

因为函数 func1 中的最后一条命令 ls -l badfile 没有执行成功,所以函数执行完后,变量 $? 的值为 1 而不是 0

return 命令

Bash Shell 可以使用 return 命令指定函数退出时的状态值(整数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat return.sh
#!/bin/bash

function db1 {
read -p "Enter a value: " value
echo "doubling the value"
return $[ $value *2 ]
}

db1
echo "The new value is $?"
$ ./return.sh
Enter a value: 32
doubling the value
The new value is 64

PS: 注意函数执行结束后需要立即使用 $? 获取其返回值,前面不能隔有其他命令;
使用 return 指定的退出码必须介于 0 到 255 之间

3. 函数输出

就像可以把命令的输出内容赋值给 Shell 变量一样,函数的输出同样也可以通过 variable=$(function_name) 的形式赋值给某个变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat func2.sh
#!/bin/bash

function db1 {
read -p "Enter a value: " value
echo $[ $value * 2 ]
}

result=$(db1)
echo "The new value is $result"
$ ./func2.sh
Enter a value: 32
The new value is 64

4. 函数中的变量
向函数传递参数

Bash Shell 对待函数就像对待普通的脚本文件一样。我们可以向脚本程序传递参数,也可以以类似的方式向函数传递参数。

函数可以使用标准参数(如 $#)环境变量代表它从命令语句里接收到的参数。
如函数本身的名称由 $0 定义,$1 代表第一个参数,$# 代表接收到的参数的数目,$* 表示函数接收到的所有参数 ($1 $2 ...)"$@" 表示函数接收到的所有参数,且每一个参数都被双引号包裹 ("$1" "$2" ...)

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
$ cat parameter.sh
#!/bin/bash

function addem {
if [ $# -eq 0 ] || [ $# -gt 2 ]
then
echo -1
elif [ $# -eq 1 ]
then
echo $[ $1 + $1 ]
else
echo $[ $1 + $2 ]
fi
}

echo "Adding 10 and 15:"
value=$(addem 10 15)
echo $value
echo "Try adding just one number:"
value=$(addem 10)
echo $value
echo "Now trying adding no numbers:"
value=$(addem)
echo $value
echo "Finally, try adding three numbers:"
value=$(addem 10 15 20)
echo $value
$ ./parameter.sh
Adding 10 and 15:
25
Try adding just one number:
20
Now trying adding no numbers:
-1
Finally, try adding three numbers:
-1

上述脚本中的 addem 函数通过 $# 变量检查传递给它的参数的数量:

  • 如果参数数量等于 0 或大于 2([ $# -eq 0 ] || [ $# -gt 2 ]),则返回 -1 表示程序非正常退出。
  • 如果参数数量等于 1([ $# -eq 1 ]),则返回两倍于该参数的值($[ $1 + $1 ])。
  • 如果参数数量等于 2([ $# -eq 2 ]),则返回这两个参数的加和($[ $1 + $2 ]
全局变量与局部变量

变量的作用域是一个很容易引起问题的点。
函数中定义的变量可以拥有区别于普通变量的作用域,即它们可以对脚本中的其他部分“不可见”。

函数使用两种类型的变量:

  • 全局变量
  • 局部变量

全局变量是在整个脚本中都保持有效的变量。默认情况下,Shell 脚本中的任何变量都是全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat global.sh
#!/bin/bash

function db1 {
value=$[ $value * 2 ]
}

read -p "Enter a value: " value
db1
echo "The new value is: $value"
$ ./global.sh
Enter a value: 24
The new value is: 48

区别于函数中的全局变量,任何只在函数内部生效的变量可以声明为局部变量,只需要在变量的声明前面加上 local 关键字即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cat local.sh
#!/bin/bash

function func1 {
local temp=$[ $value + 5 ]
result=$[ $temp * 2 ]
}

temp=4
value=6

func1
echo "The result is $result"
echo "The temp value is $temp"
$ ./local.sh
The result is 22
The temp value is 4

由于使用了 local 关健字指定函数内部的 $temp 变量为局部变量,所以函数 func1$temp 变量值的变化(变为 11)并不影响函数外部 $temp 变量的值(仍为 4)。

5. 函数递归

这里用一个计算阶乘的示例简单说明下 Shell 脚本中的函数递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ cat factorial.sh
#!/bin/bash

function factorial {
if [ $1 -eq 1 ]
then
echo 1
else
local temp=$[ $1 - 1 ]
local result=$(factorial $temp)
echo $[ $result * $1 ]
fi
}

read -p "Enter value: " value
result=$(factorial $value)
echo "The factorial of $value is: $result"
$ ./factorial.sh
Enter value: 4
The factorial of 4 is: 24

6. 库

函数的使用可以减少脚本中的重复代码。即可以在脚本的其他部分直接使用函数名调用函数,完成该函数定义的功能,而无需再重新输入一遍定义该函数的大段语句。

这种形式的代码复用可以扩展到多个脚本文件。Bash Shell 允许用户创建库文件,其他 Shell 脚本可以通过引用该库文件使用其中定义的函数。

示例如下:

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
$ cat myfuncs
function addem {
echo $[ $1 + $2 ]
}

function multem {
echo $[ $1 * $2 ]
}

function divem {
if [ $2 -ne 0 ]
then
echo $[ $1 / $2 ]
else
echo -1
fi
}
$ cat use_myfuncs.sh
. ./myfuncs

value1=10
value2=5

result1=$(addem $value1 $value2)
result2=$(multem $value1 $value2)
result3=$(divem $value1 $value2)

echo "The result of adding them is: $result1"
echo "The result of multiplying them is: $result2"
echo "The result of dividing them is: $result3"
$ ./use_myfuncs.sh
The result of adding them is: 15
The result of multiplying them is: 50
The result of dividing them is: 2

其中 myfuncs 库文件中分别定义了 addem multem divem 三个函数,use_myfuncs.sh 脚本用来引用 myfuncs 库并使用其中定义的函数(这两个文件都需要有执行权限)。

重点在于 use_myfuncs.sh 脚本中的第一行命令 . ./myfuncs。其中第一个 .source 命令的缩写。
source 命令的作用是在当前语境下调用另一个脚本,而不是创建一个新的 Shell 会话。这样 myfuncs 中的函数就可以直接被 use_myfuncs.sh 脚本使用。

参考资料

Linux Command Line and Shell Scripting Bible 3rd Edition