Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1093407
  • 博文数量: 242
  • 博客积分: 10209
  • 博客等级: 上将
  • 技术积分: 3028
  • 用 户 组: 普通用户
  • 注册时间: 2008-03-12 09:27
文章分类

全部博文(242)

文章存档

2014年(1)

2013年(1)

2010年(51)

2009年(65)

2008年(124)

我的朋友

分类:

2008-04-10 16:49:50

高级Bash脚本编程指南(四)
 
 
第13章 系统与管理命令
======================
在/etc/rc.d目录中的启动和关机脚本中包含了好多有用的(和没用的)这些系统管理命令. 这些
命令通常总是被root用户使用, 用与系统维护或者是紧急文件系统修复.一定要小心使用这些工
具, 因为如果滥用的话, 它们会损坏你的系统.
Users 和 Groups 类命令
users
 显示所有的登录的用户. 这个命令与 who -q 基本一致.
groups
 列出当前用户和他所属于的组. 这相当于 $GROUPS 内部变量, 但是这个命令将会给出组名
 字, 而不是数字.
  bash$ groups
  bozita cdrom cdwriter audio xgrp
 
  bash$ echo $GROUPS
  501
chown, chgrp
 chown 命令将会修改一个或多个文件的所有权. 对于root来说这是一种非常好的将文件的
 所有权从一个用户换到另一个用户的方法. 一个普通用户不能修改文件的所有权, 即使他
 是文件的宿主也不行. [1]
  root# chown bozo *.txt
 chgrp 将会修改一个或个文件党组所有权. 你必须是这些文件的宿主, 并且是目的组的成
 员(或者root), 这样才能使用这个操作.
    1 chgrp --recursive dunderheads *.data
    2 #  "dunderheads"(译者: 晕,蠢才...) 组现在拥有了所有的"*.data"文件.
    3 #+ 包括所有$PWD目录下的子目录中的文件(--recursive的作用就是包含子目录).
useradd, userdel
 useradd 管理命令将会在系统上添加一个用户帐号, 并且如果指定的话, 还会为特定的用
 户创建home目录. 相应的userdel 命令将会从系统上删除一个用户帐号, [2] 并且删除相
 应的文件.
 注意: adduser命令与useradd是相同的, adduser通常都是一个符号链接.
usermod
 修改用户帐号. 可以修改密码, 组身份, 截止日期, 或者给定用户帐号的其他的属性. 使
 用这个命令, 用户的密码可能会被锁定, 因为密码会影响到帐号的有效性.
groupmod
 修改指定组. 组名字或者ID号都可以使用这个命令来修改.
id
 id 将会列出当前进程的真实和有效用户ID, 还有用户的组ID. 这与Bash的内部变量
 $UID, $EUID, 和 $GROUPS 很相像.
  bash$ id
  uid=501(bozo) gid=501(bozo) groups=501(bozo),22(cdrom),80(cdwriter),81(audio)
 
  bash$ echo $UID
  501
 注意: id 命令只有在有效ID与真实ID不符时才会显示有效id.
 参见 Example 9-5.
who
 显示系统上所有已经登录的用户.
  bash$ who
  bozo  tty1     Apr 27 17:45
  bozo  pts/0    Apr 27 17:46
  bozo  pts/1    Apr 27 17:47
  bozo  pts/2    Apr 27 17:49
 -m 选项将会只给出当前用户的详细信息. 将任意两个参数传递到who中 都等价于who -m,
 就像 who am i 或者 who The Man.
  bash$ who -m
  localhost.localdomain!bozo  pts/2    Apr 27 17:49
 whoami 与who -m 很相似, 但是只列出用户名.
  bash$ whoami
  bozo
w
 显示所有的登录的用户和属于它们的进程. 这是一个who的扩展版本. w的输出可以通过管
 道传递到grep中, 这样就可以查找指定的用户或进程.
  bash$ w | grep startx
  bozo  tty1     -                 4:22pm  6:41   4.47s  0.45s  startx
logname
 显示当前用户的登录名(可以在/var/run/utmp中找到). 这与上边的whoami很相近.
  bash$ logname
  bozo
 
  bash$ whoami
  bozo
 然而...
  bash$ su
  Password: ......
 
  bash# whoami
  root
  bash# logname
  bozo
 注意: logname只会打印出登录的用户名, 而whoami 将会给出附着到当前进程的用户名.
  就像我们上边看到的那样, 这两个名字有时会不同.
su
 使用一个代替的用户来运行一个程序或脚本. su rjones 将会以 rjones 来启动一个
 shell. 一个不加参数的su默认就是root. 参见 Example A-15.
sudo
 以root(或其他用户)的身份来运行一个命令. 这个命令可以运行在脚本中, 这样就允许以
 正规的用户身份来运行脚本.
    1 #!/bin/bash
    2
    3 # 一些命令.
    4 sudo cp /root/secretfile /home/bozo/secret
    5 # 一些命令.
 文件 /etc/sudoers 持有允许调用sudo的用户名.
passwd
 设置, 修改, 或者管理用户的密码.
 passwd 命令可以用在脚本中, 但可能你不想这么用.
Example 13-1 设置一个新密码
################################Start Script#######################################
 1 #!/bin/bash
 2 #  setnew-password.sh: 只用于说明目的.
 3 #                      如果真正运行这个脚本并不是一个好主意.
 4 #  这个脚本必须以root身份运行.
 5
 6 ROOT_UID=0         # Root 的 $UID 0.
 7 E_WRONG_USER=65    # 不是 root?
 8
 9 E_NOSUCHUSER=70
10 SUCCESS=0
11
12
13 if [ "$UID" -ne "$ROOT_UID" ]
14 then
15   echo; echo "Only root can run this script."; echo
16   exit $E_WRONG_USER
17 else
18   echo
19   echo "You should know better than to run this script, root."
20   echo "Even root users get the blues... "
21   echo
22 fi 
23
24
25 username=bozo
26 NEWPASSWORD=security_violation
27
28 # 检查bozo是否在这里.
29 grep -q "$username" /etc/passwd
30 if [ $? -ne $SUCCESS ]
31 then
32   echo "User $username does not exist."
33   echo "No password changed."
34   exit $E_NOSUCHUSER
35 fi 
36
37 echo "$NEWPASSWORD" | passwd --stdin "$username"
38 #  'passwd'命令 '--stdin' 选项允许
39 #+ 从stdin(或者管道)中获得一个新的密码.
40
41 echo; echo "User $username's password changed!"
42
43 # 在脚本中使用'passwd'命令是很危险的.
44
45 exit 0
################################End Script#########################################
 passwd 命令的 -l, -u, 和 -d 选项允许锁定, 解锁,和删除一个用户的密码. 只有root
 用户可以使用这些选项.
ac
 显示用户登录的连接时间, 就像从 /var/log/wtmp 中读取一样. 这是GNU的一个统计工具.
  bash$ ac
    total       68.08
last
 用户最后登录的信息, 就像从/var/log/wtmp中读出来一样. 这个命令也可以用来显示远
 端登录.
 比如, 显示最后几次系统的重启信息:
  bash$ last reboot
  reboot   system boot  2.6.9-1.667      Fri Feb  4 18:18          (00:02)   
  reboot   system boot  2.6.9-1.667      Fri Feb  4 15:20          (01:27)   
  reboot   system boot  2.6.9-1.667      Fri Feb  4 12:56          (00:49)   
  reboot   system boot  2.6.9-1.667      Thu Feb  3 21:08          (02:17)   
  . . .
  wtmp begins Tue Feb  1 12:50:09 2005
newgrp
 不用登出就可以修改用户的组ID. 并且允许存取新组的文件. 因为用户可能同时属于多个
 组, 这个命令很少被使用.

终端类命令
tty
 显示当前用户终端的名字. 注意每一个单独的xterm窗口都被算作一个不同的终端.
  bash$ tty
  /dev/pts/1
stty
 显示并(或)修改终端设置. 这个复杂命令可以用在脚本中, 并可以用来控制终端的行为和
 其显示输出的方法. 参见这个命令的info页, 并仔细学习它.
Example 13-2 设置一个擦除字符
################################Start Script#######################################
 1 #!/bin/bash
 2 # erase.sh: 在读取输入时使用"stty"来设置一个擦除字符.
 3
 4 echo -n "What is your name? "
 5 read name                      #  试试退格键
 6                                #+ 来删除输入的字符.
 7                                #  有什么问题?
 8 echo "Your name is $name."
 9
10 stty erase '#'                 #  将 "hashmark" (#) 设置为退格字符.
11 echo -n "What is your name? "
12 read name                      #  使用#来删除最后键入的字符.
13 echo "Your name is $name."
14
15 # 警告: 即使在脚本退出后, 新的键值还是保持设置.(译者: 使用stty erase '^?' 恢复)
16
17 exit 0
################################End Script#########################################
Example 13-3 关掉终端对于密码的echo
################################Start Script#######################################
 1 #!/bin/bash
 2 # secret-pw.sh: 保护密码不被显示
 3
 4 echo
 5 echo -n "Enter password "
 6 read passwd
 7 echo "password is $passwd"
 8 echo -n "If someone had been looking over your shoulder, "
 9 echo "your password would have been compromised."
10
11 echo && echo  # 在一个"与列表"中产生2个换行.
12
13
14 stty -echo    # 关闭屏幕的echo.
15
16 echo -n "Enter password again "
17 read passwd
18 echo
19 echo "password is $passwd"
20 echo
21
22 stty echo     # 恢复屏幕的echo.
23
24 exit 0
25
26 # 详细的阅读stty命令的info页, 以便于更好的掌握这个有用并且狡猾的工具.
################################End Script#########################################
 一个具有创造性的stty命令的用法, 检测用户所按的键(不用敲回车).
Example 13-4 按键检测
################################Start Script#######################################
 1 #!/bin/bash
 2 # keypress.sh: 检测用户按键 ("hot keys").
 3
 4 echo
 5
 6 old_tty_settings=$(stty -g)   # 保存老的设置(为什么?).
 7 stty -icanon
 8 Keypress=$(head -c1)          # 或者 $(dd bs=1 count=1 2> /dev/null)
 9                               # 在非GNU的系统上
10
11 echo
12 echo "Key pressed was \""$Keypress"\"."
13 echo
14
15 stty "$old_tty_settings"      # 恢复老的设置.
16
17 # 感谢, Stephane Chazelas.
18
19 exit 0
################################End Script#########################################
 参见 Example 9-3.
 注意: 终端与模式terminals and modes
  一般情况下, 一个终端都是工作在canonical(标准)模式下. 当用户按键后, 事实上所
  产生的字符并没有马上传递到运行在当前终端上的程序. 终端上的一个本地的缓存保
  存了这些按键. 当用按下ENTER键的时候, 才会将所有保存的按键信息传递到运行的程
  序中. 这就意味着在终端内部存在一个基本的行编辑器. 

   bash$ stty -a
   speed 9600 baud; rows 36; columns 96; line = 0;
   intr = ^C; quit = ^\; erase = ^H; kill = ^U; eof = ^D; eol = ; eol2 = ;
   start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O;
   ...
   isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
  在使用canonical模式的时候, 可以对本地终端行编辑器所定义的特殊按键进行重新定
  义.
   bash$ cat > filexxx
   whaIfoo barhello world
  
   bash$ cat filexxx
   hello world  
   bash$ wc -c < filexxx
   12  
  控制终端的进程只保存了12个字符(11个字母加上一个换行), 虽然用户敲了26个按键.
  在 non-canonical ("raw") 模式, 每次按键(包括特殊定义的按键, 比如 ctl-H)将会
  立即发送一个字符到控制进程.
  Bash提示符禁用了icanon和echo, 因为它用自己的更好的行编辑器代替了终端的基本
  行编辑器. 比如, 当你在Bash提示符下敲ctl-A的时候, 终端将不会显示 ^A, 但是
  Bash将会获得\1字符, 然后解释这个字符, 这样光标就移动到行首了.
  Stéphane Chazelas
setterm
 设置特定的终端属性. 这个命令将向它的终端的stdout写一个字符串, 这个字符串将修改
 终端的行为.
  bash$ setterm -cursor off
  bash$
 setterm 命令可以被用在脚本中来修改写到stdout的文本的外观, 虽然如果你仅仅只想完
 成这个目的, 还有特定的更好的工具可以用.

    1 setterm -bold on
    2 echo bold hello
    3
    4 setterm -bold off
    5 echo normal hello
tset
 显示或初始化终端设置. 可以说这是stty的功能比较弱的版本.
 
  bash$ tset -r
  Terminal type is xterm-xfree86.
  Kill is control-U (^U).
  Interrupt is control-C (^C).
 
setserial
 设置或者显示串口参数. 这个脚本只能被root用户来运行, 并且通常都在系统安装脚本
 中使用.
    1 # 来自于 /etc/pcmcia/serial 脚本:
    2
    3 IRQ=`setserial /dev/$DEVICE | sed -e 's/.*IRQ: //'`
    4 setserial /dev/$DEVICE irq 0 ; setserial /dev/$DEVICE irq $IRQ
getty, agetty
 一个终端的初始化过程通常都是使用getty或agetty来建立, 这样才能让用户登录. 这些
 命令并不用在用户的shell脚本中. 它们的行为与stty很相似.
mesg
 使能或禁用当前用户终端的存取权限. 禁用存取权限将会阻止网络上的另一用户向这个终
 端写消息.
 注意: 当你正在编写文本文件的时候, 在文本中间突然来了一个莫名其妙的消息, 这对你
  来说是非常烦人的. 在多用户的网络环境下, 当你不想被打断的时候, 你可能因此希
  望禁用对你终端的写权限.
