第二章 shell程序设计

时间:2023-02-19 20:16:00

2.1 为什么使用shell编程

使用shell进行程序设计的原因之一是,你可以快速简单的完成编程.而且,即使是最基本的Linux安装也会提供一个shell

ls -al | more

这个命令使用了ls和more工具并通过管道实现了文件列表的分屏显示.每个工具就是一个组成部件

man bash | col -b | lpr

这个命令可以打印bash使用手册的参考副本

2.3 什么是shell

shell是一个作为用户与Linux系统间接口的程序,它允许用户向操作系统输入需要执行的命令

我们可以使用 ‘<’ 和 ‘>’ 对输入,输出进行重定向,使用 ‘|’ 在同时执行的程序之间实现数据的管道传递 使用$(…)获取子进程的输出

2.4 管道和重定向

2.4.1 重定向输出

把 ls 命令的输出保存到文件 lsoutput.txt 中

ls -l > lsoutput.txt

文件描述符 0 代表一个程序的标准输入, 文件描述符 1 代表标准输出, 而文件描述符 2 代表标准错误输出

使用 >> 操作符将输出附加到一个文件中

ps >> lsoutput.txt

这条命令会将ps命令的输出附加到指定文件的尾部

如果想对标准错误输出重定向,你需要把想要重定向的文件描述符编号加在 > 操作符的前面.因为标准错误输出的文件描述符编号是 2 ,所以使用 2> 操作符.当需要丢弃错误信息并阻止它显示在屏幕上时,这个方法很有用

下面的命令将把标准输出和标准错误输出分别重定向到不同的文件中

kill -HUP 1234 > killout.txt 2> killerr.txt

如果你想把两组输出都重定向到一个文件中,你可以用 >&操作符来结合两个输出

KILL -HUP 1234 > killouterr.txt 2>&1

这条命令的含义是”将标准输出重定向到文件 killouterr.txt 中,然后将标准错误输出重定向到与标准输出相同的地方”.请注意操作符出现的顺序,如果顺序有误,重定向将不会按照你预期的那样执行

可以使用Linux的通用”回收站” /dev/null来有效地丢弃所有的输出信息

kill -l 1234 >/dev/null 2>&1

2.4.2 重定向输入

more < killout.txt

2.4.3 管道

你可以用管道操作符 | 来连接进程.Linux与MS-DOS不同,在Linux下通过管道连接的进程可以同时运行,并且随着数据流在他们之间的传递可以自动的进行协调

例如,使用sort命令对ps命令的输出进行排序

不使用管道的做法:

ps > psout.txt
sort psout.txt > pssort.out

使用管道:

ps | sort > pssort.out

如果想在屏幕上分页显示输出结果,你可以再连接到第三个进程 more, 将他们都放在同一个命令行上

ps | sort | more

允许连接的进程的数目是没有限制的.假设你想看看系统中运行的所有进程的名字,但是不包括shell本身,可以使用下面的命令

ps -xo comm | sort | uniq | grep -v sh | more

这个命令首先按字母顺序排序 ps 命令,在用 uniq 命令出去名字相同的进程,然后用 grep -v sh 命令删除名为 sh 的进程,最终将结果分页显示在屏幕上

如果你有一系列的命令需要执行,相应的输出文件是在这一组命令被创建的同时立刻被创建或写入的,所以绝不要在命令流中重复使用相同的文件名.如果你尝试执行如下命令:

cat mydata.txt | sort | uniq > mydata.txt

最终将得到一个空文件,因为你在读取文件 mydata.txt 之前就已经覆盖了这个文件的内容

2.5 作为程序设计语言的shell

编写shell脚本的程序有两种方式.你可以输入一系列命令让shell交互的执行他们,也可以把这些命令保存到一个文件中,然后将该文件作为一个程序来调用

2.5.1 交互式程序

假设你想要从大量的C语言文件中查找包含POSIX的文件.与其使用 grep 命令在每个文件中搜索字符串,然后再分别列出包含该字符串的文件,不如用下面的交互式脚本来执行整个操作

for file in *
do
if grep -l POSIX $file
then
more $file
fi
done

grep 命令输出它找到的包含POSIX字符串的文件,然后 more 命令将文件的内容显示在屏幕上.最后,返回shell提示符.需要注意的是,你用shell变量来处理每个文件,以使该脚本自文档化.你也可以将变量起名为i,但变量名file更容易理解

shell还提供了通配符扩展(通常称为globbing).可以用通配符 * 来匹配一个字符串,通配符 ? 来匹配单个字符,而 [set] 允许匹配括号中任何一个单个字符,[^set] 对括号中的内容取反,即匹配任何没有出现在给出的字符集中的字符,扩展的花括号 {}(只能用在部分shell中,包括bash)允许你将任意的字符串组放在一个集合中,以供shell扩展,例如

ls my_{finger,toe}s

这个命令将列出文件my_fingers和my_toes,它使用shell来检查当前目录下的每个文件

有经验的Linux用户可能会用一种更有效的方式来执行这个简单的操作,也许使用如下命令:

more `grep -l POSIX *`(grep -l POSIX *是在反引号当中,反引号在tab键的上面)

或者使用功能相同的另一种命令形式:

more $(grep -l POSIX *)

此外,下面的命令将输出包含POSIX字符串的文件名

grep -l POSIX * | more

2.5.2 创建脚本

#!/bin/sh

for file in *
do
if grep -q POSIX $file
then
echo $file
fi
done

exit 0

程序中的注释以 # 开头,一直持续到该行结束.请注意第一行 #!/bin/sh ,它是一种特殊形式的注释, #! 字符告诉系统同一行上紧跟在它后面的那个参数是用来执行本文件的程序.在这个例子中, /bin/sh是默认的shell程序

