linux操作系统 第10章 Shell程序设计

                                                                                   第10章    Shell程序设计


10.1  Shell语言概述
10.1.1  Shell语言的特点
  与其他编程语言相比,Shell语言具有如下特点:
  
(1)  Shell是一种解释性语言。这就是说,用Shell语言写的程序不需编译,可以直接由Shell进程解释执行。解释性语言的特点是快捷方便,可以即编即用,但与编译性语言的目标程序来比,解释性语言程序的运行速度要低一些。
  
(2)  Shell是基于字符串的语言Shell只是做字符串处理,不支持复杂的数据结构和运算。Shell的输出也全部是字符方式的。

  (3)  Shell是命令级语言Shell程序全部由命令而不是语句组成,几乎所有的Shell命令和可执行程序都可用来编写Shell程序。Shell命令十分丰富,命令的组合功能也十分强大。所以,用简单的命令和命令组合形成的Shell程序即简洁又高效,可以实现各种复杂的功能。
  另外需要说明的是,不同版本的
Shell程序不完全兼容,差别可能是细微的,也可能是明显的。本章介绍的是Bash编程,它的应用较广泛,兼容性也很好。

10.1.2  Shell程序
  Shell程序也称Shell脚本(script),是由一系列Shell命令为基本元素构成的文本文件。简单的Shell程序可以只是一个命令序列,高级Shell程序中还可以包含复杂的命令组合,定义各种参数和变量、使用条件命令、控制结构以及其他高级特性。
  例
10.1  1Shell程序:
  $ cat hello  #hello程序
  
# This is a shell script to say hello.
  echo Hello World!

  echo -n “Today is ”
  date “+%A, %B %d, %Y.”
  $ . hello  #运行hello程序
  
Hello World!
  Today is Saturday, October 13, 2007.
  $
  这个hello程序的第1行是注释,后面3行是命令。在执行此程序时,Shell依次执行这3个命令并输出显示信息。

Shell程序中的成分:
Shell 脚本是以行为单位的,在执行脚本的时候会分解成一行一行依次执行。脚本中所包含的成分主要有  :注释、命令、Shell变量和结构控制语句。
注释:用于对脚本进行解释和说明,在注释行的前面加上符号#,这样在执行脚本的时候Shell就不会对该行进行解释。
命令:Shell脚本中可以出现任何在交互方式下可以使用的命令。
变量: Shell支持字符串变量和整型变量
结构控制语句:用于编写复杂脚本的流程控制语句

10.1.3  Shell程序的建立与执行
  Shell脚本是文本文件,因此可以用任何文本编辑器(viemacs)建立和编辑脚本。Shell脚本文件的名称没有限定的扩展名,通常不带扩展名或带“.sh”扩展名。
  
Shell脚本的执行方式主要有3种。
  (1) 将脚本作为可执行文件执行
  
$ chmod  a+x  hello
  $ ./hello
  用文本编辑器生成的脚本文件默认是没有x权限的,也就是说是不可直接执行的。赋予x权限后,脚本就可以像一般的Shell命令那样执行了。如果脚本不在系统存放命令的标准目录下,需要在执行时指定脚本的路径。上例中脚本hello放在当前目录下,所以要用./hello来运行。

  (2) 启动一个Shell子进程来执行脚本文件:
  $ bash hello  #bash < hello
  执行此命令行时,Shell进程先启动一个bash子进程,让它执行hello脚本的内容,执行完毕后bash子进程也终止。在这种方式中,脚本是作为命令参数传给子Shell的。子Shell运行时读取该文件并执行其内容,因此脚本文件不必有执行权限。这种方法常用于运行一个其他版本的Shell脚本。假如当前Shellbash,而chello是用C Shell语言写的脚本,则要执行这个脚本时可以用csh chello命令启动一个csh进程来执行它。

  (3) 让当前Shell进程执行脚本文件:
  $ . hello  #注意.后面的空格
  
.”是一个Shell内部命令,hello是它的参数。“.”命令的功能是读取参数指定的文件,执行其内容。这种执行方式与第2种方式类似,区别在于此方式是由当前Shell进程来执行脚本文件的。

      10.2  Shell特殊字符
  Shell定义了一些特殊的字符,称为元字符(meta-characters),它们对Shell有特殊的含义。Shell在读入命令行后,要先对命令行进行扫描,找出元字符并进行相应的替换或处理,以确定要执行的程序和它的参数及执行方式等。
  
Shell的元字符包括:文件通配符、输入/输出重定向及管道符、注释符、命令执行控制符、命令组合与替换符、转义符等。恰当地运用这些字符能够使Shell的功能得到充分发挥。以下分类介绍这些特殊字符的含义和用法。

10.2.1  通配符
  通配符用于描述命令中的文件名参数。当Shell在命令的参数中遇到带有通配符的文件名模式时,它将目录中的所有文件与该模式进行匹配,并用匹配的文件名替换参数中的文件名模式。表9-1列出了常用的通配符。

linux操作系统 第10章 Shell程序设计

  利用通配符来描述文件参数可以简化对多个文件的处理操作。例10.2示意了用通配符构造的文件名模式与文件名之间的匹配关系。
  例
10.2  通配符的匹配作用:
  
zip*  匹配以字符zip开始的任何字符串;
  *
zip  匹配以字符zip结尾的任何字符串;
  
rc?.d  匹配以rc开始、以.d结束, 中间为任何单个字符的字符串;
  [a-d,x,y]  匹配字符abcdxy
  
[!Z]  匹配不为Z的单个字符;
  
[a-f]*  匹配字符af开头的字符串,如abcd2e3.cf.dat
  *
[!o]  匹配不以o结尾的字符串。

10.2.2  输入/输出重定向与管道符
  输入/输出重定向和管道符的作用是改变命令的输入/输出环境。当Shell在命令行中遇到输入/输出重定向或管道符时,它将对命令的标准输入/输出文件作相应的更改,然后再执行命令。表9-2列出了常用的输入/输出重定向与管道符。

linux操作系统 第10章 Shell程序设计

  1. 标准输入/输出重定向
  “<”是标准输入重定向符,它将标准输入stdin重定向到一个文件。“>”是标准输出重定向符,它将标准输出重定向到一个文件。为了区分是哪种输出重定向,可以在符号前加一个文件描述符fdstdoutfd1stderrfd2,所以1>表示标准输出重定向,2>表示标准错误输出重定向。未指定fd时,默认地表示是1>
  10.3  将标准输入改为infile,标准输出改为outfile,标准错误输出改为errfile文件:
  $ myproc >outfile 2>errfile <infile

  2. 合并重定向与归并重定向
  “&>”是标准输出合并重定向符,它将标准输出与标准错误输出合在一起重定向到一个文件。“>&”是标准输出归并重定向符,它将一种标准输出归并到另一种标准输出流中。符号的前后各用一个fd来表示归并的方式。1>&2表示将stdout归并到stderr流中,>&2默认地表示是1>&22>&1表示将stderr归并到stdout流中。
  例