wall
 这是一个缩写单词 "write all", 也就是, 向登录到网络上的任何终端的所有用户都发送
 一个消息. 最早这是一个管理员的工具, 很有用, 比如, 当系统有问题的时候, 管理可以
 警告系统上的所有人暂时离开 (参见 Example 17-1).
  bash$ wall System going down for maintenance in 5 minutes!
  Broadcast message from bozo (pts/1) Sun Jul  8 13:53:27 2001...
  System going down for maintenance in 5 minutes!
 注意: 如果某个特定终端使用mesg来禁止了写权限, 那么wall将不会给它发消息.

信息与统计类
uname
 输出系统的说明(OS, 内核版本, 等等.)到stdout. 使用 -a 选项, 将会给出详细的信息
 (参见 Example 12-5). 使用-s选项只会输出OS类型.
  bash$ uname -a
  Linux localhost.localdomain 2.2.15-2.5.0 #1 Sat Feb 5 00:13:43 EST 2000 i686 unknown
 
  bash$ uname -s
  Linux
arch
 显示系统的硬件体系结构. 等价于 uname -m. 参见 Example 10-26.
  bash$ arch
  i686
 
  bash$ uname -m
  i686
lastcomm
 给出前一个命令的信息, 存储在/var/account/pacct文件中. 命令名字与用户名字都可以
 使用选项来指定. 这是GNU的一个统计工具.
lastlog
 列出系统上所有用户最后登录的时间. 存在/var/log/lastlog文件中.
  bash$ lastlog
  root          tty1                      Fri Dec  7 18:43:21 -0700 2001
  bin                                     **Never logged in**
  daemon                                  **Never logged in**
  ...
  bozo          tty1                      Sat Dec  8 21:14:29 -0700 2001
 
 
 
  bash$ lastlog | grep root
  root          tty1                      Fri Dec  7 18:43:21 -0700 2001
 注意: 如果用户对于/var/log/lastlog文件没有读权限的话, 那么调用这个命令就会失败.
lsof
 列出打开的文件. 这个命令将会把所有当前打开的文件列出一份详细的表格, 包括文件的
 所有者信息, 尺寸, 与它们相关的信息等等. 当然, lsof也可以管道输出到 grep 和(或)
 awk来分析它的结果.
  bash$ lsof
  COMMAND    PID    USER   FD   TYPE     DEVICE    SIZE     NODE NAME
  init         1    root  mem    REG        3,5   30748    30303 /sbin/init
  init         1    root  mem    REG        3,5   73120     8069 /lib/ld-2.1.3.so
  init         1    root  mem    REG        3,5  931668     8075 /lib/libc-2.1.3.so
  cardmgr    213    root  mem    REG        3,5   36956    30357 /sbin/cardmgr
  ...
strace
 为了跟踪系统和信号的诊断和调试工具. 调用它最简单的方法就是strace COMMAND.
  bash$ strace df
  execve("/bin/df", ["df"], [/* 45 vars */]) = 0
  uname({sys="Linux", node="bozo.localdomain", ...}) = 0
  brk(0)                                  = 0x804f5e4
  ...
 这是 Solaris truss命令的Linux的等价工具.
nmap
 网络端口扫描器. 这个命令将会扫描一个服务器来定位打开的端口, 并且定位这些端口相
 关的服务. 这是一个防止网络被黑客入侵的一个重要的安全工具.

    1 #!/bin/bash
    2
    3 SERVER=$HOST                           # localhost.localdomain (127.0.0.1).
    4 PORT_NUMBER=25                         # SMTP 端口.
    5
    6 nmap $SERVER | grep -w "$PORT_NUMBER"  # 这个指定端口打开了么?
    7 #              grep -w 匹配整个单词,
    8 #+             这样就不会匹配类似于1025这种含有25的端口了.
    9
   10 exit 0
   11
   12 # 25/tcp     open        smtp
nc
 nc(netcat)工具是一个完整的工具包, 可以使用它来连接和监听TCP和UDP端口. 它可以用
 来作为诊断和测试工具, 也可以用来作为基于脚本的HTTP客户端和服务器的组件.
  bash$ nc localhost.localdomain 25
  220 localhost.localdomain ESMTP Sendmail 8.13.1/8.13.1; Thu, 31 Mar 2005 15:41:35 -0700
Example 13-5 Checking a remote server for identd
################################Start Script#######################################
 1 #! /bin/sh
 2 ## Duplicate DaveG's ident-scan thingie using netcat. Oooh, he'll be p*ssed.
 3 ## Args: target port [port port port ...]
 4 ## Hose stdout _and_ stderr together.
 5 ##
 6 ##  优点: runs slower than ident-scan, giving remote inetd less cause
 7 ##+ for alarm, and only hits the few known daemon ports you specify.
 8 ##  缺点: requires numeric-only port args, the output sleazitude,
 9 ##+ and won't work for r-services when coming from high source ports.
10 # 脚本作者: Hobbit <>
11 # 授权使用在本书中.
12
13 # ---------------------------------------------------
14 E_BADARGS=65       # 至少需要两个参数.
15 TWO_WINKS=2        # 需要睡多长时间.
16 THREE_WINKS=3
17 IDPORT=113         # Authentication "tap ident" port.
18 RAND1=999
19 RAND2=31337
20 TIMEOUT0=9
21 TIMEOUT1=8
22 TIMEOUT2=4
23 # ---------------------------------------------------
24
25 case "${2}" in
26   "" ) echo "Need HOST and at least one PORT." ; exit $E_BADARGS ;;
27 esac
28
29 # Ping 'em once and see if they *are* running identd.
30 nc -z -w $TIMEOUT0 "$1" $IDPORT || { echo "Oops, $1 isn't running identd." ; exit 0 ; }
31 #  -z scans for listening daemons.
32 #     -w $TIMEOUT = How long to try to connect.
33
34 # Generate a randomish base port.
35 RP=`expr $$ % $RAND1 + $RAND2`
36
37 TRG="$1"
38 shift
39
40 while test "$1" ; do
41   nc -v -w $TIMEOUT1 -p ${RP} "$TRG" ${1} < /dev/null > /dev/null &
42   PROC=$!
43   sleep $THREE_WINKS
44   echo "${1},${RP}" | nc -w $TIMEOUT2 -r "$TRG" $IDPORT 2>&1
45   sleep $TWO_WINKS
46
47 # 这个脚本看起来是不是一个瘸腿脚本, 或者其它更差的什么东西?
48 # ABS Guide 作者注释: "并不是真的那么差,
49 #+                            事实上相当清楚."
50
51   kill -HUP $PROC
52   RP=`expr ${RP} + 1`
53   shift
54 done
55
56 exit $?
57
58 #  注意事项:
59 #  ---------
60
61 #  尝试注释一下第30行的程序, 并且使用"localhost.localdomain 25"
62 #+ 作为参数来运行这个脚本.
63
64 #  For more of Hobbit's 'nc' example scripts,
65 #+ look in the documentation:
66 #+ the /usr/share/doc/nc-X.XX/scripts directory.
################################End Script#########################################
 并且, 当然, 这里还有Dr. Andrew Tridgell在BistKeeper事件中臭名卓著的一行脚本:
    1 echo clone | nc thunk.org 5000 > e2fsprogs.dat
free
 使用表格形式来显示内存和缓存的使用情况. 这个命令的输出非常适合于使用 grep, awk
 或者Perl来分析. procinfo命令将会显示free命令所能显示的所有信息, 而且更多.
  bash$ free
      total       used       free     shared    buffers     cached
    Mem:         30504      28624       1880      15820       1608       16376
    -/+ buffers/cache:      10640      19864
    Swap:        68540       3128      65412
 显示未使用的RAM内存:
  bash$ free | grep Mem | awk '{ print $4 }'
  1880
procinfo
 从/proc pseudo-filesystem中提取和显示所有信息和统计资料. 这个命令将给出更详细
 的信息.
  bash$ procinfo | grep Bootup
  Bootup: Wed Mar 21 15:15:50 2001    Load average: 0.04 0.21 0.34 3/47 6829
lsdev
 显示设备, 也就是显示安装的硬件.
  bash$ lsdev
  Device            DMA   IRQ  I/O Ports
  ------------------------------------------------
  cascade             4     2
  dma                          0080-008f
  dma1                         0000-001f
  dma2                         00c0-00df
  fpu                          00f0-00ff
  ide0                     14  01f0-01f7 03f6-03f6
  ...
du
 递归的显示(磁盘)文件的使用状况. 除非指定, 默认是当前工作目录.
  bash$ du -ach
  1.0k    ./wi.sh
  1.0k    ./tst.sh
  1.0k    ./random.file
  6.0k    .
  6.0k    total
df
 使用列表的形式显示文件系统的使用状况.
  bash$ df
  Filesystem           1k-blocks      Used Available Use% Mounted on
  /dev/hda5               273262     92607    166547  36% /
  /dev/hda8               222525    123951     87085  59% /home
  /dev/hda7              1408796   1075744    261488  80% /usr
dmesg
 将所有的系统启动消息输出到stdout上. 方便出错,并且可以查出安装了哪些设备驱动和
 察看使用了哪些系统中断. dmesg命令的输出当然也可以在脚本中使用 grep, sed, 或
 awk 来进行分析.
  bash$ dmesg | grep hda
  Kernel command line: ro root=/dev/hda2
  hda: IBM-DLGA-23080, ATA DISK drive
  hda: 6015744 sectors (3080 MB) w/96KiB Cache, CHS=746/128/63
  hda: hda1 hda2 hda3 < hda5 hda6 hda7 > hda4
stat
 显示一个或多个给定文件(也可以是目录文件或设备文件)的详细的统计信息.
  bash$ stat test.cru
    File: "test.cru"
    Size: 49970        Allocated Blocks: 100          Filetype: Regular File
    Mode: (0664/-rw-rw-r--)         Uid: (  501/ bozo)  Gid: (  501/ bozo)
  Device:  3,8   Inode: 18185     Links: 1   
  Access: Sat Jun  2 16:40:24 2001
  Modify: Sat Jun  2 16:40:24 2001
  Change: Sat Jun  2 16:40:24 2001
 如果目标文件不存在, stat 将会返回一个错误信息.
  bash$ stat nonexistent-file
  nonexistent-file: No such file or directory
vmstat
 显示虚拟内存的统计信息.
  bash$ vmstat
  procs                      memory    swap          io system         cpu
  r  b  w   swpd   free   buff  cache  si  so    bi    bo   in    cs  us  sy id
  0  0  0      0  11040   2636  38952   0   0    33     7  271    88   8   3 89
netstat
 显示当前网络的统计和信息, 比如路由表和激活的连接. 这个工具存取/proc/net(第27章)
 中的信息. 参见 Example 27-3.
 netstat -r 等价于 route 命令.
  bash$ netstat
  Active Internet connections (w/o servers)
  Proto Recv-Q Send-Q Local Address           Foreign Address         State     
  Active UNIX domain sockets (w/o servers)
  Proto RefCnt Flags       Type       State         I-Node Path
  unix  11     [ ]         DGRAM                    906    /dev/log
  unix  3      [ ]         STREAM     CONNECTED     4514   /tmp/.X11-unix/X0
  unix  3      [ ]         STREAM     CONNECTED     4513
  . . .
uptime
 显示系统运行的时间, 还有其他一些统计信息.
  bash$ uptime
  10:28pm  up  1:57,  3 users,  load average: 0.17, 0.34, 0.27
 注意: load average 如果小于或等于1, 那么就意味着系统会马上处理. 如果
  load average大于1, 那么就意味着进程需要排队. 如果load average大于3,
  那么就意味着, 系统性能已经显著下降了.
hostname
 显示系统的主机名字. 这个命令在 /etc/rc.d 安装脚本(/etc/rc.d/rc.sysinit
 或类似的)中设置主机名. 等价于uname -n, 并且与$HOSTNAME内部变量很相像.
  bash$ hostname
  localhost.localdomain
 
  bash$ echo $HOSTNAME
  localhost.localdomain
 与 hostname 命令很相像的命令还有 domainname, dnsdomainname, nisdomainname, 和
 ypdomainname 命令. 使用这些来显示或设置系统DNS 或者 NIS/YP 域名. 对于hostname
 命令来说使用不同的选项一样可以达到上边这些命令的目的.
hostid
 显示主机的32位的16进制ID.
  bash$ hostid
  7f0100
 注意: 这个命令据说对于特定系统可以获得一个"唯一"的序号. 某些产品的注册过程可能
  会需要这个序号来作为用户的许可证. 不幸的是, hostid 只会使用字节转换的方法
  来用16进制显示机器的网络地址.
  一个没有网络的Linux机器的典型的网络地址设置在/ect/hosts中.
   bash$ cat /etc/hosts
   127.0.0.1               localhost.localdomain localhost
  碰巧, 通过对127.0.0.1进行字节转换, 我们获得了 0.127.1.0, 用16进制表示就是
  007f0100, 这就是上边hostid返回的结果. 这样几乎所有的无网络的Linux机器都会
  得到这个hostid.
sar
 sar (System Activity Reporter系统活动报告) 命令将会给出系统统计的一个非常详细的
 概要. Santa Cruz Operation("老" SCO)公司在1999年4月份以开源软件的形式发布了sar.
 这个命令并不是基本Linux发行版的一部分, 但是你可以从Sebastien Godard 写的
 sysstat utilities 包中获得这个工具.
  bash$ sar
  Linux 2.4.9 (brooks.seringas.fr)  09/26/03
 10:30:00          CPU     %user     %nice   %system   %iowait     %idle
 10:40:00          all      2.21     10.90     65.48      0.00     21.41
 10:50:00          all      3.36      0.00     72.36      0.00     24.28
 11:00:00          all      1.12      0.00     80.77      0.00     18.11
 Average:          all      2.23      3.63     72.87      0.00     21.27
 14:32:30          LINUX RESTART
 15:00:00          CPU     %user     %nice   %system   %iowait     %idle
 15:10:00          all      8.59      2.40     17.47      0.00     71.54
 15:20:00          all      4.07      1.00     11.95      0.00     82.98
 15:30:00          all      0.79      2.94      7.56      0.00     88.71
 Average:          all      6.33      1.70     14.71      0.00     77.26