因为脚本程序本质上被看作是shell的标准输入,所以它可以包含任何能够通过你的PATH环境变量引用到的Linux命令

exit命令的作用是确保脚本程序能够返回一个有意义的退出吗.当程序以交互方式运行时,我们很少需要检查它的退出码,但如果你打算从另一个脚本程序里调用这个脚本程序并查看它是否执行成功,那么返回一个适当的退出码就很重要了

在shell程序设计里, 0表示成功.注意,这个脚本没有使用任何的文件扩展名或后缀,一般情况下,Linux和UNIX很少利用文件扩展名来决定文件的类型.可以为脚本使用 .sh 或者其他扩展名,但shell并不关心这一点.大多数预安装的脚本程序并没有使用任何文件扩展名,检查这些文件是否是脚本程序的最好方法是使用 file 命令, 例如:

file first 或者 file /bin/bash

2.5.3 把脚本设置为可执行

运行脚本程序有两种方法

/bin/sh first

或者

chmod +x first
first

你可能会看到一条错误信息告诉你未找到命令,因为shell环境变量 PATH 并没有被设置为在当前目录下查找要执行的命令.要解决这个问题,一种办法是在命令行上直接输入命令 PATH=$PATH:. 或编辑 .bash_profile文件,将刚才这条命令添加到文件的末尾,然后退出登陆之后重新登陆进来.另外,你也可以在保存脚本程序的目录中输入命令 ./first,该命令的作用是把脚本程序的完整的相对路径告诉shell

用 ./ 来指定路径还有另一个好处,他能够保证你不会意外执行系统中与你的脚本同文件名的另一个命令

在确信你的脚本程序能够正确执行后,你可以把它充当前目录移到一个更适合的地方去.如果这个命令只供你本人使用,可以在你的家目录创建一个 bin 目录,并且将该目录添加到你自己的 PATH 变量中.如果你想让其他人也能够执行这个脚本程序,你可以将 /usr/local/bin 或其他系统目录作为添加新程序的适当位置

系统管理员用来设置文件属主和访问权限的一系列命令如下所示

sudo cp first /usr/local/bin/
sudo chown root /usr/local/bin/first
sudo chgrp root /usr/local/bin/first
sudo chmod 755 /usr/local/bin/first

可以使用含义更明确的 chmod 命令

sudo chmod u=rwx,go=rx /usr/local/bin/first

2.6 shell语法

在接下来的几节里,我们将学习一下内容

  • 变量: 字符串, 数字, 环境和参数
  • 条件: shell中的布尔值
  • 程序控制: if, elif, for, while, until, case
  • 命令列表
  • 函数
  • shell内置命令
  • 获取命令的执行结果
  • here文档

2.6.1 变量

在shell里,使用变量之前通常并不需要事先为他们做出声明.你只是通过使用他们(比如当你给他们赋初始值时)来创建他们.在默认情况下,所有变量都被看作字符串并以字符串来存储,即使它们被赋值为数值时也是如此.shell和一些工具程序会在需要时把数值型字符串转换为对应的数字以对它们进行操作.Linux是一个区分大小写的系统,因此shell认为变量foo与Foo是不同的,而这两者与FOO又是不同的

在shell中,你可以通过在变量名前加一个 访., 符号.当你为变量赋值时,你只需要使用变量名,该变量会根据需要被自动创建.一种检查变量内容的单间方式就是在变量名前面加一个 $ 符号,再用 echo 命令将它的内容输出到终端上

例如:在命令行上,你可以通过设置和检查变量salutation的不同之来实际查看变量的使用

salutation=Hello
echo $salutation

salutation="Yes Dear"
echo $salutation

salutation=7+5
echo $salutation

注意,如果字符串里包含空格,就必须用引号把他们括起来.此外,等号两边不能有空格

你可以使用 read 命令将用户的输入赋值给一个变量.这个命令需要一个参数,即准备读入用户输入数据的变量名,然后他会等待用户输入数据.通常情况下用户按下回车键时, read 命令结束.当从终端上读取一个变量时,你一般不需要使用引号,如下所示:

$ read salutation
Hello world

$ echo $salutation
Hello world
1. 使用引号

一般情况下,脚本文件中的参数以空白字符分隔(可以是一个空格,一个制表符或者一个换行符).如果你想在一个参数中包含一个或者多个空白字符,你就必须给参数加上引号

如果你把一个 ,;,, 字符前面加上一个 \ 字符以取消它的特殊含义

字符串通常都被放在双引号中,以防止变量被空白字符分开,同时又允许 $ 扩展

变量的使用实例

#!/bin/sh

myvar="Hi here"

echo $myvar
echo "$myvar"
echo '$myvar'
echo \$myvar

echo enter some text
read myvar

echo '$myvar' now equals $myvar
exit 0
2.环境变量

当一个shell脚本程序开始执行时,一些变量会根据环境设置中的值进行初始化.这些变量通常用大写字母做名字,以便把他们和用户在脚本程序里定义的变量区分开来,后者按惯例都是用小写字母做名字

$HOME  :   当前用户的家目录
$PATH : 以冒号分隔的用来搜索命令的目录列表
$PS1 : 命令提示符,通常是 $ 字符
$PS2 : 二级提示符,用来提示后续的输入,通常是 > 字符
$IFS : 输入域分隔符.当shell读取输入时,它给出用来分隔单次的一组字符,它们通常是空格,制表符和换行符
$0 : shell脚本的名字
$# : 传递给脚本的参数的个数
$$ : shell脚本的进程号,脚本程序通常会用它来生成一个唯一的临时文件,如:/tmp/tmpfile_$$
3.参数变量