10.4  将标准输出和标准错误输出改向到out文件:
  $ myprog &> out

  10.5  将标准输出改向到out文件,并将标准错误输出并入到标准输出中:
  $ myprog > out 2>&1  #等价于myprog &> out
  10.6  将标准输出并入标准错误输出流:
  $ myprog 1>&2  #避免标准输出被管道改向

  3. 附加重定向
  “>>”是标准输出附加重定向符,它将标准输出stdout或标准错误输出stderr用追加的方式重定向到一个文件。1>>>>表示stdout附加重定向,2>>表示stderr附加重定向。
  例
10.7  .bash_profile文件的尾部添加一行:
  $ echo ‘PATH=$PATH:.’ >> .bash_profile
  4.  here文档
  “<<”是一种特殊的标准输入重定向机制,称为“here文档(here document)”here文档的表示格式是“<< 结束标记字符串”,它的作用是指示Shell将本命令行后面的输入行作为命令的标准输入传给命令,直到遇到结束标记字符串为止。

  例10.8  here文档的使用:
  
sort << End
   > Jone Doe
   > David Nice
   > Masood Shah
   > End
   David Nice
   Jone Doe
   Masood Shah
  cat here-doctest
   sort << End

   Jone Doe
   David Nice
   Masood Shah
   End
  here-doctest
   David Nice
   Jone Doe
   Masood Shah
  $
  here文档主要用在Shell脚本中。它允许将脚本中某个命令的标准输入直接写在该命令行之后。这样,当执行到该命令行时,它不再去等待标准输入而是在本文档内(here文档)直接获取输入进行处理,如上例中here-doctest脚本所示。

  5. 管道
  “|”是管道符,它将前一命令的标准输出作为后一命令的标准输入。“| tee”T型管道符,它将前一命令的标准输出存入一个文件中,并传递给后一命令作为标准输入。
  10.9  将一个目录下的文件列表按名逆序排序后浏览:
  $ ls /dev | sort -r | more
  10.10  将一个文件的内容排序后保存并统计其行数:
  $ sort mylist | tee sort-list | wc -l

10.2.3  命令执行控制符
  命令执行控制符用于控制命令的执行方式,指示Shell何时该执行这个命令以及在何处(前台、后台)执行这个命令。表9-3列出了常用的命令执行控制符。

linux操作系统 第10章 Shell程序设计

  1. 顺序执行
  “;”是顺序执行符,它将两个或多个命令组合在一个命令行中,指示Shell顺序执行这些命令。
  
10.11  转到上一级目录,显示目录的路径名和目录的文件列表:
  $ cd ..; pwd; ls

  2. 条件执行
  “&&”是逻辑与执行符,它将两个或多个命令组合在一个命令行中,指示Shell依次执行这些命令直到某个命令失败为止。“||”是逻辑或执行符,它将两个或多个命令组合在一个命令行中,指示Shell依次执行这些命令直到某个命令成功为止。
  10.12  将文件file1复制到file2,如果成功则删除file1
  
$ cp file1 file2 && rm file1
  10.13  将文件file1复制到file2,如果失败则显示file1
  $ cp file1 file2 || cat file1

  3. 后台执行
  “&”是后台执行符,它指示Shell将该命令放在后台执行。后台执行的命令不占用终端与用户交互,因此Shell在执行后台命令后可以立即返回提示符。
  10.14  在后台运行yes命令,丢弃输出:
  $ yes > /dev/null &

10.2.4  命令组合符
  命令组合符的作用是指示Shell将多个命令组合在一起执行。组合的目的是对这些命令统一进行某种操作,如管道、后台运行、输入/输出重定向等。
  命令的组合形式有两种:
{ 命令; 命令; … } ( 命令; 命令; … )
  两种组合形式的区别在于前者只在本
Shell中执行命令列表,不派生新的Shell子进程,命令执行的结果会影响当前的Shell环境;后者是派生一个新的子Shell进程来执行命令列表,命令在子Shell环境中执行,其执行的结果不会影响当前的Shell环境。

  10.15  在后台顺序执行两命令,5分钟后跳出提示信息Tea is ready”
  
$ ( sleep 300; echo Tea is ready ) &
  10.16  将两命令的输出送到mydocmydoc的第1行是Report,后面是file的内容
  
$ ( echo Report; cat file ) > mydoc
  10.17  统计两个命令的输出行数,这个数字就是常规命令的数目:
  $ { ls /bin; ls /usr/bin } | wc -l

  10.18  两种括号的区别:
  $  pwd
   /home/cherry
  { cd book; pwd }  #由本Shell进程执行命令表
   
/home/cherry/book
  $  pwd
   /home/cherry/book  (Shell进程的当前目录改变了)
  ( cd ..; pwd )  #生成一个子Shell进程执行命令表
   
/home/cherry  (子进程的当前目录已改变)
  $  pwd
   /home/cherry/book  (Shell进程的当前目录没有变)
  $

