这个脚本看上去并没有什么问题:递归函数的参数传递和普通函数没什么不同,返回值是通过获取 $? 的值实现的,这是利用了执行命令的退出码。然而,最终的结果却显然是错误的。调试一下就会发现,当递归回溯到尽头时,变量 i 的值被修改为 0;而退出上次函数调用之后,变量 i 的新值也被带了回来,详细信息如清单 6 所示(请注意黑体部分)。
这段脚本问题的根源在于变量的作用域:在 shell 脚本中,不管是否在函数中定义,变量默认就是全局的,一旦定义之后,对于此后执行的命令全部可见。bash 也支持局部变量,不过需要使用 local 关键字进行显式地声明。local 是bash 中的一个内嵌命令,其作用是将变量的作用域设定为只有对本函数及其子进程可见。局部变量只能在变量声明的代码块中可见,这也就意味着在函数内声明的局部变量只能在函数代码块中才能被访问,它们并不会污染同名全局变量。因此为了解决上面这个程序的问题,我们应该使用 local 关键字将 i 声明为局部变量。修改后的脚本如清单 7 所示。
[root@localhost shell]# cat -n getline1.sh
1 #!/bin/bash
2
3 GetLine()
4 {
5 string=$1
6 file=$2
7
8 line=`grep -n $string $file`
9 if [ $? -eq 0 ]
10 then
11 printf "$string is found as the %drd line in $file /n" `echo $line /
| cut -f1 -d:`
12 num=`grep $string $file | wc -l`
13 rtn=0
14 else
15 printf "$string is not found in $file /n"
16 num=0
17 rtn=1
18 fi
19
20 return $rtn;
21 }
22
23 if [ ! -f testfile.$ ]
24 then
25 cat >> testfile.$ <<EOF
26 first line .
27 second line ..
28 third line ...
29 EOF
30 fi
31
32 num=0
33 rtn=0
34 for i in "second" "six" "line"
35 do
36 echo
37 GetLine $i testfile.$
38 echo "return value: $rtn"
39
40 if [ $num -gt 0 ]
41 then
42 echo "$num occurences found totally."
43 fi
44 done
[root@localhost shell]# ./getline1.sh
second is found as the 2rd line in testfile.4280
return value: 0
1 occurences found totally.
six is not found in testfile.4280
return value: 1
line is found as the 1rd line in testfile.4280
return value: 0
3 occurences found totally.
[root@localhost shell]#
[root@localhost shell]# cat -n getline2.sh
1 #!/bin/bash
2
3 GetLine()
4 {
5 local string=$1
6 local file=$2
7
8 local line=`grep -n $string $file`
9 if [ $? -eq 0 ]
10 then
11 printf "$string is found as the %drd line in $file /n" `echo $line /
| cut -f1 -d:`
12 num=`grep $string $file | wc -l`
13 rtn=0
14 else
15 printf "$string is not found in $file /n"
16 num=0
17 rtn=1
18 fi
19
20 return $rtn;
21 }
22
23 if [ ! -f testfile.$ ]
24 then
25 cat >> testfile.$ <<EOF
26 first line .
27 second line ..
28 third line ...
29 EOF
30 fi
31
32 num=0
33 rtn=0
34 for i in "second" "six" "line"
35 do
36 echo
37 GetLine $i testfile.$
38 echo "return value: $rtn"
39
40 if [ $num -gt 0 ]
41 then
42 echo "$num occurences found totally."
43 fi
44 done
[root@localhost shell]# ./getline2.sh
second is found as the 2rd line in testfile.4300
return value: 0
1 occurences found totally.
six is found as the 0rd line in testfile.4300 return value: 0
line is found as the 1rd line in testfile.4300
return value: 0
3 occurences found totally.
清单 12 的运行结果显示,在文件中搜索 six 关键字时的结果是错误的,调试会发现,问题的原因在于:第 8 行使用 local 将 line 声明为局部变量,并将 grep 命令的执行结果赋值给 line 变量。然而不论 grep 是否成功在文件中找到匹配项(grep 程序找到匹配项返回值为 0,否则返回值为 1),第 9 行中 $? 的值总是 0。实际上,第 8 行相当于执行了两条语句:第一条语句使用 grep 在文件中查找匹配项,第二条语句将 grep 命令的结果赋值给变量 line,并设定其作用域只对于本函数及其子进程可见。因此第 9 行命令中 $? 的值实际上是执行 local 命令的返回值,不管 grep 命令的结果如何,它总是 0。
要解决这个问题,可以将第 8 行的命令拆分开,首先使用单独一行将变量 line 声明为 local的,然后再执行这条 grep 命令,并将结果赋值给变量 line(此时前面不能加上 local)。
[root@localhost shell]# cat -n getline3.sh
1 #!/bin/bash
2
3 GetLine()
4 {
5 string=$1
6 file=$2
7
8 line=`grep -n $string $file`
9 if [ $? -eq 0 ]
10 then
11 printf "$string is found as the %drd line in $file /n" `echo $line /
| cut -f1 -d:`
12 num=`grep $string $file | wc -l`
13 rtn=0
14 else
15 printf "$string is not found in $file /n"
16 num=0
17 rtn=1
18 fi
19
20 return $rtn;
21 }
22
23 if [ ! -f testfile.$ ]
24 then
25 cat >> testfile.$ <<EOF
26 first line .
27 second line ..
28 third line ...
29 EOF
30 fi
31
32 num=0
33 rtn=0
34 for i in "second" "six" "line"
35 do
36 echo
37 (GetLine $i testfile.$)
38 echo "return value: $? (rtn = $rtn)"
39
40 if [ $num -gt 0 ]
41 then
42 echo "$num occurences found totally."
43 fi
44 done
[root@localhost shell]# ./getline3.sh
second is found as the 2rd line in testfile.4534
return value: 0 (rtn = 0)
six is not found in testfile.4534
return value: 1 (rtn = 0)
line is found as the 1rd line in testfile.4534
return value: 0 (rtn = 0)
[root@localhost shell]# cat -n getline4.sh
1 #!/bin/bash
2
3 GetLine()
4 {
5 string=$1
6 file=$2
7 num=$3
8 rtn=$4
9
10 line=`grep -n $string $file`
11 if [ $? -eq 0 ]
12 then
13 printf "$string is found as the %drd line in $file /n" /
`echo $line | cut -f1 -d:`
14 eval ${num}=`grep $string $file | wc -l`
15 eval ${rtn}=0
16 else
17 printf "$string is not found in $file /n"
18 eval ${num}=0
19 eval ${rtn}=1
20 fi
21
22 return ${!rtn};
23 }
24
25 if [ ! -f testfile.$ ]
26 then
27 cat >> testfile.$ <<EOF
28 first line .
29 second line ..
30 third line ...
31 EOF
32 fi
33
34 g_num=0
35 g_rtn=0
36 for i in "second" "six" "line"
37 do
38 echo
39 (GetLine $i testfile.$ g_num g_rtn)
40 echo "return value: $? (g_rtn = $g_rtn)"
41
42 if [ $g_num -gt 0 ]
43 then
44 echo "$g_num occurence(s) found totally."
45 fi
46 done
[root@localhost shell]# ./getline4.sh
second is found as the 2rd line in testfile.4576
return value: 0 (g_rtn = 0)
six is not found in testfile.4576
return value: 1 (g_rtn = 0)
line is found as the 1rd line in testfile.4576
return value: 0 (g_rtn = 0)