如果脚本程序在调用时带有参数,一些额外的变量就会被创建.即使没有传递任何参数,环境变量 $# 也依然存在,只不过它的值是0罢了

$1, $2, ...     :   脚本程序的参数
$* : 在一个变量中列出所有的参数,各个参数之间用环境变量IFS中的第一个字符分隔开.
如果IFS被修改了,那么 $* 将命令行分割为参数的方式随之改变
$@ : 它是$*的一种精巧的变体,它不使用 IFS 环境变量,所以即使 IFS 为空,参数也不会挤在一起

通过下面的例子,你可以很容易的看出 @ * 之间的区别

$ IFS=''
$ set foo bar bam
$ echo "$@"
foo bar bam
$ echo "$*"
foobarbam
$ unset IFS
$ echo "$*"
foo bar bam

如你所见,双引号里面的 @,IFS.,访,使 @ 是明智的选择

除了使用 echo 命令查看变量的内容外,你还可以使用 read 命令来读取他们

使用参数和环境变量实例

#!/bin/sh

salutation="Hello"
echo $salutation
echo "The program $0 is now running"
echo "The second parameter was $2"
echo "The first parameter was $1"
echo "The parameter list was $*"
echo "The user's home directory is $HOME"

echo "Please enter a new greeting"
read salutation

echo $salutation
echo "The script is now complete"
exit 0

2.6.2 条件

一个shell脚本能够对任何可以从命令行上调用的命令的退出码进行测试,其中也包括你自己编写的脚本程序.这也就是为什么要在所有自己编写的脚本程序的结尾包括返回值 exit 命令的原因

test 或 [ 命令

在实际工作中,大多数脚本程序都会广泛的使用shell的布尔判断命令 [ 或 test.在一些系统上,这两个命令的作用是一样的,只是为了增强可读性,当使用 [ 命令时,我们还是用符号 ] 来结尾

实例:检查一个文件是否存在

if test -f fred.c
then
...
fi

还可以这样写:
if [ -f fred.c ]
then
...
fi

test命令的退出码(表明条件是否满足)决定是否需要执行后面的条件代码

注意,你必须在 [ 符号和被检查的条件之间流出空格.要记住这一点,你可以把 [ 符号看作和 test 命令一样,而 test 命令之后总是应该有一个空格

如果你喜欢把then和if放在同一行,就必须要用一个分号把test语句和then分隔开.如下所示:

if [ -f fred.c ];then
...
fi

test命令可以使用的条件类型可以归类为三类:字符串比较,算数比较与文件有关的条件测试

字符串比较

string1 = string2   :   如果两个字符串相同则结果为真
string1 != string2 : 如果两个字符串不同则结果为真
-n string : 如果字符串不为空则结果为真
-z string : 如果字符串为null(一个空字符串)则结果为真

算术比较

expression1 -eq expression2 :   如果两个表达式相等则结果为真
expression1 -ne expression2 : 如果两个表达式不相等则结果为真
expression1 -gt expression2 : 如果expression1大于expression2则结果为真
expression1 -ge expression2 : 如果expression1大于等于expression2则结果为真
expression1 -lt expression2 : 如果expression1小于expression2则结果为真
expression1 -le expression2 : 如果expression1小于等于expression2则结果为真
!expression 如果表达式为假则结果为真,反之亦然

文件条件测试

-d file :   如果文件是一个目录则结果为真
-e file : 如果文件存在则结果为真.注意,历史上 -e 不可移植,所以通常使用的是 -f 选项
-f file : 如果文件是一个普通文件则结果为真
-g file : 如果文件的set-group-id位被设置则结果为真
-r file : 如果文件可读则结果为真
-s file : 如果文件的大小不为0则结果为真
-u file : 如果文件的set-user-id位被设置则结果为真
-w file : 如果文件可写则结果为真
-x file : 如果文件可执行则结果为真

测试 /bin/bash 文件状态

#!/bin/sh

if [ -f /bin/sh ]
then
echo "file /bin/bash is exists"
fi

if [ -d /bin/bash ]
then
echo "/bin/bash is a directory"
else
echo "/bin/bash is not a directory"
fi

exit 0

2.6.3 控制结构

1. if 语句

if 语句非常简单:他对某个命令的执行结果进行测试,然后更具测试结果有条件的执行一组语句.如下所示:

if condition
then
statements
else
statements
fi

使用 if 语句

#!/bin/sh

echo "Is it morning? Please enter yes or no"
read timeofday

if [ $timeofday = "yes" ]
then
echo "This is morning!"
else
echo "This is afternoon!"
fi

exit 0

注意,你用额外的空白符来缩进 if 结构内部的语句.这只是为了照顾人么的阅读习惯,shell会忽略这些多余的空白符

2. elif 语句

elif 结构允许你在 if 结构的 else 部分被执行时增加第二个检查条件

用 elif结构 做进一步检查

#!/bin/sh

echo "Is it morning? Please enter Yes or no"

read timeofday

while true
do
if [ $timeofday = "yes" ]; then
echo "good morning!"
exit 0
elif [ $timeofday = "no" ]; then
echo "good afternoon!"
exit 0
else
echo "Sorry, $timeofday not recongnied. Please enter yes or no!"
read timeofday
fi
done

exit 0
3. 一个与变量有关的问题

这个脚本程序还潜藏着一个更加隐蔽的问题.运行这个新的脚本程序,但这次不回答问题,而是直接按下回车键.你将看到如下所示的出错信息:

./elif.sh: 9: [: =: unexpected operator
./elif.sh: 12: [: =: unexpected operator

问题就在第一个 if 语句中.在对变量 timeofday 进行测试的时候,它包含一个空字符串,使得 if 语句成为下面这个样子:

if [ = "yes" ]

而这不是一个合法的条件,为了避免出现这种情况,你必须给变量加上引号,如下所示:

if [ "$timeofday" = "yes" ]

这样,一个空变量提供的就是一个合法的测试了:

if [ "" = "yes" ]

新的脚本程序如下:

#!/bin/sh

echo "Is it morning? Please enter Yes or no"

read timeofday

while true
do
if [ "$timeofday" = "yes" ]; then
echo "good morning!"
exit 0
elif [ "$timeofday" = "no" ]; then
echo "good afternoon!"
exit 0
else
echo "Sorry, $timeofday not recongnied. Please enter yes or no!"
read timeofday
fi
done

exit 0

如果你想让 echo 命令去掉每一行后面的换行符,可移植性最好的办法是使用 printf 命令,而不是 echo 命令.有的shell用 echo -e 命令来完成这一任务,但是并不是所有的系统都支持改名了.bash 使用 echo -n 命令来去除换行符

4. for语句

我们可以用 for 结构来循环处理一组值,这组值可以是任意字符串的集合.它们可以在程序里被列出,更常见的是使用shell的文件名扩展结果

它的语法很简单

for variable in values
do
statements
done

实例:使用固定字符串的 for 循环

#!/bin/sh

for foo in bar fud 43
do
echo "$foo"
done

exit 0

使用通配符扩展的 for 循环

#!/bin/sh

for file in $(for*.sh)
do
echo $file
done

exit 0

这个例子演示了 (command).,for () 中的命令的输出结果

shell 脚本程序中的所有变量扩展都是在脚本程序被执行时而不是在编写它时完成的.所以,变量声明中的语法错误只有在执行时参会被发现,就像前面我们诶空变量加引号中的例子中看到的那样

5. while语句

因为在默认情况下,所有的shell变量值都被认为是字符串,所以for循环特别适合对于一系列字符串进行循环处理,但是如果你事先并不知道循环要执行的次数,那么它就显得不那么有用了

如果需要重复执行一个命令序列,但是事先又不知道这个命令序列应该执行的次数,通常可以使用 while 循环

while condition do
statements
done

一个简陋的密码检查程序:

#!/bin/sh

echo "Enter password"
read secret

while [ "$secret" != "secret" ];
do
echo "Sorry,try again"
read secret
done

exit 0
6. until语句

until 语句的语法如下所示:

until condition
do
statements
done

它与 while 循环很相似,只是把测试条件反过来了.换句话说,循环反复执行直到条件为真,而不是条件为真时反复执行

一般来说,如果循环至少执行一次,那么就是用 while 循环;如果可能根本都不需要执行循环,就使用 until 循环

下面是一个 until 循环的例子,你设置一个警报,当某个特定的用户登陆时该警报就会启动

#!/bin/sh

until who | grep "$1" > /dev/null
do
sleep 60
done

echo -n '\a'
echo "**** $1 has just logged in****"

exit 0
7. case语句

case 结构比你目前为止见过的其他结构都要稍微复杂一些,它的语法如下:

case cariable in
pattern [ | pattern ] ...) statements;;
pattern [ | pattern ] ...) statements;;
...
esac

请注意, 每个模式行都以双分号 (;;) 结尾.因为你可以在前后模式之间放置多条语句,所以需要使用一个双分号来标记前一个语句的结束和后一个模式的开始

在case结构的模式中使用如 * 这样的通配符时要小心.因为case将使用第一个匹配的模式,即使后续的模式有更加精确的匹配也是如此

case 示例一: 用户输入

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

case $timeofday in
yes ) echo "good morning";;
no ) echo "good afternoon";;
y ) echo "good morning";;
n ) echo "good afternoon";;
* ) echo "Sorry, answer not recongnized";;
esac