readelf
 显示指定的 elf 格式的2进制文件的统计信息. 这个工具是binutils工具包的一部分.
  bash$ readelf -h /bin/bash
  ELF Header:
    Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    Class:                             ELF32
    Data:                              2's complement, little endian
    Version:                           1 (current)
    OS/ABI:                            UNIX - System V
    ABI Version:                       0
    Type:                              EXEC (Executable file)
    . . .
size
 size [/path/to/binary] 命令可以显示2进制可执行文件或归档文件每部分的尺寸. 这个
 工具主要是程序员使用.
  bash$ size /bin/bash
  text    data     bss     dec     hex filename
   495971   22496   17392  535859   82d33 /bin/bash
系统日志类
logger
 附加一个用户产生的消息到系统日之中 (/var/log/messages). 不是root用户也可以调用
 logger.
    1 logger Experiencing instability in network connection at 23:10, 05/21.
    2 # 现在, 运行 'tail /var/log/messages'.
 通过在脚本中调用一个logger命令, 就可以将调试信息写到/var/log/messages中.
    1 logger -t $0 -i Logging at line "$LINENO".
    2 # "-t" 选项可以为长的入口指定标签.
    3 # "-i" 选项记录进程ID.
    4
    5 # tail /var/log/message
    6 # ...
    7 # Jul  7 20:48:58 localhost ./test.sh[1712]: Logging at line 3.
logrotate
 这个工具用来管理系统的log文件, 可以在合适的时候轮换, 压缩, 删除, 和(或)e-mail
 它们. 这个工具将从老的log文件中取得一些杂乱的记录保存在/var/log中. 通常使用
 cron 来每天运行logrotate.
 在/etc/logrotate.conf中添加合适的入口就可以管理自己的log文件了, 就像管理系统
 log文件一样.
 注意: Stefano Falsetto 创造了rottlog, 他认为这是logrotate的改进版本.
作业控制
ps
 进程统计: 通过进程所有者和PID(进程ID)来列出当前执行的进程. 通常都是使用ax选项
 来调用这个命令, 并且结果可以通过管道传递到 grep 或 sed 中来搜索特定的进程
 (参见 Example 11-12 和 Example 27-2).
  bash$  ps ax | grep sendmail
  295 ?    S   0:00 sendmail: accepting connections on port 25
 如果想使用"树"的形式来显示系统进程: ps afjx 或者 ps ax --forest.
pgrep, pkill
 ps 命令与grep或kill结合使用.
  bash$ ps a | grep mingetty
  2212 tty2     Ss+    0:00 /sbin/mingetty tty2
  2213 tty3     Ss+    0:00 /sbin/mingetty tty3
  2214 tty4     Ss+    0:00 /sbin/mingetty tty4
  2215 tty5     Ss+    0:00 /sbin/mingetty tty5
  2216 tty6     Ss+    0:00 /sbin/mingetty tty6
  4849 pts/2    S+     0:00 grep mingetty

  bash$ pgrep mingetty
  2212 mingetty
  2213 mingetty
  2214 mingetty
  2215 mingetty
  2216 mingetty
pstree
 使用"树"形式列出当前执行的进程. -p选项显示PID,和进程名字.
top
 连续不断的显示cpu使用率最高的进程. -b 选项将会以文本方式显示, 以便于可以在脚本
 中分析或存取.
  bash$ top -b
    8:30pm  up 3 min,  3 users,  load average: 0.49, 0.32, 0.13
  45 processes: 44 sleeping, 1 running, 0 zombie, 0 stopped
  CPU states: 13.6% user,  7.3% system,  0.0% nice, 78.9% idle
  Mem:    78396K av,   65468K used,   12928K free,       0K shrd,    2352K buff
  Swap:  157208K av,       0K used,  157208K free                   37244K cached
    PID USER     PRI  NI  SIZE  RSS SHARE STAT %CPU %MEM   TIME COMMAND
    848 bozo      17   0   996  996   800 R     5.6  1.2   0:00 top
   1 root       8   0   512  512   444 S     0.0  0.6   0:04 init
   2 root       9   0     0    0     0 SW    0.0  0.0   0:00 keventd
    ... 
nice
 使用修改后的优先级来运行一个后台作业. 优先级从19(最低)到-20(最高). 只有root用
 户可以设置负的(比较高的)优先级. 相关的命令是renice, snice, 和skill.
nohup
 保持一个命令的运行, 即使用户登出系统. 这个命令做为前台进程来运行, 除非前边加 &.
 如果你在脚本中使用nohup命令, 最好和wait 命令一起使用, 这样可以避免创建一个
 孤儿进程或僵尸进程.
pidof
 取得一个正在运行的作业的进程ID(PID). 因为一些作业控制命令, 比如kill和renice只
 能使用进程的PID(而不是它的名字), 所以有时候必须的取得PID. pidof命令与$PPID内部
 变量非常相似.
  bash$ pidof xclock
  880
Example 13-6 pidof 帮助杀掉一个进程
################################Start Script#######################################
 1 #!/bin/bash
 2 # kill-process.sh
 3
 4 NOPROCESS=2
 5
 6 process=xxxyyyzzz  # 使用不存在的进程.
 7 # 只不过是为了演示...
 8 # ... 并不想在这个脚本中杀掉任何真正的进程.
 9 #
10 # 如果, 举个例子, 你想使用这个脚本来断线Internet,
11 #     process=pppd
12
13 t=`pidof $process`       # 取得$process的pid(进程id).
14 # 'kill'必须使用pid(不能用程序名).
15
16 if [ -z "$t" ]           # 如果没这个进程, 'pidof' 返回空.
17 then
18   echo "Process $process was not running."
19   echo "Nothing killed."
20   exit $NOPROCESS
21 fi 
22
23 kill $t                  # 对于顽固的进程可能需要'kill -9'.
24
25 # 这里需要做一个检查, 看看进程是否允许自身被kill.
26 # 或许另一个 " t=`pidof $process` " 或者 ...
27
28
29 # 整个脚本都可以使用下边这句来替换:
30 #    kill $(pidof -x process_name)
31 # 但是这就没有教育意义了.
32
33 exit 0
################################End Script#########################################
fuser
 取得一个正在存取某个或某些文件(或目录)的进程ID. 使用-k选项将会杀掉这些进程. 对
 于系统安全来说, 尤其是在脚本中想阻止未被授权的用户存取系统服务的时候, 这个命令
 就显得很有用了.
  bash$ fuser -u /usr/bin/vim
  /usr/bin/vim:         3207e(bozo)
 
  bash$ fuser -u /dev/null
  /dev/null:            3009(bozo)  3010(bozo)  3197(bozo)  3199(bozo)
 当正常的插入或删除保存的媒体, 比如CD ROM或者USB闪存设备的时候, fuser的应用也显
 得特别重要. 有时候当你想umount一个设备失败的时候(出现设备忙的错误消息), 这意味
 着某些用户或进程正在存取这个设备. 使用fuser -um /dev/device_name可以搞定这些,
 这样你就可以杀掉所有相关的进程.
  bash$ umount /mnt/usbdrive
  umount: /mnt/usbdrive: device is busy
 
  bash$ fuser -um /dev/usbdrive
  /mnt/usbdrive:        1772c(bozo)
 
  bash$ kill -9 1772
  bash$ umount /mnt/usbdrive
 fuser 的-n选项可以获得正在存取某一端口的进程. 当和nmap命令组合使用的时候尤其
 有用.
  root# nmap localhost.localdomain
  PORT     STATE SERVICE
  25/tcp   open  smtp
 
  root# fuser -un tcp 25
  25/tcp:               2095(root)
 
  root# ps ax | grep 2095 | grep -v grep
  2095 ?        Ss     0:00 sendmail: accepting connections
cron
 管理程序调度器, 执行一些日常任务, 比如清除和删除系统log文件, 或者更新slocate命
 令的数据库. 这是at命令的超级用户版本(虽然每个用户都可以有自己的crontab文件, 并
 且这个文件可以使用crontab命令来修改). 它以幽灵进程T的身份来运行, 并且从
 /ect/crontab中获得执行的调度入口.
 注意: 一些Linux的风格都使用crond, Matthew Dillon的cron.
进程控制和启动类
init
 init 命令是所有进程的父进程. 在系统启动的最后一步调用, init 将会依据
 /etc/inittab来决定系统的运行级别. 只能使用root身份来运行它的别名telinit.
telinit
 init命令的符号链接, 这是一种修改系统运行级别的一个手段, 通常在系统维护或者紧急
 的文件系统修复的时候才用. 只能使用root身份调用. 调用这个命令是非常危险的 - 在
 你使用之前确定你已经很好地了解它.
runlevel
 显示当前和最后的运行级别, 也就是, 确定你的系统是否终止(runlevel 为0), 还是运行
 在单用户模式(1), 多用户模式(2), 或者是运行在X Windows(5), 还是正在重启(6). 这
 个命令将会存取/var/run/utmp文件.
halt, shutdown, reboot
 设置系统关机的命令, 通常比电源关机的优先级高.
service
 开启或停止一个系统服务. 启动脚本在/etc/init.d中, 并且/etc/rc.d在系统启动的时候
 使用这个命令来启动服务.
  root# /sbin/service iptables stop
  Flushing firewall rules:                                   [  OK  ]
  Setting chains to policy ACCEPT: filter                    [  OK  ]
  Unloading iptables modules:                                [  OK  ]
网络类
ifconfig
 网络的接口配置和调试工具.
  bash$ ifconfig -a
  lo        Link encap:Local Loopback
      inet addr:127.0.0.1  Mask:255.0.0.0
      UP LOOPBACK RUNNING  MTU:16436  Metric:1
      RX packets:10 errors:0 dropped:0 overruns:0 frame:0
      TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
      collisions:0 txqueuelen:0
      RX bytes:700 (700.0 b)  TX bytes:700 (700.0 b)
 ifconfig 命令绝大多数情况都是在启动时候设置接口, 或者在重启的时候关闭它们.
    1 # 来自于 /etc/rc.d/init.d/network 的代码片段
    2
    3 # ...
    4
    5 # 检查网络是否启动.
    6 [ ${NETWORKING} = "no" ] && exit 0
    7
    8 [ -x /sbin/ifconfig ] || exit 0
    9
   10 # ...
   11
   12 for i in $interfaces ; do
   13   if ifconfig $i 2>/dev/null | grep -q "UP" >/dev/null 2>&1 ; then
   14     action "Shutting down interface $i: " ./ifdown $i boot
   15   fi
   16 # grep命令的GNU指定的 "-q" 的意思是"安静", 也就是不产生输出.
   17 # 这样, 后边重定向到/dev/null的操作就有点重复了.
   18       
   19 # ...
   20
   21 echo "Currently active devices:"
   22 echo `/sbin/ifconfig | grep ^[a-z] | awk '{print $1}'`
   23 #                            ^^^^^  应该被引用防止globbing.
   24 #  下边这段也能工作.
   25 #    echo $(/sbin/ifconfig | awk '/^[a-z]/ { print $1 })'
   26 #    echo $(/sbin/ifconfig | sed -e 's/ .*//')
   27 #  Thanks, S.C.做了额外的注释.
 参见 Example 29-6.
iwconfig
 这是为了配置无线网络的命令集合. 可以说是上边的ifconfig的无线版本.
route
 显示内核路由表信息, 或者查看内核路由表的修改.
  bash$ route
  Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
  pm3-67.bozosisp *               255.255.255.255 UH       40 0          0 ppp0
  127.0.0.0       *               255.0.0.0       U        40 0          0 lo
  default         pm3-67.bozosisp 0.0.0.0         UG       40 0          0 ppp0
chkconfig
 检查网络配置. 这个命令负责显示和管理在启动过程中所开启的网络服务(这些服务都是
 从/etc/rc?.d目录中开启的).
 最开始是从IRIX到Red Hat Linux的一个接口, chkconfig在某些Linux发行版中并不是核
 心安装的一部分.
  bash$ chkconfig --list
  atd             0:off   1:off   2:off   3:on    4:on    5:on    6:off
  rwhod           0:off   1:off   2:off   3:off   4:off   5:off   6:off
  ...
tcpdump
 网络包的"嗅探器". 这是一个用来分析和调试网络上传输情况的工具, 它所使用的手段是
 把匹配指定规则的包头都显示出来.
 显示主机bozoville和主机caduceus之间所有传输的ip包.
  bash$ tcpdump ip host bozoville and caduceus
 当然,tcpdump的输出可以被分析, 可以用我们之前讨论的文本处理工具来分析结果.