10.2.5  命令替换符
  当一个字符串被括在反撇号“`”中时,该字符串将先被Shell作为命令解释执行,然后用命令执行后的输出结果替换‘字符串’。
  10.19  命令替换符的用法
  
$ echo Today is `date +%A`  #替换后为echo Today is
                Thursday
   Today is Thursday
  $
  Shell在解析这个命令行时遇到替换符,于是先执行了date命令,用它的输出替代了原date命令所在的位置,然后执行echo命令

10.2.6  其他元字符
  9-4列出了其他几个常用的元字符。

linux操作系统 第10章 Shell程序设计

  空格是命令行元素的分割符,它指示Shell如何识别和拆分完整的命令名、选项及参数。$字符的作用将在10.3节中介绍。#字符用于注释,它表示Shell忽略其后的内容。
  10.20  使用注释符对命令进行说明:
  echo hello  # say hello
   hello
  $

10.2.7  元字符的引用
  当需要引用元字符的原始含义,而不是它的特殊含义时,就必须用引用符对它进行转义,消除其特殊含义。当Shell遇到引用符时,它将该引用符作用范围内的字符看作是普通字符。常用的引用符有3种,即转义符、单引号和双引号。表9-5列出了它们的含义。

linux操作系统 第10章 Shell程序设计

  10.21  在命令行中引用元字符:
  $ echo “* is a wildcard.”  #消除*字符的特殊含义
   *
is a wildcard.
  $ echo ‘The prompter is “$”’  #消除双引号字符的特殊含义
   
The prompter is “$”
  $ echo “Don’t do that!”   #消除单引号字符的特殊含义
   
Don’t do that!
  $ echo “Name   ID   Age   Class”  #消除空格符的特殊含义
   
Name   ID   Age   Class
  $ echo Name   ID   Age   Class  #未转义的空格被看作是分隔符
   
Name ID Age Class
  $ echo \\\*           #1个和第3\字符是转义符
   
\*
  $

       10.3  Shell 变 量
  Shell提供了定义和使用变量的功能。用户可以将一些常用的数据存放在Shell变量中,并在命令行中引用这些变量。使用变量可以定制Shell的行为,方便Shell的使用和编程。
10.3.1  变量的定义与使用
  变量是具有名字的一块内存空间,用于保存程序中要用到的数据。Shell是基于字符串的编程语言,Shell的变量只能存储字符串,因此Shell变量只有两种类型,即字符串和字符串数组

  1. 定义变量
  在Shell中,对变量的定义与赋值是同时完成的。有3种方式可以为变量赋值:
  
(1) 用赋值命令,格式是:
  
变量名=字符串
  注意:变量的名字必须以字母或下划线开头,可以包括字母、数字和下划线。赋值号“=”两边不能有空格。如果字符串中含有空格,应用引号将字符串括起。

  10.22  用变量赋值命令定义变量:
  $ nodehost=beijing.WEB
  $ user=“zhang ming”
  $ path=/bin:/usr/bin:/etc/bin
  $ count=10
  (2) read命令,从标准输入读入字符串赋给变量,格式是:
  
read变量名 [变量名…]

  10.23  定义3个变量并为它们输入值:
  $ read usera userb userc
    joe  zhao  ming
  $
  执行该read命令时,它将等待用户的输入。用户为每个变量输入一个字符串值,中间用空格分开。
  
(3) for命令中定义变量,用于进行循环控制。具体的格式和用法将在10.5.3小节介绍。

  2. 引用变量
  引用变量即是求出变量的值(字符串),替换在发生引用的位置。
  引用变量的方法是在变量名前加引用字符“
$”,格式是:
  
$变量名  或  ${变量名}
  当命令行中出现$字符时,Shell将紧跟其后面的字符串解释为变量名,并对其进行求值和替换。若$字符后面没有合法的变量名,则Shell将此$字符作为普通字符看待。

  例10.24  在命令中引用变量:
  $  dir=/home/cherry/cprogram
  echo $dir  #实际执行echo /home/cherry/cprogram
   /home/cherry/cprogram
  $ cd $dir/hello  #实际执行cd /home/cherry/cprogram/hello
  $ pwd
    /home/cherry/cprogram/hello
  $ echo $ dir  #这里的$被看作普通字符
  
$ dir
  $

  在执行echo命令时,Shell先识别出变量名dir,然后求出dir的值,替换到$dir的位置,最后再执行echo。在执行cd命令时,Shell也是先识别出变量名为dir(因为后面的/字符不是变量名的合法字符),然后引用该变量的值形成实际运行参数。最后的echo命令中,$后面是空格而不是合法的变量名,因此$被作为普通字符对待。
  
注意:引用未定义的变量将得到一个空字符串。若变量名后紧随有字母、数字或下划线,则应将变量名用{}括起

  例10.25  引用变量的方法:
  $ echo $dir_1  # dir_1变量未定义,实际执行echo
     (空串)
  $ echo ${dir}_1  #实际执行echo /home/cherry/cprogram_1
    /home/cherry/cprogram_1
  $ str=“This is a string”
  $ echo “${str}ent test of variables”
     This is a stringent test of variables
  $ echo “$strent test of variables”  #实际执行echo “test of
 
                   variables”
    test of variables
  $

  2. 本地变量与导出变量
  
本地变量包括那些没有被导出的用户变量和环境变量,以及所有的特殊变量。根据作用域的规则,这些变量只能在本Shell中使用。导出变量包括那些导出的用户变量和环境变量。这些导出的变量既可以在本Shell中使用,也可以被本Shell的所有子进程使用。大多数的环境变量都是导出的,用户变量根据需要决定是否导出。所有的特殊变量都是本地的。
  一个
Shell的导出变量的全体(9‑1中右侧部分)构成了该Shell的命令执行环境。用不带参数和选项的env命令可以显示Shell的命令执行环境,即所有的导出变量。有关Shell命令执行环境的介绍见10.3.4小节。

  3. 设置只读变量
  为了防止变量的值被修改(也就是被重新赋值),可以用readonly命令将变量设置为只读的。readonly命令的格式是:
  
readonly 变量名 [变量名…]
  10.26  设置只读变量:
  
$ readonly dir
  $ dir=/home/cherry/project  #对只读变量进行赋值
  
bash: dir: readonly variable  (赋值失败)
  $

  4. 清除变量
  用unset命令清除变量。清除后的变量变为未定义变量,引用其值将得到空字符串。注意:只读变量是不能被清除的。
  
unset命令的格式是:
  
unset变量名[变量名…]
  例10.27  清除变量:
  $ unset dir
  $ echo $dir

  $

10.3.2  变量的作用域
  
变量的作用域是指变量可以被引用的范围。根据变量的作用域来划分,Shell变量可以分为两类,即本地变量和导出变量(也可称为局部变量和全局变量)
  
1. 本地变量
  
在一个Shell中定义的变量默认只在此Shell中才有意义,也就是说它们的作用是局部的。我们称这种变量为本地变量。本地变量只在本Shell中有定义,而在子Shell中是不存在的。

  例10.28  本地变量的作用域:
  
$  dir=/home/cherry/cprogram
  echo $dir
   /home/cherry/cprogram
  $ bash    #进入子Shell
  $ echo $dir
    
(空串表示变量未定义)
  $

  2. 导出变量
  
Shell执行一个命令或脚本时,它通常会派生出一个子进程,由此子进程来执行命令。在很多情况下,我们希望Shell的变量在其子进程中也可以使用,这可以通过“导出(export)”操作,将Shell的变量传递给子进程。导出的变量称为导出变量,它与本地变量的区别在于导出变量可以被任何子进程引用,而本地变量仅在定义它的进程环境下才能使用。
  导出变量的命令是
export,格式为:
  
export 变量名[变量名…]
  当Shell的一个子进程开始运行时,它继承了该Shell进程的全部导出变量。子进程可以修改继承来的变量的值,但修改只是对自己的变量副本进行,不影响父进程中的变量的值。

  10.29  导出变量与本地变量的使用:
  
$ name=Zhang; export name  #定义并导出变量name
  $ title=Dr.; export title  #定义并导出变量title
  $ greeting=“Good morning”   #定义变量greeting
  $ cat var_test
   name=Wang
   echo “$greeting $title $name!”
  $ bash var_test  #在子Shell中引用变量
    
Dr. Wang!
  $ echo “$greeting $title $name!”   #在本Shell中引用变量
    
Good morning Dr. Zhang!
  $

  从执行var_test的子Shell进程的输出可以看出,子进程继承了父Shell进程的两个导出变量,但没有继承父进程的本地变量greeting。在子进程中修改了name的值。它的输出显示未经修改的title变量值与父进程相同;name变量的值已被修改;greeting变量则是未定义的。最后一个echo命令显示子进程对name的修改不影响父进程的name变量值。
  
用命令export还可以将已导出的变量“收回”,使其变回为本地变量,不再为子进程可用。收回变量的命令格式是:
  
export -n变量名

10.3.3  变量的分类
  根据用途和定义方式的不同,Shell变量可以大致分为3类,即用户变量、环境变量和特殊变量。按照作用域的不同,Shell变量又可以分为本地变量与导出变量两类。如图9‑1所示。

linux操作系统 第10章 Shell程序设计

9‑1  Shell变量的分类

  1. 用户变量、环境变量与特殊变量
  
用户变量是由用户为实现某种应用目的而定义的变量。例如,用户可以将一个目录的路径名记在一个变量中,在命令行中可以直接引用该变量,从而避免冗长的输入,如例10.所示。根据需要,用户变量可多可少,可有可无。必要时可以将用户变量导出,使其为子进程可用。
  
环境变量是由系统预定义的一组变量,用于为Shell提供有关运行环境的信息。环境变量定义在Shell的启动文件中,Shell启动后这些变量就已经存在了。在随后的Shell运行过程中,用户可以直接引用环境变量,也可以对其重新赋值以改变环境设置。导出的环境变量可被其子进程继承使用。有关环境变量的含义及环境配置的介绍见10.3.4小节。

  特殊变量是由Shell自定义的一组变量,用于记录Shell当前的运行状态的一些信息,如运行参数、进程号等。特殊变量是本地的、只读的,用户可以引用这些变量,但不能修改它们的值,也不能导出它们。有关特殊变量的含义和使用方法的介绍见10.3.5小节。
  用不带参数和选项的
set命令可以显示Shell的所有变量(不包括特殊变量)

10.3.4  环境变量
  
1. 环境与环境变量
  Shell执行时需要了解一些有关系统和用户的基本信息,比如系统名、用户名、终端类型、使用的语言以及其他默认选项等。这些信息以变量的形式提供,称为环境变量。环境变量的全体就称为Shell的执行环境。
  通常,环境变量是系统预定义的,是对
Shell运行环境的标准设置。不过用户也可以根据需要添加自己的环境变量,实现对Shell的运行环境的特殊设置。系统环境变量的名称全部是大写的,它们是系统预留的,不能用做它用。表9-6列出了常用的几个系统预定义的环境变量。

  表9-6中除PS1PS2变量之外的环境变量都是导出变量,可以在命令子进程中直接使用。

linux操作系统 第10章 Shell程序设计

  2. 环境变量的定义
  环境变量定义在Shell的启动配置文件中,主要的文件有/etc/profile~/.bash_profile~/.bashrcShell启动时通过执行这些配置文件建立起运行环境。启动完成后,这些环境变量都已经被赋予相应的值。在随后的Shell执行过程中,用户可以直接使用这些变量。用户也可以通过编辑配置文件来定制自己的Shell环境,如添加自己的环境变量或修改系统环境变量的初值。

  例10.30  /etc/profile文件中定义的部分环境变量:
  
# /etc/profile
  
  USER=“`id -un`”
  LOGNAME=USER
  MAIL=“/var/spool/mail/$USER”
  HOSTNAME=`/bin/hostname`
  
  export … USER LOGNAME MAIL HOSTNAME
  关于环境配置的更多介绍见10.3.3小节。

  3. 环境变量的使用
  
Shell启动后,用户可以直接引用环境变量,也可以对它们重新赋值。对环境变量的修改只在本次Shell会话中有效,若要使修改长久有效则需要修改启动配置文件。
  
10.31  Shell中引用和修改环境变量:
  $   echo “Hello $LOGNAME!”  #使用LOGNAME变量
   
Hello cherry!
  $ echo $PATH  #显示PATH变量的当前值
    /usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/home/cherry/bin
  $ myprog  #运行当前目录下的一个程序
    
bash:  myprog:  command not found   (找不到命令)

  $ ./myprog  #指定命令的路径,运行程序
   
(执行命令)
  PATH=$PATH:.  #修改PATH变量,使其包含当前目录
  echo $PATH
   /usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/home/cherry/bin:.
  myprog  #Shell在当前目录下搜索到命令
   
(执行命令)
  echo PS1 is “$PS1”, PS2 is “$PS2”
   PS1 is “$”, PS2 is “>”
  $ cat << End > namelist  #将后续行的输入写入namelist文件
  
> Jone Doe
  > David Nice
  > Masood Shah
  >End

  $ PS1=“[[email protected]$HOSTNAME]$”  #修改主命令提示符
  
$ PS2=!   #修改次命令提示符
  [[email protected]]$ cat << End > namelist
   ! Jone Doe
   ! David Nice
   ! Masood Shah
   ! End
  [[email protected]]$

  4.  Shell命令的执行环境
  
所有导出的环境变量和用户变量都可以被Shell的子进程继承使用,它们构成了Shell命令的执行环境。Shell命令的可执行代码有两种形式,即脚本文件和二进制的可执行代码,它们均以Shell的子进程方式运行。命令子进程通过访问命令执行环境可以获取有关运行环境的信息,并将其应用在自己的处理逻辑中。因此,通过对导出变量的设置可以改变Shell下的命令的执行环境,从而影响命令的执行结果。
  命令程序访问执行环境的方法是:脚本程序可以直接引用或修改其执行环境中的变量;
C程序可以用setenv()getenv()函数访问其执行环境中的变量

  例10.32  在脚本中访问执行环境:
  
WORKDIR=$HOME/project/src; export WORKDIR
  cd ~/scripts
  cat env_test
   echo “I am [email protected]$HOSTNAME.”
   cd $WORKDIR
   echo “I am now in $PWD.”
  ./env_test
   I am [email protected]
   I am now in /home/cherry/project/src.
  echo $PWD
   /home/cherry/scripts
  $

  脚本中不加定义地引用了4个环境变量,其中LOGNAMEHOSTNAMEPWD是系统环境变量,WORKDIR是用户定义的导出变量。它们构成了该脚本命令的执行环境。注意:脚本在执行cd命令时修改了PWD环境变量的值,但这种修改只在子进程内有效,它不会影响到父Shell进程的环境变量的取值。

10.3.5  特殊变量
  
Shell中有一组预定义的特殊的变量,其功能是记录Shell当前的运行状态的一些信息,主要包括运行参数、进程标识和命令退出状态等。特殊变量的变量名和值由Shell自动设置。特殊变量都是只读变量,因此在程序中可以引用这些变量,但不能直接对它们赋值。表9-7列出了几个常用的特殊变量。
  注意:由于都是只读变量,表
9-7中列出的是变量的引用表达式,而不是变量名。用户可以用$*$#等来引用变量,但不能将*或#等看作为变量名来进行赋值、导出等操作。

linux操作系统 第10章 Shell程序设计

  1. 参数变量
  
Shell脚本传递数据的途径有两种,一种是通过导出变量,另一种是通过命令行参数。Shell在解析命令行时首先将命令的参数识别出来,存入专门设置的变量中,这些记录运行参数的变量就称为参数变量。命令在其执行期间可以直接引用这些变量,获得此次运行的参数。
  参数变量主要有以下几个:
  
(1) $#:记录命令行参数的个数。
  
(2) $*:记录命令行的整个参数。
  
(3) [email protected]:记录了命令行的各个参数。

  (4)  $i:称为位置变量,是按位置记录命令参数的一组变量,分别为$0$1$2$9${10}。其中,$0为命令名本身$1为命令的第1个参数,$2为命令的第2个参数,。所有超过参数个数的位置变量i(i>$#)的值为空字符串。
  例如,某个命令的名称为
myprog,执行时的命令行是myprog -s “How are you!” joe jean。当该命令被执行时,Shell隐含地为它建立起一系列的参数变量,各参数变量的内容如下:
  
$#4
  $0myprog  
  $1-s

  $2How are you!  
  $3joe  
  $4jean
  $*-s How are you! joe jean   
  “S*”:“-s How are you! joe jean”
  [email protected]-s How are you! joe jean  
  “[email protected]:“-s” “How are you!” “joe” “jean”

  例10.33  在程序中引用参数变量:
  
$ cat var_test
   echo My name is $0, I have $# parameters, they are [email protected]
   echo The first of them is \“$1\”
  $ ./var_test How are you
   My name is var_test, I have 3 parameters, they are How are you
   The first of them is “How”
  $ ./var_test “How are you” cherry
   My name is var_test, I have 2 parameters, they are How are you cherry
   The first of them is “How are you”
  $

  $*[email protected]变量之间有着细微的差别,[email protected]的各参数字符串之间是用一个特殊的分隔符分隔的,而$*的各参数之间是用一个空格作为分隔符的。在不带引号的情况下,$*[email protected]变量的作用相同,它们都等价于$1 $2 $3 …。但在带有引号时,它们的作用就不同了。引号使空格失去分隔符的作用,因此“$*”是一个含有普通空格字符的字符串,它等价于“$1 $2 $3 …”;而[email protected]中的分隔符在引号下仍然有效,因此“[email protected]是由分隔符分隔的各个参数字符串构成的字符串序列,等价于“$1” “$2”“$3”…。当需要逐个处理参数字符串时(例如在for循环中,见10.5.3小节),使用“[email protected]更为安全,因为只有它能正确地区分出各个参数。

  2. 设置参数变量
  
参数变量是只读的,因此用户不能直接对参数变量重新赋值,但却可以通过Shell提供的命令来设置这些变量。
  
1) set命令设置位置变量
  用
set命令可以对位置变量及其他参数变量强制赋值,对位置变量赋值后,参数变量$#[email protected]$*等也相应地被重新赋值。
  格式:
set字符串1字符串2…
  其中,字符串i是要赋给第i个位置变量的值。注意:set不能对$0赋值。

  用set --命令可以清除所有的位置变量。相应地,[email protected]$*变量也被清除,$#变量被清0
  
10.34  设置位置变量:
  
$ date
   五  1024  12:08:05  CST  2008
  $ set `date`
  $ echo  $6  $2  $3  $1
   2008  10 月  24 
  
$
  date命令输出了7个字符串,它们被依次赋给了$1$7变量。随后的echo命令显示了其中的4个变量。

  2) shift命令移动位置变量
  
shift命令的功能是将位置变量与命令行参数的对应关系右移指定的位数,同时将$#变量的值减去相应的数,[email protected]$*等也相应地被重新赋值。注意:shift只移动$1及其后面的位置变量的值,$0变量的值保持不变。
  格式:
shift [位移量]
  未指定位移量参数时,右移1位。

  例10.35  shift命令移动位置变量的值:
  
$ cat proc1
   echo $0; $#; $1; $2; $3; $4; $5;
   shift
   echo $0; $#; $1; $2; $3; $4; $5;
   shift 2
   echo $0; $#; $1; $2; $3; $4; $5;
  $ bash proc1 A B C D E
   proc1; 5; A; B; C; D; E;
   proc1; 4; B; C; D; E; ;
   proc1; 2; D; E; ; ; ;
  $

  3. 其他特殊变量
  
其他常用的特殊变量包括纪录命令退出状态的$?变量和纪录进程PID$$$!变量等。
  
1) 退出状态变量
  
Linux系统中,每个命令在执行结束退出时都要返回给系统一个状态值。例如在C程序中是调用exit(status)函数退出,在Shell脚本中则是用exit status命令退出。其中的status就是返回给系统的状态值。通常的约定是,程序成功结束时返回0状态值;程序出错时返回非0的状态值(比如12、-1)

  $?变量记录了最近一条命令执行结束后的退出状态。当一个命令子进程退出时,它调用exit(status)将退出状态码放入自己的PCB中。随后,Shell处理子进程的善后工作,将它的退出状态码保存在$?变量中。如果该命令执行成功后退出则$?0,否则为非0Shell通过$?来判断上一条命令的成功与否。例如,在进行逻辑与执行“命令1 && 命令2”和逻辑或执行“命令1 || 命令2”时,Shell就是通过命令1执行后的$?值来决定是否执行命令2

  10.36  $?变量获得命令的退出状态:
  
$ rm file1
  $ echo $?
   0  (删除文件成功)
  $ rm file2
   rm: cannot remove ‘file2’ : No such file or directory
  $ echo $?
   1  (删除文件失败)
  $

  2) 命令的进程号
  
$$变量记录了本命令的执行进程的进程号PID。当一个命令以子进程的方式开始运行时,Shell将它的进程号PID存入到它的$$变量中。命令程序中可以引用该变量,但不能修改它。另外一个可能用到的变量是$!变量,它记录了当前作业(即最后一个进入后台的那个作业)PID
  
10.37  $$变量获得命令的进程号:
  
$ echo $0, $$  #显示本Shell进程的命令名和PID
   bash, 15452
  $ sleep 30 &
   [1] 4439

  $ echo $!
   4439
  $ cat pid_test
   echo “My name is $0, my PID is $$.”
  $ pid_test  #以子进程方式执行脚本
   
My name is pid_test, my PID is 16620.
  $ . pid_test  #由本Shell进程执行该脚本
   
My name is bash, my PID is 15452.
  $
  从上例可以看出,当以命令方式直接运行脚本时,它是由一个子进程执行的,具有自己的PID当用“.”命令执行脚本时,它是由本Shell进程执行的,其报出的PID就是本Shell进程的PID

       10.4  Shell表达式
  Shell语言支持表达式计算。Shell表达式主要有两种形式,一是用于数值计算的算术表达式,其结果是数值;另一种是用于进行条件测试或判断的逻辑表达式,其结果是真假值。

10.4.1  数字运算表达式
  与高级语言中的变量不同,Shell变量只有字符类型,它们只能存放整数数字字符串,如“127”等。Shell本身也没有数字运算的能力,必须借助某些命令来进行算术运算。expr就是用来进行数字表达式计算的命令。
  
expr命令
  功能计算表达式。
  
格式】expr 数值1 运算符 数值2
  参数】expr支持以下运算符:
  
+、-、*、/加、减、乘、除、取余。

  & 逻辑与、逻辑或。
  
===!=   等于、恒等于、不等于。
  
><>=<=   大于、小于、大于等于、小于等于。
  
输出+-、*、/%运算输出结果数值;&运算当两个数值都非0时输出第1个数值,否则输出0| 运算当第1个数值非0时输出第1个数,否则输出第2个数;比较运算为真时输出1,否则输出0
  
退出状态算术运算返回状态0;逻辑和比较运算结果为真(0)时返回状态0,为假(0)时返回状态1;出错时返回状态2

  说明
  (1) 运算符两侧必须留有空格,与运算数分开。
  
(2) 算术运算的数值必须是整数,可以是数字字符串常量(如“123”),也可以是数字字符串变量($a)expr命令负责将数字字符串解释为整数,然后进行运算。
  
(3) 运算符如果是Shell的元字符,如*、&|><等,必须用转义符‘\’使其失去特殊意义,不被Shell解释执行。

  例10.38  expr命令用法示例:
  $ a=13
  $ expr “$a” - 4 + 2    # 13-4+2,注意运算符两旁要留有空格
   
11
  $ expr 4 \* 5    # 4*5
   20
  $ expr 5 + 7 / 3    # 5+7/3/运算优先于+运算,结果取整
   
7
  $ expr `expr 5 + 7` / 3  # (5+7)/3,用命令替换改变运算次序
   
4
  $ expr $a \>= 3;   echo $?   # 13 >= 3
   
1
   0

  $ expr 5 \& “$a”; echo $?   # 5 and 13
   5
   0
  $ expr 5 \& 0; echo $?  # 5 and 0
   0
   1
  $ expr 5 \| $a; echo $?  # 5 or 13
   5
   0
  $ a=`expr $a - 2`  #替换后为a=11
  $ echo $a
   11
  $

10.4.2  逻辑测试表达式
  test命令可对字符串、整数及文件进行各类测试。test命令并不输出任何结果,而是用退出状态来表示测试的结果:退出状态为0时表示test成功,测试结果为真;退出状态为1表示test失败,测试结果为假。test的测试结果用于在控制结构中进行条件判断。
  test命令使用的表达式形式见表9-8

linux操作系统 第10章 Shell程序设计

  test命令
  功能测试表达式的真假值。
  
格式】test有两种等价的格式:
 
 test 表达式
  [ 表达式 ] (注意“[”和“]”两侧的空格)
  
参数表达式的常用形式见表9-8
  
退出状态测试结果为真时返回状态0;为假时返回状态1;出错时返回状态2
  
说明
  (1) 运算符两侧必须留有空格,与运算数分开。
  
(2) 表达式中若使用Shell的元字符,如><()等,必须用转义符“\”使其失去特殊意义,不被Shell解释执行。

  1. 字符串测试
  10.39  字符串测试:
  
$ user=smith
  $ test “$user” = smith  #两字符串相等?
  
$ echo $?  #显示测试结果
   
0  ()
  $ test -z “$user”  #字符串为空串?
  
$ echo $?
   1  ()

  10.40  含有空格的字符串及空字符串的测试:
  
$ user1=“Tom Smith”
  $ test “$user1” = “Tom Smith”  # test “Tom Smith” = “Tom Smith”
  $ echo $?
   0  ()
  $ test $user1 = “Tom Smith”  # test Tom Smith = “Tom Smith”
   bash: test: too many arguments
  $ echo $?
   2  (出错)
  $ test “$user2” = smith  # test “” = smith
  $ echo $?
   1  ()

  $ test $user2 = smith  # test  = smith
   bash: test: =: unary operator expected
  $ echo $?
   2  (出错)
  $
  当表达式中使用变量时,最好将其用双引号括起(如“$var”)。这样,Shell进行变量替换后的字符串被双引号括起,作为一个单一的字符串传递给test命令。上例中,变量user1的字符串中含有空格,“$user1”被替换为一个带有空格的字符串“Tom Smith”,而$user1替换后成为两个字符串TomSmith。变量user2为空,“$user2”被替换为一个空串“”,而$user2则被替换为空。所以,空串或带空格的串在没有双引号的情况下都会导致test报错。

  2. 数字测试表达式
  10.41  数字比较测试:
  
$ x1=5
  $ test “$x1” -eq 5; echo $?  # test “5” = 5
   0  ()
  $

  3. 文件测试表达式
  例10.42  文件测试:
  
$ test -f /home/cherry/message; echo $? #检查指定的文件是否存在
  
0  ()
  $ gcc myprog.c 2>err  #编译,错误信息记入err文件
  
$ test -s err && echo “Errors found”  #检查编译错误文件是否为空
  
$
  2test命令测试为真时,表示有编译错误发生,继续执行echo命令;否则不执行。

  10.43  目录测试:
  
$ cat dir_test
  test -d $1 && echo “$1 is a directory ” && exit 0   #测试参数1是否是目录
  
echo “ $1 is not a directory” ; exit 1
  $  dir_test proc
   proc is a directory
  $
  如果test -d $1命令测试为真,则顺序执行第1行的后两个命令,以0状态退出;如果测试为假,则执行第2行命令,以1状态退出。

  4. 逻辑测试表达式
  例10.44  带有逻辑运算符的表达式测试:
  
$ test -f file -a ! -s file  #测试file是否为空的普通文件
  $ echo $?
   1  ()
  $ read a b c
   (输入2 7 5)
  $ test \( $a -eq 0 -o $b -gt 5 \) -a $c -le 8  # ($a=0 or $b>5) and ($c<=8)
  $ echo $?
   0  ()

  $ cat check_input  #测试输入的数字是否有效
   
echo -n “Input a number(1-10): ”
   read a
   test $a -lt 1 -o $a -gt 10 && echo error! && exit 1
   echo ok
   exit 0
  $ bash check_input
   Input a number(1-10): 5
   ok
  $ bash check_input
   Input a number(1-10): 15
   error!
  $

       10.5  Shell控制结构
  与C语言的控制结构语句类似,Shell提供了几个专门的内部命令来构造控制结构,用它们可以构造出任意的分支与循环。这些命令(有时也称为语句)可以分为以下几类:
  ● 分支结构:
ifcase
  ● 循环结构:
whileuntilfor
  ● 循环控制:
breakcontinue
  ● 结束:
returnexit

10.5.1  条件与条件命令
  控制结构需要根据某个条件作出控制转向的决策。在C语言中,条件是某表达式的取值,0为假,非0为真。Shell语言中,条件是某命令的退出状态。当命令执行成功时,它返回一个0状态($?0),此时条件为真;若命令失败,返回一个非0状态($?不为0),则此时条件为假。
  
用于进行条件判断的命令就称为条件命令。任何shell命令都具有退出状态,因而都可以作为条件命令使用。此外,Shell还提供了3个内部命令::truefalse。它们不作任何操作,只是返回一个特定的退出状态。:true返回0false返回非0。它们可作为恒真和恒假条件命令使用。

10.5.2  分支控制命令
  分支控制命令用于控制程序在不同的条件取值下执行不同的流程。用于分支控制的命令有ifcaseif命令用于两路分支控制,case命令用于多路分支控制。
  1.  if命令
  if命令根据条件命令执行的结果决定后续命令的执行路径。if命令的一般形式为:
  if 条件命令
   
then 命令列表1  #$?0,执行此分支
   
[ else 命令列表2 ]  #否则,执行此分支
  
fi

  if命令的执行过程是:首先执行条件命令,然后根据条件命令的退出状态作为条件决定后续的执行路径。若条件为真则执行命令列表1,否则执行命令列表2
  条件命令通常是一个
test表达式命令,但也可以是任何其他的命令或命令列表。如果条件命令是一个命令列表,则Shell将依次执行其中的各个命令,并将最后一个命令的退出状态作为条件。命令列表可以是一个或多个命令,也可以是另一个if语句,从而形成嵌套if结构。另外,if结构可以没有else分支,但不能缺省then分支。

  10.45  判断参数1是否存在且是普通文件:
  if  [ -f  $1 ]
    then  echo “$1 is an ordinary file.”
    else  echo “$1 is not an ordinary file or not exists.”
  fi
  10.46  检查某用户是否登录,如果登录,则向他发即时信息:
  echo -n “ Enter User Name: ”
  read user
  if  who | grep $user
    then  echo “Don’t forget the meeting” | write $user
    else  echo “$user has not logged.”
  fi

  例10.46中的条件命令为who | grep命令,以grep命令的执行结果作为条件。若grepwho的输出中找到指定的用户名,则条件为真,表明该用户已登录;否则,条件为假,表明该用户未登录。当条件为真时,脚本利用write命令向用户发送即时消息(有关write命令的介绍见11.2.5小节)

  2.  case命令
  用case命令进行多路条件测试,结构更清晰。case命令的格式为:
  case 测试字符串 in
   模式1)  命令列表1;;
   模式2)  命令列表2;;
   ...
   模式n)  命令列表n;;
  esac
  case命令的执行过程是:先将测试字符串与各个模式字符串逐一比较,若发现了一个匹配的模式则执行该模式对应的命令列表。注意:若有多个匹配的模式时,只执行最前面的那一个分支。

  例10.47  根据参数个数进行相应处理:
  case $# in
   0)  DIR=“.” ;;
   1)  DIR=$1 ;;
   *)  echo “Usage:  $0 [directory name]”; exit 1 ;;
  esac
  rm -i $DIR/*.bak
  exit 0
  该脚本的功能是清除指定目录下的所有.bak文件。若没有指定参数则清除当前目录;若带有1个参数则清除该参数指定的目录;若参数个数多于1个则提示命令的用法。

  10.48  按时间显示问候语:
  hour= `date +%H`
  case $hour in
   08|09|10|11|12)  echo “Good Morning!” ;;
   13|14|15|16|17)  echo “Good Afternoon!” ;;
   18|19|20|21|22)  echo “Good Evening!” ;;
         *)  echo “Hello!” ;;
  esac
  该脚本先用date命令求出当前的小时数,然后根据这个数字按时间段显示不同的问候语。注意:模式中的“|”表示“或”的意思,用于将多个模式合并到同一个分支;“*”表示“任意”,表示当前面没有匹配的模式时执行此分支。

10.5.3  循环控制命令
  循环控制命令用于重复执行某个处理过程Shell提供了3种循环控制结构,即forwhileuntil结构。它们的结构控制含义与C语言中forwhiledo语句相同,只是形式上有所不同。另外,这些循环控制命令既可以用在脚本程序中作为循环控制语句,也可以以多行命令的方式在Shell下直接执行。

  1.  for命令
  for命令常用于简单的、确定的循环处理。for命令的格式为:
  
for变量 [ in 字符串列表 ]
  do
   命令列表
  
done
  for命令的执行过程是:定义一个变量,它依次取字符串列表中的各个字符串的值。对每次取值都执行命令列表,直到所有的字符串都处理完。当没有指定字符串列表时,默认指脚本的参数列表,即:for i 等同于for i in “[email protected]

  例10.49  循环处理一组文件:
  $ cat countlines
    sum=0
   for i in *.c  #对当前目录下的每个.c文件做
  
do
        lines=`wc -l $i`
     sum=`expr $sum + $lines`
     done
     echo $sum
  $ countlines
    463
  $
  这是统计c源文件行数的脚本,它逐个求出当前目录下每个.c文件的行数,并将它们累加起来输出。

  10.50  循环处理参数列表:
  $ cat lunch
    for i in “[email protected]”  #对参数行中的每个参数
   
do
      echo “Time for lunch, I treat !” | mail $i
   done
  $ lunch bill fred joe
  $
  这是一个邀请同事午餐的脚本。执行该脚本时需带若干个用户名作为参数,脚本用mail命令向每个参数指定的用户发送一个邀请邮件(关于mail命令的介绍见第11.3.3小节)

  2.  while命令
  while命令的作用是进行有条件的循环控制,当条件为真时执行循环体命令列表,直到条件为假时结束。while命令常用于循环次数或循环处理的对象不够明确的循环过程。
while命令的格式为:
  
while 条件命令
  
do
   命令列表
  
done

  例10.51  向所有登录用户发即时消息:
  $ cat inform
   echo -n “Enter message :”
   read MESSAGE #读入消息
   
set `who -q`     #找出已登录用户,存入位置变量中
   
while [ $1 != “#” ]   #对所有“#”前的位置变量
   
do
       echo $MESSAGE | write $1  #发送即时消息
      
shift 1
   done

  $ who -q  #输出已登录用户名与用户数
   
root  zhao  mary  cherry  cherry
  #  users=5
  $ ./inform
   Enter message : The network will go down at 1:00 pm.
  $
  该脚本先从标准输入接收消息,然后用who -q命令找出已登录的用户名,再用write命令向每个登录用户发送一个即时消息。该消息将出现在这些用户正使用的终端屏幕上。

  3.  until命令
  until命令的作用与while命令类似,只不过是在条件不成立时执行循环体命令,直到条件成立until命令常用于监视某事件的发生。
  
until命令的格式为:
    
until 条件命令
    
do
      命令列表
    
done

  例10.52  监视某用户是否登录:
  $ cat watchfor
    if [ ! $# .eq 1 ]  #参数个数检查
       
then echo “Usage: watchfor user” ; exit 1
    fi
    until who | grep $1   #搜索已登录的用户,直到被监视的用
              户登录
   
do
       sleep 60
   done
  $ watchfor joe
    joe  tty1  Dec 24 09:46
  $

  该脚本运行时需要带一个用户名参数。脚本首先检查参数个数,如果不是1,则提示命令的用法并退出。如果检查通过,则每隔60秒在已登录的用户中搜索一次参数指定的用户,直到其登录。

10.5.4  退出循环命令
  breakcontinue命令的作用与C语言中的breakcontinue语句相同,用于在必要时跳出循环。break用于终止整个循环。当执行break命令时,控制转移到循环体后(done之后)的命令执行。continue用于终止本轮循环,直接进入下一轮循环执行。当执行continue命令时,控制转移到循环体开始处(do之后)的命令执行。
  另外,
breakcontinue命令只能应用在forwhileuntil命令的循环体内。

  例10.53  break命令终止循环:
  $ cat break_test
   while  :  # always true
   do
        echo “Continue? [y/n]: ”
        read replay
        if [ $replay = n ]
          then break
        fi
   done
  $ break_test
   Continue? [y/n]: y
   Continue? [y/n]: y
   Continue? [y/n]: n
  $

  该脚本中的循环条件为恒真,这是个无限循环结构,但在循环体中使用了break,使其可以在适当的时候(输入“n”字符时)退出循环。

  例10.54  continue命令跳过某轮循环:
  $ cat continue_test
   cd $1
   for file in *  #对当前目录下的所有文件
    
do
       if [ -d $file ]
         then continue
       fi
    ls -l $file
    done
  $ continue_test /home/cherry
   -rwxr-xr-x  1  cherry  faculty  564  Jan 31  08:59  memo
   -rwxr-x--x  1  cherry  faculty   91  Jan 04  17:05  routine
  $

  该脚本运行时将逐个检查参数1指定目录中的各个文件,略过子目录文件,列出其他文件的详细列表。

10.5.5  退出命令
  exit命令是Shell的退出命令,用在Shell脚本中时表示退出脚本程序,返回到其父Shellexit命令的格式是:exit [退出码],其中退出码是返回给父进程的退出状态值。如果程序中没有显式地使用exit命令退出,则脚本的退出状态将是其退出前执行的最后一条命令的退出状态。

  例10.55  利用exit命令退出:
  if [ ! -d $1 ]
    then echo error && exit 1
    else ls $1
  fi
  exit 0
  该脚本接受一个目录名作为参数。当判断参数不是目录时报错退出,否则显示目录列表后正常退出。显式地给出退出码是一个好的编程习惯。

       10.6  Shell程序综合举例
  本节通过几个实用的例子展示Shell基本编程技术的综合应用。更复杂的应用需要涉及Shell高级编程的一些特性,读者可参阅专门的Shell编程教程。
  10.56  处理一批文件的程序procfile
  
$ cat procfile
   #!/bin/bash
   #usage: procfile files
   for i do
    while true do
     echo -n “$i: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: ”
     read choice

     case $choice in
      e*) vi $i;;
      v*) cat $i;;
      r*) rm $i;;
      n*) break;;
      q*) exit 0;;
       *) echo Illegal Option;;
       esac
    done
   done
  $ procfile file1 file2 file3
   file1: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: n
   file2: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: v

   …(显示file2的内容)
   file2: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: r
   file2: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: n
   file3: Edit, View, Remove, Next, Quit? [e|v|r|n|q]: q
  $
  1行以#!打头的注释行指示Shell应使用bash来执行此脚本。脚本执行时需要带若干个文件名作为运行参数。for循环用于依次处理参数中的各个文件。对每个文件的处理过程是一个while循环,它先列出可执行的操作的菜单,然后读取用户的输入,再通过case结构根据输入对文件进行指定的操作。当输入n时结束对这个文件的处理,进入下一个文件的处理过程。输入q时退出程序。

  10.57  一个求数字累加和的程序
  
$ cat addall
   #!/bin/bash
   if [ $# = 0 ] then
    echo “Usage: $0 number-list”
    exit 1
   fi
   sum=0  # sum of numbers
   count=$#  # count of numbers
   while [ $# != 0 ]
   do
   sum=`expr $sum + $1`
   shift
   done

   # display final sum
   echo “The sum of the given $count numbers is $sum.”
   exit 0
  $ addall
   Usage: addall number-list
  $ addall 1 4 9 16 25 36 49
   The sum of the given 7 numbers is 140.
  $
  addall脚本以一系列整数为参数,对它们求和。脚本首先检查参数个数,如果没有参数则提示命令的用法并退出。 脚本通过while循环将参数表中的参数一个个累加到sum变量中,然后显示累加结果。

  例10.58  一个脚本命令groups,用于求用户所在的用户组的名称:
  $ cat /usr/bin/groups
   #!/bin/sh
   usage=“Usage:
    $0 --help  display this help and exit
    $0 --version  output version information and exit
    $0 [user]…  print the groups a user is in”
   fail=0
   case $# in
       1) case “$1” in
         --help )  echo “$usage” || fail=1; exit $fail;;
         --version ) echo “groups (GNU coreutils) 4.5.3” || fail=1; exit $fail;;
     * ) ;;
        esac;;
     * ) ;;

  esac

  if [ $# -eq 0 ]; then
     id -Gn   #求当前用户的组名
    
fail=$?
  else
     for name in “[email protected]”; do
        groups=`id -Gn -- $name`   #$name用户的组名
        
status=$?
        if test $status = 0; then
          echo $name : $groups
      else
          fail=$status
       fi
    done

   fi
   exit $fail
  $ groups --help
   Usage:
    groups --help  display this help and exit
    groups --version  output version information and exit
    groups [user]…  print the groups a user is in
  $ groups cherry zhao
    cherry: faculty
    zhao: guest
  $
  注意该脚本对于参数的判别。它允许带一个选项或0至多个用户名参数。对于--help选项,它显示帮助信息;对于--version选项,它显示版本信息;若是用户名参数,则调用id命令求出组名;没有参数和选项时它求出当前用户的用户组名。

  例10.59  一个简单的文本文件打包程序:
  
$ cat bundle  #打包程序
   
#bundle: group files into distribution package
   echo “# To unbundle, bash this file” 
   for i do
    echo “cat > $i << End of $i”
    cat $i
    echo “End of $i”
   done
  $ bundle file1 file2 file3 > bundlefile  #打包3个文件
  
$ cat bundlefile  #打好的包文件
   
# To unbundle, bash this file

   cat > file1 << End of file1
   
   …(file1的内容)
   
   End of file1
   cat > file2 << End of file2
   
   …(file2的内容)
   
   End of file2
   cat > file3 << End of file3
   
   …(file3的内容)
   
   End of file3

  $ cp bundlefile newdir/
  $ cd newdir
  $ bash bundlefile  #在另一个目录中解包
  
$ ls
   bundlefile    file1    file2    file3
  $
  bundle脚本的功能是将参数指定的多个文本文件打包成一个文件以便发行。打包后的包文件具有自解包的功能。打包的方法是:将每个文件的内容前加上一行cat命令,尾部加一行结束标记,将多个文件存放在一个文件中。解包时,依次执行包文件中的cat命令,利用here文档机制将文件的内容复原出来。

           习    题
  9-1  什么是Shell脚本?写出执行一个Shell脚本的3种方法。
  
9-2  什么是Shell变量?如何定义Shell变量?如何引用Shell变量?
  
9-3  按照用途,Shell变量分为哪几类?各类的用途是什么?
  
9-4  哪个环境变量是用于保存命令的搜索路径的?如何修改搜索路径,使其包含“~”目录和“.”目录?这样的修改有什么作用?
  
9-5  根据引号的作用写出下列命令的输出:
  
echo who | wc -l

  echo `who | wc -l`
  echo ‘who | wc -l’
  echo “who | wc -l”
  9-6  根据引号的作用写出下列命令的输出:
  
echo ‘My logname is $LOGNAME’
  echo “My logname is $LOGNAME”
  9-7  如何用echo命令将字符串 /home/$user/* 显示出来?
  
9-8  解释以下命令有何不同:
  
find . -name ‘[A-H]’* -print  find . -name ‘[A,H]’* -print

  9-9  解释以下命令有何不同:
  
wc wc  wc < wc  wc > wc  wc | wc
  9-10  以下命令可完成什么任务?
  cat > letter 2> save < memo
  cat > letter < memo 2>&1
  cat 2> save < memo | sort > letter
  9-11  解释以下脚本的功能:
  
mail root << !!!
  ****** PROBLEM AGAIN ******

  The midnight error has struck again!
  All processing stopped.
  !!!
  9-12  编写一个Shell程序,它以立方体的边长作为参数,显示立方体的体积。
  
9-13  编写一个Shell程序,它删除当前目录下(包括各级子目录)所有长度为0的文件。
  
9-14  编写一个Shell程序,它将第2个参数及其后的各个参数指定的文件复制到第1个参数指定的目录中。要求对输入的参数做必要的检查。

  9-15  编写一个Shell程序,它能够将指定目录及其子目录中的包含字符串root的文本文件找出来。
  
9-16  编写一个Shell程序,它以一个英文月份名作为参数,显示当年该月的日历。
  
9-17  修改例10.51中的inform脚本,使其只向不包括自己的所有登录用户发送即时消息。
  
9-18  根据例10.57中的addall脚本编写一个脚本,它带有若干个文件名作为运行参数,脚本的功能是统计这些文件的大小之和。