exit 0;

case 示例二: 合并匹配模式

#!/bin/sh

echo "Is it morning? Please enter yes or no"
read timeofday

case $timeofday in
yes | y | YES | Y ) echo "good morning";;
no | n | NO | N ) echo "good afternoon";;
* ) echo "Sorry, answer not recongnized";;
esac

exit 0

case 示例三: 执行多条语句

#!/bin/sh

echo "Is it morning? Please enter yes or no"
read timeofday

case $timeofday in
YES | Y | yes | y )
echo "good morning"
echo "Up bright an early this morning"
;;
[Nn]* )
echo "good afternoon"
;;
* )
echo "sorry, answer not recongnized"
echo "Please answer yes or no"
exit 1
;;
esac

exit 0

为了让case的匹配功能更强大,你可以使用如下的模式:

[Yy] | [Yy][Ee][Ss] )
8. 命令列表
  • AND 列表

AND 列表结构允许你按照这样的方式执行一系列命令:只有在前面所有的命令都执行成功的情况下执行后一条命令.它的语法是:

statement1 && statement2 && statement3 && ...

从左开始顺序执行每条命令,如果一条命令返回的是true,它右边的下一条命令才能执行.如此持续直到有一条命令返回false,或者列表中所有的命令都执行完毕. && 的作用是检查前一条命令的返回值

每条语句都是独立执行,这就允许你把许多不同的命令回合在一个单独的命令列表中,就像下面的脚本程序显示的那样.AND 列表作为一个整体,只有在列表中的所有命令都执行成功时,才算它执行成功,否则,就算它失败

AND 列表

#!/bin/sh

touch file_one
rm -f file_two

if [ -f file_one ] && echo "Hello" && [ -f file_two ] && echo "there "
then
echo "in if"
else
echo "in else"
fi

exit 0
  • OR 列表

OR列表结构允许我们持续执行一系列命令直到有一条命令成功为止,其后的命令将不在被执行.它的语法是:

statement1 || statement2 || statement3 || ...