文件系统类
mount
 加载一个文件系统, 通常都用来安装外部设备, 比如软盘或CDROM. 文件/etc/fstab 将会
 提供一个方便的列表, 这个列表列出了所有可用的文件系统, 分区和设备, 另外还包括某
 些选项, 比如是否可以自动或者手动的mount. 文件/etc/mtab 显示了当前已经mount的文
 件系统和分区(包括虚拟的, 比如/proc).
 mount -a 将会mount所有列在/ect/fstab中的文件系统和分区, 除了那些标记有非自动选
 项的. 在启动的时候, 在/etc/rc.d中的一个启动脚本(rc.sysinit或者一些相似的脚本)
 将会这么调用, mount所有可用的文件系统和分区.
    1 mount -t iso9660 /dev/cdrom /mnt/cdrom
    2 # 加载 CDROM
    3 mount /mnt/cdrom
    4 # 方便的方法, 如果 /mnt/cdrom 包含在 /etc/fstab 中
 这个多功能的命令甚至可以将一个普通文件mount到块设备中, 并且这个文件就好像一个
 文件系统一样. mount可以将文件与一个loopback设备相关联来达到这个目的.
 ccomplishes that by associating the file with a loopback device. 这种应用通常
 都是用来mount和检查一个ISO9660镜像,在这个镜像被烧录到CDR之前. [3]
Example 13-7 检查一个CD镜像
################################Start Script#######################################
1 # 以root身份...
2
3 mkdir /mnt/cdtest  # 如果没有的话,准备一个mount点.
4
5 mount -r -t iso9660 -o loop cd-image.iso /mnt/cdtest   # mount这个镜像.
6 #                  "-o loop" option equivalent to "losetup /dev/loop0"
7 cd /mnt/cdtest     # 现在检查这个镜像.
8 ls -alR            # 列出目录树中的文件.
9                    # 等等.
################################End Script#########################################
umount
 卸除一个当前已经mount的文件系统. 在正常删除之前已经mount的软盘和CDROM之前, 这
 个设备必须被unmount, 否则文件系统将会损坏.
   1 umount /mnt/cdrom
   2 # 现在你可以按下退出按钮(指的是cdrom或软盘驱动器上的退出钮), 并安全的退出光盘.
sync
 强制写入所有需要更新的buffer上的数据到硬盘上(同步带有buffer的驱动器). 如果不是
 严格必要的话,一个sync就可以保证系统管理员或者用户刚刚修改的数据会安全的在突然
 的断点中幸存下来. 在比较早以前, 在系统重启前都是使用 sync; sync (两次, 这样保
 证绝对可靠), 这是一种很有用的小心的方法.
 有时候, 比如当你想安全删除一个文件的时候(参见 Example 12-55), 或者当磁盘灯开始
 闪烁的时候, 你可能需要强制马上进行buffer刷新.
losetup
 建立和配置loopback设备.
Example 13-8 在一个文件中创建文件系统
################################Start Script#######################################
1 SIZE=1000000  # 1M
2
3 head -c $SIZE < /dev/zero > file  # 建立指定尺寸的文件.
4 losetup /dev/loop0 file           # 作为loopback设备来建立.
5 mke2fs /dev/loop0                 # 创建文件系统.
6 mount -o loop /dev/loop0 /mnt     # Mount它.
7
8 # Thanks, S.C.
################################End Script#########################################
mkswap
 创建一个交换分区或文件. 交换区域随后必须马上使用swapon来使能.
swapon, swapoff
 使能/禁用 交换分区或文件. 这两个命令通常在启动和关机的时候才有效.
mke2fs
 创建Linux ext2 文件系统. 这个命令必须以root身份调用.
Example 13-9 添加一个新的硬盘驱动器
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 在系统上添加第二块硬盘驱动器.
 4 # 软件配置. 假设硬件已经安装了.
 5 # 来自于本书作者的一篇文章.
 6 # 在"Linux Gazette"的问题#38上, .
 7
 8 ROOT_UID=0     # 这个脚本必须以root身份运行.
 9 E_NOTROOT=67   # 非root用户将会产生这个错误.
10
11 if [ "$UID" -ne "$ROOT_UID" ]
12 then
13   echo "Must be root to run this script."
14   exit $E_NOTROOT
15 fi 
16
17 # 要非常谨慎的小心使用!
18 # 如果某步错了, 可能会彻底摧毁你当前的文件系统.
19
20
21 NEWDISK=/dev/hdb         # 假设/dev/hdb空白. 检查一下!
22 MOUNTPOINT=/mnt/newdisk  # 或者选择另外的mount点.
23
24
25 fdisk $NEWDISK
26 mke2fs -cv $NEWDISK1   # 检查坏块, 详细输出.
27 #  注意:    /dev/hdb1, *不是* /dev/hdb!
28 mkdir $MOUNTPOINT
29 chmod 777 $MOUNTPOINT  # 让所有用户都具有全部权限.
30
31
32 # 现在, 测试一下...
33 # mount -t ext2 /dev/hdb1 /mnt/newdisk
34 # 尝试创建一个目录.
35 # 如果工作起来了, umount它, 然后继续.
36
37 # 最后一步:
38 # 将下边这行添加到/etc/fstab.
39 # /dev/hdb1  /mnt/newdisk  ext2  defaults  1 1
40
41 exit 0
################################End Script#########################################
 参见 Example 13-8 和 Example 28-3.
tune2fs
 调整ext2文件系统. 可以用来修改文件系统参数, 比如mount的最大数量. 必须以root身
 份调用.
 注意: 这是一个非常危险的命令. 如果坏了, 你需要自己负责, 因为它可能会破坏你的文
  件系统.
dumpe2fs
 打印(输出到stdout上)非常详细的文件系统信息. 必须以root身份调用.
  root# dumpe2fs /dev/hda7 | grep 'ount count'
  dumpe2fs 1.19, 13-Jul-2000 for EXT2 FS 0.5b, 95/08/09
  Mount count:              6
  Maximum mount count:      20
hdparm
 列出或修改硬盘参数. 这个命令必须以root身份调用, 如果滥用的话会有危险.
fdisk
 在存储设备上(通常都是硬盘)创建和修改一个分区表. 必须以root身份使用.
 注意: 谨慎使用这个命令. 如果出错, 会破坏你现存的文件系统.
fsck, e2fsck, debugfs
 文件系统的检查, 修复, 和除错命令集合.
 fsck: 检查UNIX文件系统的前端工具(也可以调用其它的工具). 文件系统的类型一般都是
  默认的ext2.
 e2fsck: ext2文件系统检查器.
 debugfs: ext2文件系统除错器. 这个多功能但是危险的工具的用处之一就是(尝试)恢复
  删除的文件. 只有高级用户才能用.
 上边的这几个命令都必须以root身份调用, 这些命令都很危险, 如果滥用的话会破坏文件
 系统.
badblocks
 检查存储设备的坏块(物理损坏). 这个命令在格式化新安装的硬盘时或者测试备份的完整
 性的时候会被用到. [4] 举个例子, badblocks /dev/fd0 测试一个软盘.
 badblocks可能会引起比较糟糕的结果(覆盖所有数据), 在只读模式下就不会发生这种情
 况.如果root用户拥有需要测试的设备(通常都是这种情况), 那么root用户必须调用这个
 命令.
lsusb, usbmodules
 lsusb 命令会列出所有USB(Universal Serial Bus通用串行总线)总线和使用USB的设备.
 usbmodules 命令会输出连接USB设备的驱动模块的信息.
  root# lsusb
  Bus 001 Device 001: ID 0000:0000 
  Device Descriptor:
    bLength                18
    bDescriptorType         1
    bcdUSB               1.00
    bDeviceClass            9 Hub
    bDeviceSubClass         0
    bDeviceProtocol         0
    bMaxPacketSize0         8
    idVendor           0x0000
    idProduct          0x0000
    . . .
mkbootdisk
 创建启动软盘, 启动盘可以唤醒系统, 比如当MBR(master boot record主启动记录)坏掉
 的时候. mkbootdisk 命令其实是一个Bash脚本, 由Erik Troan所编写, 放在/sbin目录中.
chroot
 修改ROOT目录. 一般的命令都是从$PATH中获得的, 相对的默认的根目录是 /. 这个命令
 将会把根目录修改为另一个目录(并且也将把工作目录修改到那). 出于安全目的, 这个命
 令时非常有用的, 举个例子, 当系统管理员希望限制一些特定的用户, 比如telnet上来的
 用户, 将他们限定到文件系统上一个安全的地方(这有时候被称为将一个guest用户限制在
 "chroot 监牢"中). 注意, 在使用chroot之后, 系统的二进制可执行文件的目录将不再
 可用了.
 chroot /opt 将会使得原来的/usr/bin目录变为/opt/usr/bin. 同样,
 chroot /aaa/bbb /bin/ls 将会使得ls命令以/aaa/bbb作为根目录, 而不是以前的/.
 如果使用alias XX 'chroot /aaa/bbb ls', 并把这句放到用户的~/.bashrc文件中的话,
 这将可以有效地限制运行命令"XX"时, 命令"XX"可以使用文件系统的范围.
 当从启动盘恢复的时候(chroot 到 /dev/fd0), 或者当系统从死机状态恢复过来并作为进
 入lilo的选择手段的时候, chroot命令都是非常方便的. 其它的应用还包括从不同的文件
 系统进行安装(一个rpm选项)或者从CDROM上运行一个只读文件系统. 只能以root身份调用,
 小心使用.
 注意: 由于正常的$PATH将不再被关联了, 所以可能需要将一些特定的系统文件拷贝到
  chrooted目录中.
lockfile
 这个工具是procmail包的一部分(). 它可以创建一个锁定文件, 锁定文
 件是一种用来控制存取文件, 设备或资源的标记文件. 锁定文件就像一个标记一样被使用,
  如果特定的文件, 设备, 或资源正在被一个特定的进程所使用("busy"), 那么对于其它进
 程来说, 就只能受限进行存取(或者不能存取).
    1 lockfile /home/bozo/lockfiles/$0.lock
    2 # 创建一个以脚本名字为前缀的写保护锁定文件.
 锁定文件用在一些特定的场合, 比如说保护系统的mail目录以防止多个用户同时修改, 或
 者提示一个modem端口正在被存取, 或者显示Netscape的一个实例正在使用它的缓存. 脚本
 可以做一些检查工作, 比如说一个特定的进程可以创建一个锁定文件, 那么只要检查这个
 特定的进程是否在运行, 就可以判断出锁定文件是否存在了. 注意如果脚本尝试创建一个
 已经存在的锁定文件的话, 那么脚本很可能被挂起.
 一般情况下, 应用创建或检查锁定文件都放在/var/lock目录中. [5] 脚本可以使用下面
 的方法来检测锁定文件是否存在.
    1 appname=xyzip
    2 # 应用 "xyzip" 创建锁定文件 "/var/lock/xyzip.lock".
    3
    4 if [ -e "/var/lock/$appname.lock" ]
    5 then
    6   ...
flock
 flock命令比lockfile命令用得少得多.Much less useful than the lockfile command
 is flock. It sets an "advisory" lock on a file and then executes a command
 while the lock is on. This is to prevent any other process from setting a lock
 on that file until completion of the specified command.
    1 flock $0 cat $0 > lockfile__$0
    2 #  Set a lock on the script the above line appears in,
    3 #+ while listing the script to stdout.
 注意: 与lockfile不同, flock不会自动创建一个锁定文件.
mknod
 创建块或者字符设备文件(当在系统上安装新硬盘时可能是必要的). MAKEDEV工具事实上
 具有nknod的全部功能, 而且更容易使用.
MAKEDEV
 创建设备文件的工具. 必须在/dev目录下, 并且以root身份使用.
  root# ./MAKEDEV
 这是mknod的高级版本.
tmpwatch
 自动删除在指定时间内未被存取过的文件. 通常都是被cron调用, 用来删掉老的log文件.
备份类
dump, restore
 dump 命令是一个精巧的文件系统备份工具, 通常都用在比较大的安装和网络上. [6] 它
 读取原始的磁盘分区并且以二进制形式来写备份文件. 需要备份的文件可以保存到各种各
 样的存储设备上, 包括磁盘和磁带. restore命令用来恢复dump所产生的备份.
fdformat
 对软盘进行低级格式化.

系统资源类
ulimit
 设置使用系统资源的上限. 通常情况下都是使用-f选项来调用, -f用来设置文件尺寸的限
 制(ulimit -f 1000就是将文件大小限制为1M). -c(译者注: 这里应该是作者笔误, 作者
 写的是-t)选项来限制coredump(译者注: 核心转储, 程序崩溃时的内存状态写入文件)
 尺寸(ulimit -c 0 就是不要coredumps). 一般情况下, ulimit的值应该设置在
 /etc/profile 和(或)~/.bash_profile中(参见 Appendix G).
 注意: Judicious 使用ulimit 可以保护系统免受可怕的fork炸弹的迫害.
     1 #!/bin/bash
     2 # 这个脚本只是为了展示用.
     3 # 你要自己为运行这个脚本的后果负责 -- 它*将*凝固你的系统.
     4
     5 while true  #  死循环.
     6 do
     7   $0 &      #  这个脚本调用自身 . . .
     8             #+ fork无限次 . . .
     9             #+ 直道系统完全不动, 因为所有的资源都耗尽了.
    10 done        #  这就是臭名卓著的 "sorcerer's appentice" 剧情.(译者注:巫师的厢房?没看懂)
    11
    12 exit 0      #  这里不会真正的推出, 因为这个脚本不会终止.
  当这个脚本超过预先设置的限制时, 在/etc/profile中的 ulimit -Hu XX (XX 就是需
  要限制的用户进程) 可以终止这个脚本的运行.
quota
 显示用户或组的磁盘配额.
setquota
 从命令行中设置用户或组的磁盘配额.
umask
 设定用户创建文件时权限的缺省mask(掩码). 也可以用来限制特定用户的默认文件属性.
 所有用户创建的文件属性都是由umask所指定的. The (octal) 传递给umask的8进制的值定
 义了文件的权限. 比如, umask 022将会使得新文件的权限最多为755(777 与非 022) [7]
 当然, 用户可以随后使用chmod来修改指定文件的属性. 用户一般都是将umask设置值的地
 方放在/etc/profile 和(或) ~/.bash_profile中 (参见 Appendix G).
Example 13-10 使用umask来将输出文件隐藏起来
################################Start Script#######################################
 1 #!/bin/bash
 2 # rot13a.sh: 与"rot13.sh"脚本相同, 但是会将输出写道"安全"文件中.
 3
 4 # 用法: ./rot13a.sh filename
 5 # 或     ./rot13a.sh  6 # 或     ./rot13a.sh 同时提供键盘输入(stdin)
 7
 8 umask 177               #  文件创建掩码.
 9                         #  被这个脚本所创建的文件
10                         #+ 将具有600权限.
11
12 OUTFILE=decrypted.txt   #  结果保存在"decrypted.txt"中
13                         #+ 这个文件只能够被
14                         #  这个脚本的调用者(or root)所读写.
15
16 cat "$@" | tr 'a-zA-Z' 'n-za-mN-ZA-M' > $OUTFILE
17 #    ^^ 从stdin 或文件中输入.         ^^^^^^^^^^ 输出重定向到文件中.
18
19 exit 0
################################End Script#########################################
rdev
 取得root device, swap space, 或 video mode的相关信息, 或者对它们进行修改. 通常
 说来rdev都是被lilo所使用, 但是在建立一个ram disk的时候, 这个命令也很有用. 小心
 使用, 这是一个危险的命令.
模块类
lsmod
 列出所有安装的内核模块.
  bash$ lsmod
  Module                  Size  Used by
  autofs                  9456   2 (autoclean)
  opl3                   11376   0
  serial_cs               5456   0 (unused)
  sb                     34752   0
  uart401                 6384   0 [sb]
  sound                  58368   0 [opl3 sb uart401]
  soundlow                 464   0 [sound]
  soundcore               2800   6 [sb sound]
  ds                      6448   2 [serial_cs]
  i82365                 22928   2
  pcmcia_core            45984   0 [serial_cs ds i82365]
 注意: 使用cat /proc/modules可以得到同样的结果.
insmod
 强制一个内核模块的安装(如果可能的话, 使用modprobe来代替) 必须以root身份调用.
rmmod
 强制卸载一个内核模块. 必须以root身份调用.
modprobe
 模块装载器, 一般情况下都是在启动脚本中自动调用. 必须以root身份调用.
depmod
 创建模块依赖文件, 一般都是在启动脚本中调用.
modinfo
 输出一个可装载模块的信息.
  bash$ modinfo hid
  filename:    /lib/modules/2.4.20-6/kernel/drivers/usb/hid.o
  description: "USB HID support drivers"
  author:      "Andreas Gal, Vojtech Pavlik <>"
  license:     "GPL"
杂项类
env
 使用设置过的或修改过(并不是修改整个系统环境)的环境变量来运行一个程序或脚本. 使
 用 [varname=xxx] 形式可以在脚本中修改环境变量. 如果没有指定参数, 那么这个命令
 将会列出所有设置的环境变量.
 注意: 在Bash和其它的Bourne shell 衍生物中, 是可以在单一命令行上设置多个变量的.
     1 var1=value1 var2=value2 commandXXX
     2 # $var1 和 $var2 只设置在'commandXXX'的环境中.
 注意: 当不知道shell或解释器的路径的时候, 脚本的第一行(#!行)可以使用env.
     1 #! /usr/bin/env perl
     2
     3 print "This Perl script will run,\n";
     4 print "even when I don't know where to find Perl.\n";
     5
     6 # 便于跨平台移植,
     7 # Perl程序可能没在期望的地方.
     8 # Thanks, S.C.
ldd
 显示一个可执行文件的共享库的依赖关系.
  bash$ ldd /bin/ls
  libc.so.6 => /lib/libc.so.6 (0x4000c000)
 /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)
watch
 以指定的时间间隔来重复运行一个命令.
 默认的时间间隔是2秒, 但时刻以使用-n选项来修改.
    1 watch -n 5 tail /var/log/messages
    2 # 每隔5秒钟显示系统log文件的结尾, /var/log/messages.
strip
 从可执行文件中去掉调试符号引用. 这样做可以减小尺寸, 但是就不能调试了.
 这个命令一般都用在Makefile中, 但是很少用在shell脚本中.
nm
 列出未strip过的编译后的2进制文件的符号.
rdist
 远程文件分布客户机程序: 在远端服务器上同步, 克隆, 或者备份一个文件系统.

13.1 分析一个系统脚本
---------------------
利用我们所学到的关于管理命令的知识, 让我们一起来练习分析一个系统脚本. 最简单并
且最短的系统脚本之一是killall, 这个脚本被用来在系统关机时挂起运行的脚本.
Example 13-11 killall, 来自于 /etc/rc.d/init.d
################################Start Script#######################################
 1 #!/bin/sh
 2
 3 # --> 本书作者所作的注释全部以"# -->"开头.
 4
 5 # --> 这是由Miquel van Smoorenburg所编写的
 6 # --> 'rc'脚本包的一部分, <>.
 7
 8 # --> 这个特殊的脚本看起来是是为Red Hat / FC所特定的,
 9 # --> (在其它的发行版中可能不会出现).
10
11 #  停止所有正在运行的不必要的服务
12 #+ (there shouldn't be any, so this is just a sanity check)
13
14 for i in /var/lock/subsys/*; do
15         # --> 标准的for/in循环, 但是由于"do"在同一行上,
16         # --> 所以必须添加";".
17         # 检查脚本是否在那.
18         [ ! -f $i ] && continue
19         # --> 这是一种使用"与列表"的聪明的方法, 等价于:
20         # --> if [ ! -f "$i" ]; then continue
21
22         # 取得子系统的名字.
23         subsys=${i#/var/lock/subsys/}
24         # --> 匹配变量名, 在这里就是文件名.
25         # --> 与subsys=`basename $i`完全等价.
26  
27         # -->  从锁定文件名中获得
28         # -->+ (如果那里有锁定文件的话,
29         # -->+ 那就证明进程正在运行).
30         # -->  参考一下上边所讲的"锁定文件"的内容.
31
32
33         # 终止子系统.
34         if [ -f /etc/rc.d/init.d/$subsys.init ]; then
35            /etc/rc.d/init.d/$subsys.init stop
36         else
37            /etc/rc.d/init.d/$subsys stop
38         # -->  挂起运行的作业和幽灵进程.
39         # -->  注意"stop"只是一个位置参数,
40         # -->+ 并不是shell内建命令.
41         fi
42 done
################################End Script#########################################
这个没有那么糟. 除了在变量匹配的地方玩了一点花样, 其它也没有别的材料了.
练习 1. 在/etc/rc.d/init.d中, 分析halt脚本. 比脚本killall长一些, 但是概念上很相近.
  对这个脚本做一个拷贝, 放到你的home目录下并且用它练习一下(不要以root身份运
  行它). 使用-vn标志来模拟运行一下(sh -vn scriptname). 添加详细的注释. 将
  "action"命令修改为"echos".
练习 2. 察看/etc/rc.d/init.d下的更多更复杂的脚本. 看看你是不是能够理解其中的一些脚
  本. 使用上边的过程来分析这些脚本. 为了更详细的理解, 你可能也需要分析在
  usr/share/doc/initscripts-?.??目录下的文件sysvinitfile, 这些都是
  "initscript"文件的一部分.
注意事项:
[1]  这是在Linux机器上或者在带有磁盘配额的UNIX系统上的真实情况.
[2]  如果正在被删除的特定的用户已经登录了主机, 那么 userdel 命令将会失败.
[3]  对于烧录CDR的更多的细节, 可以参见Alex Withers的文章, 创建CD, 在
  Linux Journal 的1999年的10月文章列表中.
[4]  mke2fs的-c选项也会进行坏块检查.
[5]  因为只有root用户才具有对/var/lock目录的写权限, 一般的用户脚本是不能在那里
  设置一个锁定文件的.
[6]  单用户的Linux系统的操作更倾向于使用简单的备份工具, 比如tar.
[7]  NAND(与非)是一种逻辑操作. 这种操作的效果和减法很相像.
 
第14章 命令替换
================
命令替换将会重新分配一个命令[1]甚至是多个命令的输出; 它会将命令的输出如实地添加到
另一个上下文中. [2]
使用命令替换的典型形式是使用后置引用(`...`). 后置引用形式的命令(就是被反引号括起来)
将会产生命令行文本.
   1 script_name=`basename $0`
   2 echo "The name of this script is $script_name."
这样的话, 命令的输出可以被当成传递到另一个命令的参数, 或者保存到变量中, 甚至可以用
来产生for循环的参数列表.
   1 rm `cat filename`   # "filename" 包含了需要被删除的文件列表.
   2 #
   3 # S. C. 指出使用这种形式, 可能会产生"参数列表太长"的错误.
   4 # 更好的方法是              xargs rm -- < filename
   5 # ( -- 同时覆盖了那些以"-"开头的文件所产生的特殊情况 )
   6
   7 textfile_listing=`ls *.txt`
   8 # 变量中包含了当前工作目录下所有的*.txt文件.
   9 echo $textfile_listing
  10
  11 textfile_listing2=$(ls *.txt)   # 这是命令替换的另一种形式.
  12 echo $textfile_listing2
  13 # 同样的结果.
  14
  15 # 将文件列表放入到一个字符串中的一个可能的问题就是
  16 # 可能会混进一个新行.
  17 #
  18 # 一个安全的将文件列表传递到参数中的方法就是使用数组.
  19 #      shopt -s nullglob    # 如果不匹配, 那就不进行文件名扩展.
  20 #      textfile_listing=( *.txt )
  21 #
  22 # Thanks, S.C.
注意: 命令替换将会调用一个subshell.
注意: 命令替换可能会引起word splitting.
   1 COMMAND `echo a b`     # 2个参数: a and b
   2
   3 COMMAND "`echo a b`"   # 1个参数: "a b"
   4
   5 COMMAND `echo`         # 无参数
   6
   7 COMMAND "`echo`"       # 一个空的参数
   8
   9
  10 # Thanks, S.C.
 即使没有引起word splitting, 命令替换也会去掉多余的新行.
   1 # cd "`pwd`"  # 这句总会正常的工作.
   2 # 然而...
   3
   4 mkdir 'dir with trailing newline
   5 '
   6
   7 cd 'dir with trailing newline
   8 '
   9
  10 cd "`pwd`"  # 错误消息:
  11 # bash: cd: /tmp/file with trailing newline: No such file or directory
  12
  13 cd "$PWD"   # 运行良好.
  14
  15
  16
  17
  18
  19 old_tty_setting=$(stty -g)   # 保存老的终端设置.
  20 echo "Hit a key "
  21 stty -icanon -echo           # 对终端禁用"canonical"模式.
  22                              # 这样的话, 也会禁用了*本地*的echo.
  23 key=$(dd bs=1 count=1 2> /dev/null)   # 使用'dd'命令来取得一个按键.
  24 stty "$old_tty_setting"      # 保存老的设置.
  25 echo "You hit ${#key} key."  # ${#variable} = number of characters in $variable
  26 #
  27 # 按键任何键除了回车, 那么输出就是"You hit 1 key."
  28 # 按下回车, 那么输出就是"You hit 0 key."
  29 # 新行已经被命令替换吃掉了.
  30
  31 Thanks, S.C.
注意: 当一个变量是使用命令替换的结果做为值的时候, 然后使用echo命令来输出这个变量
 (并且不引用这个变量, 就是不用引号括起来), 那么命令替换将会从最终的输出中删掉换
 行符. 这可能会引起一些异常情况.
   1 dir_listing=`ls -l`
   2 echo $dir_listing     # 未引用, 就是没用引号括起来
   3
   4 # 想打出来一个有序的目录列表.Expecting a nicely ordered directory listing.
   5
   6 # 可惜, 下边将是我们所获得的:
   7 # total 3 -rw-rw-r-- 1 bozo bozo 30 May 13 17:15 1.txt -rw-rw-r-- 1 bozo
   8 # bozo 51 May 15 20:57 t2.sh -rwxr-xr-x 1 bozo bozo 217 Mar 5 21:13 wi.sh
   9
  10 # 新行消失了.
  11
  12
  13 echo "$dir_listing"   # 用引号括起来
  14 # -rw-rw-r--    1 bozo       30 May 13 17:15 1.txt
  15 # -rw-rw-r--    1 bozo       51 May 15 20:57 t2.sh
  16 # -rwxr-xr-x    1 bozo      217 Mar  5 21:13 wi.sh
命令替换甚至允许将整个文件的内容放到变量中, 可以使用重定向或者cat命令.
   1 variable1=`   2 variable2=`cat file2`   #  将"file2"的内容放到"variable2"中.
   3                         #  但是这行将会fork一个新进程, This, however, forks a new process,
   4                         #+ 所以这行代码将会比第一行代码执行得慢.
   5
   6 #  注意:
   7 #  变量中是可以包含空白的,
   8 #+ 甚至是 (厌恶至极的), 控制字符.
   1 #  摘录自系统文件, /etc/rc.d/rc.sysinit
   2 #+ (这是红帽安装中使用的)
   3
   4
   5 if [ -f /fsckoptions ]; then
   6         fsckoptions=`cat /fsckoptions`
   7 ...
   8 fi
   9 #
  10 #
  11 if [ -e "/proc/ide/${disk[$device]}/media" ] ; then
  12              hdmedia=`cat /proc/ide/${disk[$device]}/media`
  13 ...
  14 fi
  15 #
  16 #
  17 if [ ! -n "`uname -r | grep -- "-"`" ]; then
  18        ktag="`cat /proc/version`"
  19 ...
  20 fi
  21 #
  22 #
  23 if [ $usb = "1" ]; then
  24     sleep 5
  25     mouseoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=02"`
  26     kbdoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=01"`
  27 ...
  28 fi
注意: 不要将一个非常长的文本文件的内容设置到一个变量中, 除非你有一个非常好的原因非
 要这么做不可. 不要将2进制文件的内容保存到变量中.
Example 14-1 愚蠢的脚本策略
################################Start Script#######################################
 1 #!/bin/bash
 2 # stupid-script-tricks.sh: 朋友, 别在家这么做.
 3 # 来自于"Stupid Script Tricks," 卷I.
 4
 5
 6 dangerous_variable=`cat /boot/vmlinuz`   # 这是压缩过的Linux内核本身.
 7
 8 echo "string-length of \$dangerous_variable = ${#dangerous_variable}"
 9 # 这个字符串变量的长度是 $dangerous_variable = 794151
10 # (不要使用'wc -c /boot/vmlinuz'来计算长度.)
11
12 # echo "$dangerous_variable"
13 # 千万别尝试这么做! 这样将挂起这个脚本.
14
15
16 #  文档作者已经意识到将二进制文件设置到
17 #+ 变量中是一个没用的应用.
18
19 exit 0
################################End Script#########################################
 注意, 在这里是不会发生缓冲区溢出错误. 因为这是一个解释型语言的实例, Bash就是一
 种解释型语言, 解释型语言会比编译型语言提供更多的对程序错误的保护措施.
变量替换允许将一个循环的输出放入到一个变量中.这么做的关键就是将循环中echo命令的输
出全部截取.
Example 14-2 从循环的输出中产生一个变量
################################Start Script#######################################
 1 #!/bin/bash
 2 # csubloop.sh: 从循环的输出中产生一个变量.
 3
 4 variable1=`for i in 1 2 3 4 5
 5 do
 6   echo -n "$i"                 #  对于这里的命令替换来说
 7 done`                          #+ 这个'echo'命令是非常关键的.
 8
 9 echo "variable1 = $variable1"  # variable1 = 12345
10
11
12 i=0
13 variable2=`while [ "$i" -lt 10 ]
14 do
15   echo -n "$i"                 # 再来一个, 'echo'是必须的.
16   let "i += 1"                 # 递增.
17 done`
18
19 echo "variable2 = $variable2"  # variable2 = 0123456789
20
21 #  这就证明了在一个变量声明中
22 #+ 嵌入一个循环是可行的.
23
24 exit 0
################################End Script#########################################
注意: 命令替换使得扩展有效的Bash工具集变为可能. 这样, 写一段小程序或者一段脚本就可
 以达到目的, 因为程序或脚本的输出会传到stdout上(就像一个标准的工具所做的那样),
 然后重新将这些输出保存到变量中.(译者: 作者的意思就是在这种情况下写脚本和写程序
 作用是一样的.)
   1 #include
   2
   3 /*  "Hello, world." C program  */  
   4
   5 int main()
   6 {
   7   printf( "Hello, world." );
   8   return (0);
   9 }
 bash$ gcc -o hello hello.c
   1 #!/bin/bash
   2 # hello.sh  
   3
   4 greeting=`./hello`
   5 echo $greeting
 bash$ sh hello.sh
 Hello, world.