从左开始顺序执行每条命令.如果一条命令返回的是false,它右边的下一条命令才能够被执行.如此持续直到有一条命令返回true,或者列表中的所有命令都执行完毕

|| 列表和 && 列表很相似,只是继续执行下一条语句的条件现在变为其前一条语句必须执行失败

OR 列表

#!/bin/sh

rm -rf file_one

if [ -f file_one ] || echo "Hello" || echo "There"
then
echo "in if"
else
echo "in else"
fi

exit 0
9. 语句块

如果你想在某些只允许使用单个语句的(比如在 AND 或 OR)使用多条语句,你可以把他们括在花括号 {} 中来构造一个语句块

get_confirm && {
grep -v "$cdcatnum" $tracks_file > $temp_file
cat $temp_file > $tracks_file
echo
add_record_tracks
}

2.6.4 函数

要定义一个shell函数,你只需要写出它的名字,然后是一对空括号,再把函数中的语句放在一对花括号中,如下所示:

function_name () {
statements
}

一个简单的函数

#!/bin/sh

foo(){
echo "Function foo is excuting"
}

echo "script starting"
foo
echo "script end"

exit 0

当一个函数被调用时,脚本程序的位置参数( \*, @, #,1, $2等)会被替换为函数的参数.这也是你读取传递给函数的参数的办法.当函数执行完毕后,这些参数会恢复他们先前的值

一些老本版本的shell在函数执行之后可能不会恢复位置参数的值.所以如果你想让自己的脚本程序具备可移植性,就最好不要依赖这一行为

你可以通过 return 命令让函数返回数字值.让函数返回字符串值常用的方法是让函数将字符串保存在一个变量中,该变量然后可以在函数结束之后被使用.此外,你还可以 echo 一个字符串并捕获其结果,如下所示

foo(){echo JAY;}
...
result="$(foo)"

可以使用 local 关键字在shell函数中声明局部变量,局部变量将仅在函数的作用范围内有效.此外,函数可以访问全局作用范围内的其他shell变量.如果一个局部变量和一个全局变量的名字相同,前者就会覆盖后者,但是仅限于函数的作用范围之内.例如:

#!/bin/sh

sampel_text="global variable"

foo(){
local sampel_text="local variable"
echo "Function is excuting"
echo $sampel_text
}

echo "script starting"
echo $sampel_text

foo

echo "script ended"

echo $sampel_text

exit 0

如果在函数里没有使用 return 命令指定一个返回值,函数返回的就是执行的最后一条命令的退出码

从函数中返回一个值

#!/bin/sh

yes_or_no(){
echo "Is your name $*"
while true
do
echo -n "Enter yes or no "
read x
case $x in
[Yy] | [Yy][Ee][Ss] ) return 0;;
[Nn] | [Nn][Oo] ) return 1;;
* ) echo "Answer yes or no"
esac
done
}

echo "Original parameters are $*"

if yes_or_no "$1"
then
echo "Hi $1, nice name"
else
echo "Never mind"
fi

exit 0

给函数传参数:在执行函数时直接在函数后面添加参数,然后用 1, 2, … 引用参数

2.6.5 命令

可以在shell脚本程序内部执行两类命令.一类是可以在命令提示符中执行的”普通”命令,也称为 外部命令(external command), 一类是我们提到的 “内置” 命令,也称为 内部命令(internal command).内置命令实在shell内部实现的,他们不能作为外部程序程序调用

1. break命令

你可以用这个命令在控制条件未满足之前,跳出 for, while和 until 循环.你可以为 break 命令提供一个额外的数值参数来表明需要跳出的循环层数,但我们并不建议这么做,因为他将大大降低程序的可读性.在默认情况下,break只跳出一层循环

#!/bin/sh

rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4

for file in fred*
do
if [ -d $file ]; then
break;
fi
done

echo first directory starting fred was $file

rm -rf fred*

exit 0
2. : 命令

冒号 (:) 命令是一个空命令.它偶尔会被用于简化条件逻辑,相当于 true 的一个别名.由于它是内置命令, 所它运行的比 true 快,但它的输出可读性比较差

你可能会看到将它用作 while 循环的条件, while : 实现了一个无限循环,代替了更常见的 while true

: 结构也会被用在变量的条件设置中,例如:

: ${var:=value}

如果没有 : shell将试图把 $var 当作一条命令来处理

#!/bin/sh

rm -rf fred

if [ -f fred ]; then
:
else
echo "file fred did not exists"
fi

exit 0
3. continue 命令

非常类似C语言中的同名语句,这个命令使 for, while或 until循环跳到下一次循环继续执行,循环变量取循环列表中的下一个值

#!/bin/sh

rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4

for file in fred*
do
if [ -d "$file" ]; then
echo "skipping directory $file"
continue
fi
echo file is $file
done

rm -rf fred*
exit 0
4. . 命令

点 (.) 命令用于在当前shell中执行命令

外部的 source 命令和 点 命令(这两个命令差不多是同义词)在执行脚本的程序中列出的命令时,使用的是调用该脚本程序的同一个shell

5. echo 命令

虽然,X/Open建议在现代shell中使用 printf 命令,但我们还是依照常规使用echo命令来输出结尾带有换行的字符串

去掉换行符

Linux下可以使用如下命令:

echo -n

也有可能遇到:

echo -e 
6. eval命令

eval 命令允许你对参数进行求值

foo=10
x=foo
y='$'$x
echo $y

它的输出是: $foo, 而
foo=10
x=foo
eval y='$'$x
echo $y
输出10.因此,eval命令优点像一个额外的 $,它给出一个变量的值的值

eval 命令十分有用,它允许代码被随时生成和运行.虽然它的确增加了脚本调试的复杂度,但它可以让你完成使用其他方法难以或者根本无法完成的事情

7. exec 命令

exec 命令有两种不同的用法,它的典型用法是将当前shell替换为一个不同的程序.例如:

exec wall "thanks for all the fish"

脚本中的这个命令会用wall命令替换当前的shell.脚本程序中exec命令后面的代码都不会执行.因为这个脚本的shell已经不存在了

8. exit n 命令

exit 命令使脚本程序以退出码 n 结束运行.如果你在任何一个交互式shell的命令提示符中使用这个命令,他会是你退出系统.如果你允许自己的脚本程序在退出时不指定一个退出状态,那么该脚本中最后一条被执行命令的状态将被用作返回值

在shell脚本编程中,退出码 0 表示成功,退出码 1~125 是脚本程序可以使用的错误代码.其余数字具有保留含义,如下所示:

126     :   文件不可执行
127 : 命令未找到
128及以上: 出现一个信号

示例: 如果当前目录下存在一个名为 .profile 的文件,他就返回 0 表示成功

#!/bin/sh

if [ -f .profile ]; then
exit 0
fi

exit 1

上面的代码也可以换一种方式来写

if [ -f .profile ] && exit 0 || exit 1
9. export 命令

export 命令将作为它参数的变量导出到子shell中,并使之在子shell中有效.在默认情况下,在一个shell中被创建的变量在这个shell调用的下级(子)shell中是不可用的.export 命令把自己的参数创建为一个环境变量,而这个环境变量可以被当前程序调用的其他脚本和程序看见.从更技术的角度来说,被导出的变量构成从该shell衍生的任何子进程的环境变量

我们先列出脚本程序 export2.sh

#!/bin/sh

echo "$foo"
echo "$bar"

然后是脚本程序 export1.sh,在这个脚本的结尾,我们调用 export2.sh

#!/bin/sh

foo="Hello World"
export bar="I am Alex"

./export2.sh

如果你运行这个脚本,将得到如下的输出:

./export1.sh

I am Alex
10. expr 命令

expr 命令将它的参数当作一个表达式来求值.它的最常见的用法就是进行如下形式的简单数学运算

x=`expr $x + 1`

反引号 ( “ ) 字符使x取值为命令 expr x+1. ()替换反引号 “,如下所示:

x=$(expr $x + 1)

expr 命令的功能十分强大,它可以完成许多表达式求值计算.如下所示:

expr1 | expr2   :   如果expr1非零,则等于expr1,否则等于expr2
expr1 & expr2 : 只要有一个表达式为零,则等于零,否则等于expr1
expr1 = expr2 : 等于
expr1 > expr2 : 大于
expr1 >= expr2 : 大于等于
expr1 < expr2 : 小于
expr1 <= expr2 : 小于等于
expr1 != expr2 : 不等于
expr1 + expr2 : 加法
expr1 - expr2 : 减法
expr1 * expr2 : 乘法
expr1 / expr2 : 除法
expr1 % expr2 : 取余

在较新的脚本程序中,expr 命令通常被替换为更有效的$((…))语法

11. printf 命令

只有最新版本的shell才提供 printf 命令

它的语法是:

printf "format string" parameter1 parameters2 ...
12. return 命令

return 命令的作用是使函数返回.return 命令有一个数值返回参数,这个参数在调用该函数的脚本程序里被看作是该函数的返回值.如果没有指定参数,return 命令默认返回最后一条命令的退出码

13. set 命令

set 命令的作用是为shell设置参数变量.许多命令的输出结果是以空格分隔的值,如果需要使用输出结果中的某个域,这个命令级非常有用

#!/bin/sh

echo the date is $(date)
set $(date)
echo the month is $2

exit 0
14. shift 命令

shift 命令把所有的参数变量左移一个位置,使 2 1, 3 2,以此类推.原来的 1, 0 仍将保持不变

#!/bin/sh

while [ "$1" != "" ]; do
echo "$1"
shift
done

exit 0
15. trap 命令

trap 命令用于指定在接收到信号后将要采取的行动.trap 命令的一种常见用途是在脚本程序被中断时完成清理工作

可以再命令提示符下输入命令 trap -l 来查看信号编号及其关联的名称

trap 命令有两个参数,第一个参数是接收到指定信号时将要采取的行动,第二个参数是要处理的信号名

trap command signal

常见信号

HUP(1)      :   挂起,通常因终端掉线或用户退出而引发
INT(2) : 中断,通常因按下Ctrl+C组合键而引发
QUIT(3) : 退出,通常因按下Ctrl+\组合键而引发
ABRT(6) : 中止,通常因为某些严重的执行错误而引发
ALRM(14) : 报警,通常用来处理超时
TERM(15) : 终止,通常在系统关机时发送

信号处理

#!/bin/sh

trap 'rm -f /tmp/my_tmp_file_$$' INT
echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$

echo "press interrupt (CTRL-C) to interrupt ...."
while [ -f /tmp/my_tmp_file_$$ ]; do
echo File exists
sleep 1
done

echo the file no longer exists

trap INT
echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$

echo "press interrupt (CTRL-C) to interrupt ...."
while [ -f /tmp/my_tmp_file_$$ ]; do
echo File exists
sleep 1
done

echo we never get here

exit 0
16. unset 命令

unset 命令的作用是从环境中删除变量或者函数

#!/bin/sh

foo="Hello World"
echo "$foo"

unset foo
echo $foo

exit 0
17. 另外两个有用的命令和正则表达式
  • find 命令: 这是个用于搜索文件的命令

    find / -name test -print

这个命令的含义是从根目录开始查找名为 test 的文件,并且输出该文件的完整路径