注意: 对于命令替换来说,$(COMMAND) 形式已经取代了反引号"`".
   1 output=$(sed -n /"$1"/p $file)   # 来自于 "grp.sh"例子.
   2       
   3 # 将一个文本的内容保存到变量中.
   4 File_contents1=$(cat $file1)     
   5 File_contents2=$(<$file2)        # Bash 也允许这么做.
 $(...) 形式的命令替换在处理双反斜线(\\)时与`...`形式不同.
  bash$ echo `echo \\`
 
  bash$ echo $(echo \\)
  \
 $(...) 形式的命令替换是允许嵌套的. [3]
   1 word_count=$( wc -w $(ls -l | awk '{print $9}') )
 或者, 可以更加灵活. . .
Example 14-3 找anagram(回文构词法, 可以将一个有意义的单词, 变换为1个或多个有意义的单词, 但是还是原来的子母集合)
################################Start Script#######################################
 1 #!/bin/bash
 2 # agram2.sh
 3 # 关于命令替换嵌套的例子.
 4
 5 #  使用"anagram"工具
 6 #+ 这是作者的"yawl"文字表包中的一部分.
 7 # 
 8 # 
 9
10 E_NOARGS=66
11 E_BADARG=67
12 MINLEN=7
13
14 if [ -z "$1" ]
15 then
16   echo "Usage $0 LETTERSET"
17   exit $E_NOARGS         # 脚本需要一个命令行参数.
18 elif [ ${#1} -lt $MINLEN ]
19 then
20   echo "Argument must have at least $MINLEN letters."
21   exit $E_BADARG
22 fi
23
24
25
26 FILTER='.......'         # 必须至少有7个字符.
27 #       1234567
28 Anagrams=( $(echo $(anagram $1 | grep $FILTER) ) )
29 #           |     |    嵌套的命令替换        | |
30 #        (              数组分配                 )
31
32 echo
33 echo "${#Anagrams[*]}  7+ letter anagrams found"
34 echo
35 echo ${Anagrams[0]}      # 第一个anagram.
36 echo ${Anagrams[1]}      # 第二个anagram.
37                          # 等等.
38
39 # echo "${Anagrams[*]}"  # 在一行上列出所有的anagram . . .
40
41 #  考虑到后边还有"数组"作为单独的一章进行讲解,
42 #+ 这里就不深入了.
43
44 # 可以参阅agram.sh脚本, 这也是一个找出anagram的例子.
45
46 exit $?
################################End Script#########################################
命令替换在脚本中使用的例子:
1.  Example 10-7
2.  Example 10-26
3.  Example 9-28
4.  Example 12-3
5.  Example 12-19
6.  Example 12-15
7.  Example 12-49
8.  Example 10-13
9.  Example 10-10
10. Example 12-29
11. Example 16-8 
12. Example A-17 
13. Example 27-2 
14. Example 12-42
15. Example 12-43
16. Example 12-44
注意事项:
[1]  对于命令替换来说, 这个命令可以是外部的系统命令, 也可以是内部脚本的内建
  命令, 甚至是一个脚本函数.
[2]  从技术的角度来讲, 命令替换将会抽取出一个命令的输出, 然后使用=操作赋值到
  一个变量中.
[3]  事实上, 对于反引号的嵌套是可行的, 但是只能将内部的反引号转义才行, 就像
  John默认指出的那样.
     1 word_count=` wc -w \`ls -l | awk '{print $9}'\` `
 
第15章 算术扩展
================
算术扩展提供了一种强力的工具, 可以在脚本中执行(整型)算法操作. 可以使用backticks,
double parentheses, 或 let来将字符串转换为数字表达式.
一些变化
使用反引号的算术扩展(通常都是和expr一起使用)
    1 z=`expr $z + 3`          # 'expr'命令将会执行这个扩展.
使用双括号, 和let形式的算术扩展
反引号形式的算术扩展已经被双括号形式所替代了 -- ((...)) 和 $((...)) -- 当然也可以
使用非常方便的let形式.
    1 z=$(($z+3))
    2 z=$((z+3))                                  #  也正确.
    3                                             #  使用双括号的形式,
    4                                             #+ 参数解引用
    5                                             #+ 是可选的.
    6
    7 # $((EXPRESSION)) is arithmetic expansion.  #  不要与命令
    8                                             #+ 替换相混淆.
    9
   10
   11
   12 # 使用双括号的形式也可以不用给变量赋值.
   13
   14   n=0
   15   echo "n = $n"                             # n = 0
   16
   17   (( n += 1 ))                              # 递增.
   18 # (( $n += 1 )) is incorrect!
   19   echo "n = $n"                             # n = 1
   20
   21
   22 let z=z+3
   23 let "z += 3"  #  使用引用的形式, 允许在变量赋值的时候存在空格.
   24               #  'let'操作事实上执行得的是算术赋值,
   25               #+ 而不是算术扩展.
下边是一些在脚本中使用算术扩展的例子:
1.  Example 12-9
2.  Example 10-14
3.  Example 26-1
4.  Example 26-11
5.  Example A-17

第16章 I/O 重定向
==================
默认情况下始终有3个"文件"处于打开状态, stdin (键盘), stdout (屏幕), and stderr
(错误消息输出到屏幕上). 这3个文件和其他打开的文件都可以被重定向. 对于重定向简单的
解释就是捕捉一个文件, 命令, 程序, 脚本, 或者甚至是脚本中的代码块(参见 Example 3-1
和 Example 3-2)的输出, 然后将这些输出作为输入发送到另一个文件, 命令, 程序, 或脚本
中.
每个打开的文件都会被分配一个文件描述符.[1]stdin, stdout, 和stderr的文件描述符分别
是0, 1, 和 2. 对于正在打开的额外文件, 保留了描述符3到9. 在某些时候将这些格外的文件
描述符分配给stdin, stdout, 或者是stderr作为临时的副本链接是非常有用的. [2] 在经过
复杂的重定向和刷新之后需要把它们恢复成正常的样子 (参见 Example 16-1).
   1    COMMAND_OUTPUT >
   2       # 重定向stdout到一个文件.
   3       # 如果没有这个文件就创建, 否则就覆盖.
   4
   5       ls -lR > dir-tree.list
   6       # 创建一个包含目录树列表的文件.
   7
   8    : > filename
   9       # > 会把文件"filename"截断为0长度.
  10       # 如果文件不存在, 那么就创建一个0长度的文件(与'touch'的效果相同).
  11       # : 是一个占位符, 不产生任何输出.
  12
  13    > filename   
  14       # > 会把文件"filename"截断为0长度.
  15       # 如果文件不存在, 那么就创建一个0长度的文件(与'touch'的效果相同).
  16       # (与上边的": >"效果相同, 但是在某些shell下可能不能工作.)
  17
  18    COMMAND_OUTPUT >>
  19       # 重定向stdout到一个文件.
  20       # 如果文件不存在, 那么就创建它, 如果存在, 那么就追加到文件后边.
  21
  22
  23       # 单行重定向命令(只会影响它们所在的行):
  24       # --------------------------------------------------------------------
  25
  26    1>filename
  27       # 重定向stdout到文件"filename".
  28    1>>filename
  29       # 重定向并追加stdout到文件"filename".
  30    2>filename
  31       # 重定向stderr到文件"filename".
  32    2>>filename
  33       # 重定向并追加stderr到文件"filename".
  34    &>filename
  35       # 将stdout和stderr都重定向到文件"filename".
  36
  37       #==============================================================================
  38       # 重定向stdout, 一次一行.
  39       LOGFILE=script.log
  40
  41       echo "This statement is sent to the log file, \"$LOGFILE\"." 1>$LOGFILE
  42       echo "This statement is appended to \"$LOGFILE\"." 1>>$LOGFILE
  43       echo "This statement is also appended to \"$LOGFILE\"." 1>>$LOGFILE
  44       echo "This statement is echoed to stdout, and will not appear in \"$LOGFILE\"."
  45       # 每行过后, 这些重定向命令会自动"reset".
  46
  47
  48
  49       # 重定向stderr, 一次一行.
  50       ERRORFILE=script.errors
  51
  52       bad_command1 2>$ERRORFILE       #  错误消息发到$ERRORFILE中.
  53       bad_command2 2>>$ERRORFILE      #  错误消息添加到$ERRORFILE中.
  54       bad_command3                    #  错误消息echo到stderr,
  55                                       #+ 并且不出现在$ERRORFILE中.
  56       # 每行过后, 这些重定向命令也会自动"reset".
  57       #==============================================================================
  58
  59
  60
  61    2>&1
  62       # 重定向stderr到stdout.
  63       # 得到的错误消息与stdout一样, 发送到一个地方.
  64
  65    i>&j
  66       # 重定向文件描述符i 到 j.
  67       # 指向i文件的所有输出都发送到j中去.
  68
  69    >&j
  70       # 默认的, 重定向文件描述符1(stdout)到 j.
  71       # 所有传递到stdout的输出都送到j中去.
  72
  73    0< FILENAME
  74     < FILENAME
  75       # 从文件中接受输入.
  76       # 与">"是成对命令, 并且通常都是结合使用.
  77       #
  78       # grep search-word   79
  80
  81    [j]<>filename
  82       # 为了读写"filename", 把文件"filename"打开, 并且分配文件描述符"j"给它.
  83       # 如果文件"filename"不存在, 那么就创建它.
  84       # 如果文件描述符"j"没指定, 那默认是fd 0, stdin.
  85       #
  86       # 这种应用通常是为了写到一个文件中指定的地方.
  87       echo 1234567890 > File    # 写字符串到"File".
  88       exec 3<> File             # 打开"File"并且给它分配fd 3.
  89       read -n 4 <&3             # 只读4个字符.
  90       echo -n . >&3             # 写一个小数点.
  91       exec 3>&-                 # 关闭fd 3.
  92       cat File                  # ==> 1234.67890
  93       # 随机存储.
  94
  95
  96
  97    |
  98       # 管道.
  99       # 通用目的的处理和命令链工具.
 100       # 与">"很相似, 但是实际上更通用.
 101       # 对于想将命令, 脚本, 文件和程序串连起来的时候很有用.
 102       cat *.txt | sort | uniq > result-file
 103       # 对所有的.txt文件的输出进行排序, 并且删除重复行,
 104       # 最后将结果保存到"result-file"中.

可以将输入输出重定向和(或)管道的多个实例结合到一起写在一行上.
   1 command < input-file > output-file
   2
   3 command1 | command2 | command3 > output-file
参见 Example 12-28 和 Example A-15.
可以将多个输出流重定向到一个文件上.
   1 ls -yz >> command.log 2>&1
   2 #  将错误选项"yz"的结果放到文件"command.log"中.
   3 #  因为stderr被重定向到这个文件中,
   4 #+ 所有的错误消息也就都指向那里了.
   5
   6 #  注意, 下边这个例子就不会给出相同的结果.
   7 ls -yz 2>&1 >> command.log
   8 #  输出一个错误消息, 但是并不写到文件中.
   9
  10 #  如果将stdout和stderr都重定向,
  11 #+ 命令的顺序会有些不同.
关闭文件描述符
n<&-  关闭输入文件描述符n.
0<&-, <&- 关闭stdin.
n>&-  关闭输出文件描述符n.
1>&-, >&- 关闭stdout.
子进程继承了打开的文件描述符. 这就是为什么管道可以工作. 如果想阻止fd被继承, 那么可
以关掉它.
   1 # 只重定向stderr到一个管道.
   2
   3 exec 3>&1                              # 保存当前stdout的"值".
   4 ls -l 2>&1 >&3 3>&- | grep bad 3>&-    # 对'grep'关闭fd 3(但不关闭'ls').
   5 #              ^^^^   ^^^^
   6 exec 3>&-                              # 现在对于剩余的脚本关闭它.
   7
   8 # Thanks, S.C.
如果想了解关于I/O重定向更多的细节参见 附录 E.
16.1. 使用exec
--------------
exec 件了, 而不是标准输入了(通常都是键盘输入). 这样就提供了一种按行读取文件的方法, 并且
可以使用sed 和/或 awk来对每一行进行分析.
Example 16-1 使用exec重定向标准输入
################################Start Script#######################################
 1 #!/bin/bash
 2 # 使用'exec'重定向标准输入.
 3
 4
 5 exec 6<&0          # 将文件描述符#6与stdin链接起来.
 6                    # 保存了stdin.
 7
 8 exec < data-file   # stdin被文件"data-file"所代替.
 9
10 read a1            # 读取文件"data-file"的第一行.
11 read a2            # 读取文件"data-file"的第二行.
12
13 echo
14 echo "Following lines read from file."
15 echo "-------------------------------"
16 echo $a1
17 echo $a2
18
19 echo; echo; echo
20
21 exec 0<&6 6<&-
22 #  现在将stdin从fd #6中恢复, 因为刚才我们把stdin重定向到#6了,
23 #+ 然后关闭fd #6 ( 6<&- ), 好让这个描述符继续被其他进程所使用.
24 #
25 # <&6 6<&-    这么做也可以.
26
27 echo -n "Enter data  "
28 read b1  # 现在"read"已经恢复正常了, 就是从stdin中读取.
29 echo "Input read from stdin."
30 echo "----------------------"
31 echo "b1 = $b1"
32
33 echo
34
35 exit 0
################################End Script#########################################
同样的, exec >filename 命令将会把stdout重定向到一个指定的文件中. 这样所有的命令输
出就都会发向那个指定的文件, 而不是stdout.
Example 16-2 使用exec来重定向stdout
################################Start Script#######################################
 1 #!/bin/bash
 2 # reassign-stdout.sh
 3
 4 LOGFILE=logfile.txt
 5
 6 exec 6>&1           # 将fd #6与stdout相连接.
 7                     # 保存stdout.
 8
 9 exec > $LOGFILE     # stdout就被文件"logfile.txt"所代替了.
10
11 # ----------------------------------------------------------- #
12 # 在这块中所有命令的输出就都发向文件 $LOGFILE.
13
14 echo -n "Logfile: "
15 date
16 echo "-------------------------------------"
17 echo
18
19 echo "Output of \"ls -al\" command"
20 echo
21 ls -al
22 echo; echo
23 echo "Output of \"df\" command"
24 echo
25 df
26
27 # ----------------------------------------------------------- #
28
29 exec 1>&6 6>&-      # 恢复stdout, 然后关闭文件描述符#6.
30
31 echo
32 echo "== stdout now restored to default == "
33 echo
34 ls -al
35 echo
36
37 exit 0
################################End Script#########################################
Example 16-3 使用exec在同一脚本中重定向stdin和stdout
################################Start Script#######################################
 1 #!/bin/bash
 2 # upperconv.sh
 3 # 将一个指定的输入文件转换为大写.
 4
 5 E_FILE_ACCESS=70
 6 E_WRONG_ARGS=71
 7
 8 if [ ! -r "$1" ]     # 判断指定的输入文件是否可读?
 9 then
10   echo "Can't read from input file!"
11   echo "Usage: $0 input-file output-file"
12   exit $E_FILE_ACCESS
13 fi                   #  即使输入文件($1)没被指定
14                      #+ 也还是会以相同的错误退出(为什么?).
15
16 if [ -z "$2" ]
17 then
18   echo "Need to specify output file."
19   echo "Usage: $0 input-file output-file"
20   exit $E_WRONG_ARGS
21 fi
22
23
24 exec 4<&0
25 exec < $1            # 将会从输入文件中读取.
26
27 exec 7>&1
28 exec > $2            # 将写到输出文件中.
29                      # 假设输出文件是可写的(添加检查?).
30
31 # -----------------------------------------------
32     cat - | tr a-z A-Z   # 转换为大写.
33 #   ^^^^^                # 从stdin中读取.Reads from stdin.
34 #           ^^^^^^^^^^   # 写到stdout上.
35 # 然而, stdin和stdout都被重定向了.
36 # -----------------------------------------------
37
38 exec 1>&7 7>&-       # 恢复 stout.
39 exec 0<&4 4<&-       # 恢复 stdin.
40
41 # 恢复之后, 下边这行代码将会如期望的一样打印到stdout上.
42 echo "File \"$1\" written to \"$2\" as uppercase conversion."
43
44 exit 0
################################End Script#########################################
I/O重定向是一种避免可怕的子shell中不可存取变量问题的方法.
Example 16-4 避免子shell
################################Start Script#######################################
 1 #!/bin/bash
 2 # avoid-subshell.sh
 3 # Matthew Walker提出的建议.
 4
 5 Lines=0
 6
 7 echo
 8
 9 cat myfile.txt | while read line;  #  (译者注: 管道会产生子shell)
10                  do {
11                    echo $line
12                    (( Lines++ ));  #  增加这个变量的值
13                                    #+ 但是外部循环却不能存取.
14                                    #  子shell问题.
15                  }
16                  done
17
18 echo "Number of lines read = $Lines"     # 0
19                                          # 错误!
20
21 echo "------------------------"
22
23
24 exec 3<> myfile.txt
25 while read line <&3
26 do {
27   echo "$line"
28   (( Lines++ ));                   #  增加这个变量的值
29                                    #+ 现在外部循环就可以存取了.
30                                    #  没有子shell, 现在就没问题了.
31 }
32 done
33 exec 3>&-
34
35 echo "Number of lines read = $Lines"     # 8
36
37 echo
38
39 exit 0
40
41 # 下边这些行是脚本的结果, 脚本是不会走到这里的.
42
43 $ cat myfile.txt
44
45 Line 1.
46 Line 2.
47 Line 3.
48 Line 4.
49 Line 5.
50 Line 6.
51 Line 7.
52 Line 8.
################################End Script#########################################
注意事项:
[1]  一个文件描述符说白了就是文件系统为了跟踪这个打开的文件而分配给它的一个数字.
  也可以的将其理解为文件指针的一个简单版本. 与C中的文件句柄的概念相似.
[2]  使用文件描述符5可能会引起问题. 当Bash使用exec创建一个子进程的时候, 子进程
  会继承fd5(参见Chet Ramey的归档e-mail, SUBJECT: RE: File descriptor 5 is
  held open). 最好还是不要去招惹这个特定的fd.

第17章 Here Documents
======================
here document 就是一段特殊目的的代码块. 他使用I/O 重定向的形式来将一个命令序列传递
到一个交互程序或者命令中, 比如ftp, cat, 或者ex文本编辑器.
   1 COMMAND <   2 ...
   3 InputComesFromHERE
limit string 用来划定命令序列的范围(译者注: 两个相同的limit string之间就是命令序列)
. 特殊符号 << 用来表识limit string. 这个符号具有重定向文件的输出到程序或命令的输入
的作用. 与 interactive-program < command-file 很相象, command-file包含:
   1 command #1
   2 command #2
   3 ...
而here document 的形式看上去是如下的样子:
   1 #!/bin/bash
   2 interactive-program <   3 command #1
   4 command #2
   5 ...
   6 LimitString
选择一个名字非常诡异的limit string将会避免命令列表和limit string重名的问题.
注意,某些时候here document 用在非交互工具和命令上的时候也会有好的效果, 比如, wall.
Example 17-1 广播: 发送消息给每个登录上的用户
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 wall < 4 E-mail your noontime orders for pizza to the system administrator.
 5     (Add an extra dollar for anchovy or mushroom topping.)
 6 # 额外的消息文本写在这里.
 7 # 注意: 'wall' 会打印注释行.
 8 zzz23EndOfMessagezzz23
 9
10 # 可以使用更有效率的做法
11 #         wall 12 #  然而将消息模版嵌入到脚本中
13 #+ 是一种"小吃店"(快速但是比较脏)的只能使用一次的解决办法.
14
15 exit 0
################################End Script#########################################
即使是某些不大可能的工具, 如vi也可以使用here document.
Example 17-2 仿造文件: 创建一个两行的仿造文件
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 用非交互的方式来使用'vi'编辑一个文件.
 4 # 模仿'sed'.
 5
 6 E_BADARGS=65
 7
 8 if [ -z "$1" ]
 9 then
10   echo "Usage: `basename $0` filename"
11   exit $E_BADARGS
12 fi
13
14 TARGETFILE=$1
15
16 # 在文件中插入两行, 然后保存.
17 #--------Begin here document-----------#
18 vi $TARGETFILE <19 i
20 This is line 1 of the example file.
21 This is line 2 of the example file.
22 ^[
23 ZZ
24 x23LimitStringx23
25 #----------End here document-----------#
26
27 #  注意上边^[是一个转义符,键入Ctrl+v 就行,
28 #+ 事实上它是键.
29
30 #  Bram Moolenaar指出这种方法不能正常地用在'vim'上, (译者注: Bram Moolenaar是vim作者)
31 #+ 因为可能会有终端的相互影响问题.
32
33 exit 0
################################End Script#########################################
上边的脚本也可以不用vi而用ex来实现. Here document 包含ex命令列表的做法足够形成自己
的类别了, 叫ex scripts.
   1 #!/bin/bash
   2 #  把所有后缀为".txt"文件
   3 #+ 中的"Smith"都替换成"Jones".
   4
   5 ORIGINAL=Smith
   6 REPLACEMENT=Jones
   7
   8 for word in $(fgrep -l $ORIGINAL *.txt)
   9 do
  10   # -------------------------------------
  11   ex $word <  12   :%s/$ORIGINAL/$REPLACEMENT/g
  13   :wq
  14 EOF
  15   # :%s 是"ex"的替换命令.
  16   # :wq 是保存并退出的意思.
  17   # -------------------------------------
  18 done
与"ex scripts"相似的是cat scripts.
Example 17-3 使用cat的多行消息
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  'echo' 对于打印单行消息是非常好的,
 4 #+  但是在打印消息块时可能就有点问题了.
 5 #   'cat' here document可以解决这个限制.
 6
 7 cat < 8 -------------------------------------
 9 This is line 1 of the message.
10 This is line 2 of the message.
11 This is line 3 of the message.
12 This is line 4 of the message.
13 This is the last line of the message.
14 -------------------------------------
15 End-of-message
16
17 #  用下边这行代替上边的第7行
18 #+   cat > $Newfile <19 #+       ^^^^^^^^^^
20 #+ 那么就会把输出写到文件$Newfile中, 而不是stdout.
21
22 exit 0
23
24
25 #--------------------------------------------
26 # 下边的代码不会运行, 因为上边的"exit 0".
27
28 # S.C. 指出下边代码也可以运行.
29 echo "-------------------------------------
30 This is line 1 of the message.
31 This is line 2 of the message.
32 This is line 3 of the message.
33 This is line 4 of the message.
34 This is the last line of the message.
35 -------------------------------------"
36 # 然而, 文本可能不包含双引号, 除非它们被转义.
################################End Script#########################################
- 选项用来标记here document的limit string (<<-LimitString), 可以抑制输出时前边的tab
(不是空格). 这可以增加一个脚本的可读性.
Example 17-4 带有抑制tab功能的多行消息
################################Start Script#######################################
 1 #!/bin/bash
 2 # 与之前的例子相同, 但是...
 3
 4 #  - 选项对于here docutment来说,<<-
 5 #+ 可以抑制文档体前边的tab,
 6 #+ 而*不*是空格 *not* spaces.
 7
 8 cat <<-ENDOFMESSAGE
 9  This is line 1 of the message.
10  This is line 2 of the message.
11  This is line 3 of the message.
12  This is line 4 of the message.
13  This is the last line of the message.
14 ENDOFMESSAGE
15 # 脚本在输出的时候左边将被刷掉.
16 # 就是说每行前边的tab将不会显示.
17
18 # 上边5行"消息"的前边都是tab, 不是空格.
19 # 空格是不受<<-影响的.
20
21 # 注意, 这个选项对于*嵌在*中间的tab没作用.
22
23 exit 0
################################End Script#########################################
here document 支持参数和命令替换. 所以也可以给here document的消息体传递不同的参数,
这样相应的也会修改输出.
Example 17-5 使用参数替换的here document
################################Start Script#######################################
 1 #!/bin/bash
 2 # 一个使用'cat'命令的here document, 使用了参数替换
 3
 4 # 不传命令行参数给它,   ./scriptname
 5 # 传一个命令行参数给它,   ./scriptname Mortimer
 6 # 传一个2个单词(用引号括起来)的命令行参数给它,
 7 #                           ./scriptname "Mortimer Jones"
 8
 9 CMDLINEPARAM=1     #  所期望的最少的命令行参数的个数.
10
11 if [ $# -ge $CMDLINEPARAM ]
12 then
13   NAME=$1          #  如果命令行参数超过1个,
14                    #+ 那么就只取第一个参数.
15 else
16   NAME="John Doe"  #  默认情况下, 如果没有命令行参数的话.
17 fi 
18
19 RESPONDENT="the author of this fine script" 
20  
21
22 cat <23
24 Hello, there, $NAME.
25 Greetings to you, $NAME, from $RESPONDENT.
26
27 # This comment shows up in the output (why?).
28
29 Endofmessage
30
31 # 注意上边的空行也打印到输出,
32 # 而上边那行"注释"当然也会打印到输出.
33 # (译者注: 这就是为什么不翻译那行注释的原因, 尽量保持原代码的原样)
34 exit 0
################################End Script#########################################
这是一个包含参数替换的here document的有用的脚本.
Example 17-6 上传一个文件对到"Sunsite"的incoming目录
################################Start Script#######################################
 1 #!/bin/bash
 2 # upload.sh
 3
 4 #  上传文件对(Filename.lsm, Filename.tar.gz)
 5 #+ 到Sunsite/UNC (ibiblio.org)的incoming目录.
 6 #  Filename.tar.gz是自身的tar包.
 7 #  Filename.lsm是描述文件.
 8 #  Sunsite需要"lsm"文件, 否则就拒绝贡献.
 9
10
11 E_ARGERROR=65
12
13 if [ -z "$1" ]
14 then
15   echo "Usage: `basename $0` Filename-to-upload"
16   exit $E_ARGERROR
17 fi 
18
19
20 Filename=`basename $1`           # 从文件名中去掉目录字符串.
21
22 Server="ibiblio.org"
23 Directory="/incoming/Linux"
24 #  在这里也不一定非得将上边的参数写死在这个脚本中,
25 #+ 可以使用命令行参数的方法来替换.
26
27 Password="your.e-mail.address"   # 可以修改成相匹配的密码.
28
29 ftp -n $Server <30 # -n 选项禁用自动登录.
31
32 user anonymous "$Password"
33 binary
34 bell                             # 在每个文件传输后, 响铃.
35 cd $Directory
36 put "$Filename.lsm"
37 put "$Filename.tar.gz"
38 bye
39 End-Of-Session
40
41 exit 0
################################End Script#########################################
在here document的开头引用或转义"limit string"会使得here document的消息体中的参数替
换被禁用.
Example 17-7 关闭参数替换
################################Start Script#######################################
 1 #!/bin/bash
 2 #  一个使用'cat'的here document, 但是禁用了参数替换.
 3
 4 NAME="John Doe"
 5 RESPONDENT="the author of this fine script" 
 6
 7 cat <<'Endofmessage'
 8
 9 Hello, there, $NAME.
10 Greetings to you, $NAME, from $RESPONDENT.
11
12 Endofmessage
13
14 #  当"limit string"被引用或转义那么就禁用了参数替换.
15 #  下边的两种方式具有相同的效果.
16 #  cat <<"Endofmessage"
17 #  cat <<\Endofmessage
18
19 exit 0
################################End Script#########################################
禁用了参数替换后, 将允许输出文本本身(译者注: 就是未转义的原文). 产生脚本甚至是程序
代码就是这种用法的用途之一.
Example 17-8 一个产生另外一个脚本的脚本
################################Start Script#######################################
 1 #!/bin/bash
 2 # generate-script.sh
 3 # 基于Albert Reiner的一个主意.
 4
 5 OUTFILE=generated.sh         # 所产生文件的名字.
 6
 7
 8 # -----------------------------------------------------------
 9 # 'Here document包含了需要产生的脚本的代码.
10 (
11 cat <<'EOF'
12 #!/bin/bash
13
14 echo "This is a generated shell script."
15 #  Note that since we are inside a subshell,
16 #+ we can't access variables in the "outside" script.
17
18 echo "Generated file will be named: $OUTFILE"
19 #  Above line will not work as normally expected
20 #+ because parameter expansion has been disabled.
21 #  Instead, the result is literal output.
22
23 a=7
24 b=3
25
26 let "c = $a * $b"
27 echo "c = $c"
28
29 exit 0
30 EOF
31 ) > $OUTFILE
32 # -----------------------------------------------------------
33
34 #  将'limit string'引用起来将会阻止上边
35 #+ here document的消息体中的变量扩展.
36 #  这会使得输出文件中的内容保持here document消息体中的原文.
37
38 if [ -f "$OUTFILE" ]
39 then
40   chmod 755 $OUTFILE
41   # 让所产生的文件具有可执行权限.
42 else
43   echo "Problem in creating file: \"$OUTFILE\""
44 fi
45
46 #  这个方法也用来产生
47 #+ C程序代码, Perl程序代码, Python程序代码, makefile,
48 #+ 和其他的一些类似的代码.
49 #  (译者注: 中间一段没译的注释将会被here document打印出来)
50 exit 0
################################End Script#########################################
也可以将here document的输出保存到变量中.
   1 variable=$(cat <   2 This variable
   3 runs over multiple lines.
   4 SETVAR)
   5
   6 echo "$variable"
同一脚本中的函数也可以接受here document的输出作为自身的参数.
Example 17-9 Here documents与函数
################################Start Script#######################################
 1 #!/bin/bash
 2 # here-function.sh
 3
 4 GetPersonalData ()
 5 {
 6   read firstname
 7   read lastname
 8   read address
 9   read city
10   read state
11   read zipcode
12 } # 这个函数无疑的看起来就一个交互函数, 但是...
13
14
15 # 给上边的函数提供输入.
16 GetPersonalData <17 Bozo
18 Bozeman
19 2726 Nondescript Dr.
20 Baltimore
21 MD
22 21226
23 RECORD001
24
25
26 echo
27 echo "$firstname $lastname"
28 echo "$address"
29 echo "$city, $state $zipcode"
30 echo
31
32 exit 0
################################End Script#########################################
也可以这么使用: 做一个假命令来从一个here document中接收输出. 这么做事实上就是创建了
一个"匿名"的here document.
Example 17-10 "匿名" here Document
################################Start Script#######################################
1 #!/bin/bash
2
3 : <4 ${HOSTNAME?}${USER?}${MAIL?}  # 如果其中一个变量没被设置, 那么就打印错误信息.
5 TESTVARIABLES
6
7 exit 0
################################End Script#########################################
注意: 上边所示技术的一种变化可以用来"注释"掉代码块.
Example 17-11 注释掉一段代码块
################################Start Script#######################################
 1 #!/bin/bash
 2 # commentblock.sh
 3
 4 : < 5 echo "This line will not echo."
 6 This is a comment line missing the "#" prefix.
 7 This is another comment line missing the "#" prefix.
 8
 9 &*@!!++=
10 The above line will cause no error message,
11 because the Bash interpreter will ignore it.
12 COMMENTBLOCK
13
14 echo "Exit value of above \"COMMENTBLOCK\" is $?."   # 0
15 # 这里将不会显示任何错误.
16
17
18 #  上边的这种技术当然也可以用来注释掉
19 #+ 一段正在使用的代码, 如果你有某些特定调试要求的话.
20 #  这将比对每行都敲入"#"来得方便的多,
21 #+ 而且如果你想恢复的话, 还得将添加上的"#"删除掉.
22
23 : <24 for file in *
25 do
26  cat "$file"
27 done
28 DEBUGXXX
29
30 exit 0
################################End Script#########################################
注意: 关于这种小技巧的另一个应用就是能够产生自文档化(self-documenting)的脚本.

Example 17-12 一个自文档化(self-documenting)的脚本
################################Start Script#######################################
 1 #!/bin/bash
 2 # self-document.sh: 自文档化(self-documenting)的脚本
 3 # Modification of "colm.sh".
 4
 5 DOC_REQUEST=70
 6
 7 if [ "$1" = "-h"  -o "$1" = "--help" ]     # 请求帮助.
 8 then
 9   echo; echo "Usage: $0 [directory-name]"; echo
10   sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATIONXX$/p' "$0" |
11   sed -e '/DOCUMENTATIONXX$/d'; exit $DOC_REQUEST; fi
12
13
14 : <15 List the statistics of a specified directory in tabular format.
16 ---------------------------------------------------------------
17 The command line parameter gives the directory to be listed.
18 If no directory specified or directory specified cannot be read,
19 then list the current working directory.
20
21 DOCUMENTATIONXX
22
23 if [ -z "$1" -o ! -r "$1" ]
24 then
25   directory=.
26 else
27   directory="$1"
28 fi 
29
30 echo "Listing of "$directory":"; echo
31 (printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \
32 ; ls -l "$directory" | sed 1d) | column -t
33
34 exit 0
################################End Script#########################################
使用cat 脚本 也能够完成相同的目的.
   1 DOC_REQUEST=70
   2
   3 if [ "$1" = "-h"  -o "$1" = "--help" ]     # 请求帮助.
   4 then                                       # 使用"cat 脚本" . . .
   5   cat <   6 List the statistics of a specified directory in tabular format.
   7 ---------------------------------------------------------------
   8 The command line parameter gives the directory to be listed.
   9 If no directory specified or directory specified cannot be read,
  10 then list the current working directory.
  11
  12 DOCUMENTATIONXX
  13 exit $DOC_REQUEST
  14 fi
参见 Example A-27 可以了解更多关于自文档化脚本的好例子.
注意: Here document创建临时文件, 但是这些文件将在打开后被删除, 并且不能够被任何其
 他进程所存取.
  bash$ bash -c 'lsof -a -p $$ -d0' << EOF
  > EOF
  lsof    1213 bozo    0r   REG    3,5    0 30386 /tmp/t1213-0-sh (deleted)
注意: 某些工具是不能工作在here document中的.
警告: 结束的limit string, 就是here document最后一行的limit string, 必须开始于第一
 个字符位置. 它的前面不能够有任何前置的空白. 而在这个limit string后边的空白也会
 引起异常问题. 空白将会阻止limit string的识别.(译者注: 下边这个脚本由于结束
 limit string的问题, 造成脚本无法结束, 所有内容全部被打印出来, 所以注释就不译了,
  保持例子脚本的原样.)
   1 #!/bin/bash
   2
   3 echo "----------------------------------------------------------------------"
   4
   5 cat <   6 echo "This is line 1 of the message inside the here document."
   7 echo "This is line 2 of the message inside the here document."
   8 echo "This is the final line of the message inside the here document."
   9      LimitString
  10 #^^^^Indented limit string. Error! This script will not behave as expected.
  11
  12 echo "----------------------------------------------------------------------"
  13
  14 #  These comments are outside the 'here document',
  15 #+ and should not echo.
  16
  17 echo "Outside the here document."
  18
  19 exit 0
  20
  21 echo "This line had better not echo."  # Follows an 'exit' command.
对于那些使用"here document"得非常复杂的任务, 最好考虑使用expect脚本语言, 这种语言
就是为了达到向交互程序添加输入的目的而量身定做的.

17.1. Here Strings
------------------
here string 可以被认为是here document的一种定制形式. 除了COMMAND <<<$WORD 就什么都
没有了, $WORD将被扩展并且被送入COMMAND的stdin中.
Example 17-13 在一个文件的开头添加文本
################################Start Script#######################################
 1 #!/bin/bash
 2 # prepend.sh: 在文件的开头添加文本.
 3 #
 4 #  Kenny Stauffer所捐助的脚本例子,
 5 #+ 被本文作者作了少量的修改.
 6
 7
 8 E_NOSUCHFILE=65
 9
10 read -p "File: " file   # 'read'命令的 -p 参数显示提示符.
11 if [ ! -e "$file" ]
12 then   # 如果没有这个文件那就进来.
13   echo "File $file not found."
14   exit $E_NOSUCHFILE
15 fi
16
17 read -p "Title: " title
18 cat - $file <<<$title > $file.new
19
20 echo "Modified file is $file.new"
21
22 exit 0
23
24 # 下边是'man bash'中的一段:
25 # Here Strings
26 #  here document的一种变形,形式如下:
27 #
28 #   <<29 #
30 #   word被扩展并且提供到command的标准输入中.
################################End Script#########################################
练习: 找出here string的其他用法.

第18章 休息时间 
================
这个神奇的暂停可以给读者一个休息的机会, 可能读者到了这里也会会心一笑吧.
Linux同志们, 向你们致敬! 你正在阅读的这些东西, 将会给你们带来好运. 把这份文档发给你
的10个朋友. 在拷贝这份文档之前, 在信的结尾写上一个100行的Bash脚本发送给列表上的第一
个人. 然后在信的底部删除它们的名字并添加你自己的名字.
不要打断这个链条! 并且在48小时之内完成它.
Brooklyn的Wilfred?P.没有成功的发送他的10个拷贝, 当他第2天早上醒来发现他的工作变成了
"COBOL 程序员". Newport?News的Howard?L.在一个月内才发出了他的10个拷贝, 这个时间足够
建立一个100个节点的Beowulf cluster来玩Tuxracer了. Chicago的Amelia?V.对这封信付之一
笑并且打断了这个链条, 不久之后, 她的终端爆炸了, 她现在花了好多天时间为MS Windows写
文档.
千万不要打断这个链条! 今天就把10个拷贝发出去!
阅读(2049) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~