如果你指定 -mount 选项,你就可以告诉 find 命令不要搜索挂载的其它文件系统的目录

find / -mount -name test -print

find 命令的完整语法格式如下所示

find [path] [options] [tests] [actions]

find 命令常用选项

-depth          :   在查看目录本身之前先搜索目录的内容
-follow : 跟随符号链接
-maxdepth -N : 最多搜索 N 层文件
-mount(或-xdev) : 不搜索其它文件系统中的目录

测试部分:可以提供给find命令的测试非常多,每种测试返回的结构有两种可能:true 或 false.find命令开始工作时,它按照顺序将定义的每种测试依次应用到它搜索的每个文件上,如果返回 false, find命令就停止处理他当前找到的这个文件,并继续搜索.如果一个测试返回true,find命令将继续下一个测试并对当前文件采取行动

-atime N        :   文件在 N 天之前最后被访问过
-mtime N : 文件在 N 天之前最后被修改过
-name pattern : 文件名(不包括路径名)匹配提供的模式pattern,为了确保pattern被传递给find命令而不是由shell来处理,pattern必须总是用括号括起
-newer otherfile: 文件比otherfile要新
-type c : 文件的类型为c.c是一个特殊类型.最常见的是 d(目录)和 f(普通文件)
-user username : 文件的拥有着是指定的用户username

使用带测试的 find 命令

find . -newer while2 printf

action

-exec command   :   执行一条命令..这是最常见的动作之一.这个动作必须使用 \; 字符对结束
-ok command : 与 -exec 类似,但它在执行命令之前会针对每个要处理的文件,
提示用户进行确认,这个动作必须使用 \; 字符对来结束
-print : 打印文件名
-ls : 对当前文件使用命令ls-dils

实例

find . -newer while2 -type f -exec ls -l {} \;

魔术字符串 {} 是 -exec 或 -ok 命令的一个特殊类型的参数,他将被当前文件的完整路径取代

  • grep 命令

这个不寻常的名字代表的是通用正则表达式解析器.你使用 find 命令在系统中搜索文件,而使用 grep命令在文件中搜索字符串.事实上,一种非常常见的用法是在使用 find 命令时,将 grep 作为传递给 -exec 的一条命令

grep 命令使用一个选项,一个要匹配的模式和要搜索的文件,它的语法如下所示

grep [option] PATTERN [FILES]

如果没有提供文件名,则 grep命令将搜索标准输入

grep 命令的一些主要选项

-c  :   输出匹配行的数目,而不是输出匹配的行
-E : 启用扩展表达式
-h : 取消每个输出行的普通前缀,即匹配查询模式的文件名
-i : 忽略大小写
-v : 对匹配模式取反,即搜索不匹配行而不是匹配行

实验:基本的 grep 命令用法

grep in word.txt
grep -c in words.txt words2.txt
grep -c -v in words.txt words2.txt
  • 正则表达式

在正则表达式的使用过程中,一些字符是以特定方式处理的

^   :   指向一行的开头
$ : 指向一行的结尾
. : 任意单个字符
[] : 方括号内包含一个字符范围,其中任何一个字符都可以被匹配, 例如字符范围 a-e,
或在字符范围前面加上 ^ 符号表示使用反向字符范围,即不匹配指定范围内的字符

如果想将上述字符用作普通字符,就需要在他们前面加上 \ 字符

在方括号中还可以受用一些有用的特殊匹配模式

[:alnum:]   :   字母与数字字符
[:alpha:] : 字母
[:ascii:] : ASCII字符
[:blank:] : 空格或制表符
[:cntrl:] : ASCII控制字符
[:digit:] : 数字
[:graph:] : 非控制,非空格字符
[:lower:] : 小写字母
[:print:] : 可打印字符
[:punct:] : 标点符号字符
[:space:] : 空白字符,包括垂直制表符
[:upper:] : 大写字母
[:xfigit:] : 十六进制数字

如果指定了用于扩展匹配的 -E 选项,那些用于控制匹配完成的其它字符可能会遵循正则表达式的规则.对于 grep 命令来说,我们还需要在这些字符之前加上 \ 字符

?       :   匹配是可选的,但最多匹配一次
* : 必须匹配0次或多次
+ : 必须匹配一次或多次
{n} : 必须匹配n次
{n,} : 必须匹配n次或n次以上
{n,m} : 匹配次数在n到m之间,包括n和m

示例

1. 在当前目录下查找以字母 e 结尾的行的所有文件
grep -nr d$ *

2. 查找以字母 a 结尾的单词.需要使用方括号括起来的特殊匹配字符 [:blank:]
grep -nr a[[:blank:]] *

3. 查找以 Hell 开头的5个字母组成的单词.需要使用 [[:space:]]来划定单词的结尾
grep -nr Hell.[[:space:]] *

4. 用扩展grep 模式来搜索只有五个字符长的全部由小写字母组成的单词
grep -nr -E [a-z]{5} *

2.6.6 命令的执行

编写脚本程序时,你经常需要捕获一条命令的执行结果,并把它用在 shell 脚本中.也就是说,你想要执行一条命令,并把该命令的输出放到一个变量中,可以使用 $(command) 或者比较古老的语法 `command`

所有新脚本程序都应该使用 (...),使, , `, \ 等字符所需要应用的相当复杂的规则

$(command) 的结果就是其中命令的输出.注意,这不是该命令的退出状态,而是它的字符串形式的输出结果,例如:

#!/bin/sh

echo the current directory is $PWD
echo The current users is $(who)

exit 0

这种吧命令的执行结果放到变量中的能力是非常强大的,它使得在脚本程序中使用现有的命令并捕获其输出变得很容易

1. 算术扩展

我们可以通过expr命令进行运算,一种更新更好的方法是使用 ((...)), ((…)) 中能够更有效的完成简单的算术运算,如下所示

#!/bin/sh

x=0

while [ "$x" -ne 10 ]; do
echo "$x"
x=$(($x + 1))
done

exit 0
2. 参数扩展
#!/bin/sh

for i in 1 2
do
echo my_secret_process $i_tmp
done

exit 0

输出如下:问题在于shell试图变换 $i_tmp 的值,而这个变量其实并不存在

my_secret_process
my_secret_process

正确的做法

#!/bin/sh

for i in 1 2
do
echo my_secret_process ${i}_tmp
done

exit 0

我们可以在 shell 中采用多种参数替换方法.对于参数处理问题来说,这些方法通常会提供一种精巧的解决方案.下面列出了一些常见的参数扩展方法

${param:-default}  :   如果 param 为空,就把它设置为 default 值
${#param} : 给出 param 的长度
${param%word} : 从 param 的尾部开始删除与 word 匹配的最小部分,然后返回剩余的部分
${param%%word} : 从 param 的尾部开始删除与 word 匹配的最长部分,然后返回剩余部分
${param#word} : 从 param 的头部开始删除与 word 匹配的最小部分,然后返回剩余部分
${param##word} : 从 param 的头部开始删除与 word 匹配的最长部分,然后返回剩余部分

参数的处理

#!/bin/sh

unset foo
echo ${foo:-bar}

foo=fud
echo ${foo:-bar}

foo=/usr/bin/X11/startx
echo ${foo#*/}
echo ${foo##*/}

bar=/usr/local/etc/local/networks
echo ${bar%local*}
echo ${bar%%local*}

exit 0

如果这条语句是 foo:=bar, foo 就会被赋值.这个字符串的操作符的作用是检查变量foo是否存在且不为空.如果它不为空,就返回它的值,否则就把foo赋值为bar并返回这个值

foo:?barfoo,foo:bar. {foo:+bar}语句将在变量foo存在且不为空的情况下返回bar

因为UNIX和Linux系统都非常依赖过滤器的思想,所以一个操作的记过常常必须手动进行重定向.假设你想使用cjpeg程序将一个gif文件转换为一个jpeg文件

cjpeg image.gif >image.jpg

但有时候,你可能需要对大量文件执行这类操作,那么如何实现自动重定向呢

for image in *.gif
do
cjpeg $image > ${image%%gif}jpgvim
done

2.6.7 here 文档

在shell脚本程序中像一条命令传递输入的有一种特殊方式是使用 here 文档.它允许一条命令在获得输入时就好像是在读取一个文件或键盘一样,而实际上是从脚本程序中得到输入的数据

#!/bin/sh

cat <<!FUNKY!
hello
this is a here
doucument
!FUNKY!

2.6.8 调试脚本程序

可以在调用shell时加上命令行选项,或者使用set命令

命令行选项           set选项               说明
sh -n <script> set -o noexec 只检查语法错误,不执行命令
set -n
sh -v <script> set -o verbose 在执行命令之前回显它们
set -v
sh -x <script> set -o xtrace 在处理完命令之后回显它们
set -x
sh -u <script> set -o nounset 如果使用了未定义的变量,就给出出错信息
set -u

可以使用如下命令来启用 xtrace 选项:

set -o xtrace

再用下面的命令来关闭 xtrace 选项:

set +o xtrace

2.8 综合应用

2.8.1 需求

在开始阶段,你至少应该能够做到把每张 CD唱片的基本资料保存起来,如唱片的名字,音乐类型,艺术家或作曲家的名字等.可能还想再保存一些简单的曲目信息.你希望能够以每张CD唱片为单位进行搜索,而不是以曲目资料为单位.为了让这个小小的应用程序比较完整.你还希望能够在这个应用程序中对唱片资料进行输入,更新和删除

2.8.2 设计

如果对曲目数量没有限制,你就有三种选择

  1. 只是用一个文件,用一行来保存 “标题” 信息,再用n行保存该CD唱片上的曲目信息
  2. 将每张CD唱片的所有信息都放置在一行上,允许该行一直延续直到没有曲目信息需要保存为止
  3. 把标题信息和曲目信息分开,用不同的文件来分别保存他们

只有第三种方法能够让你灵活的修改文件的格式,因此我们选择第三种方式

下一个决策是要在文件里放入哪些信息

我们决定对每张CD唱片保存以下信息

  • CD唱片的目录编号
  • 标题
  • 曲目类型(古典, 摇滚, 流行, 爵士等)
  • 作曲家或艺术家

对曲目,我们只保存两条信息:

  • 曲目编号
  • 曲名

为了把这两个文件结合起来,你必须把曲目信息和CD唱片上的其它信息关联起来.为此,你需要使用CD唱片的目录编号.因为它对每张CD唱片都是唯一的,所以它在标题文件中只出现一次,在曲目文件中对每首曲目也只出现一次

让我们来看一个示例标题文件

目录编号            标题              曲目类型            作曲家
CD123 Cool sax 爵士 Bix
CD234 Classic violin 古典 Bach
CD345 Hits99 流行 Various

它所对应的曲目信息:

目录编号                曲目编号                曲名
CD123 1 Some jazz
CD123 2 More jazz
CD234 1 Sonata in D minor
CD345 1 Dizzy

这两个文件通过目录编号结合在一起.标题文件中的一个数据项一般都对应曲目文件中的多行数据

我们使用逗号来分隔数据项.我们把将要用到的函数列在下面:

get_return()
get_confirm()
set_menu_choice()
insert_title()
insert__track()
add_record_tracks()
add_records()
find_cd()
update_cd()
count_cds()
remove_records()
list_tracks()