魅力博客

魅力Linux|魅力空间|魅力博客|学习Linux|ubuntu日记|电脑教程|手机软件

深入学习shell脚本艺术-高级Bash脚本编程指南10



作业控制
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上, http://www.linuxgazette.com.
 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包的一部分(www.procmail.org). 它可以创建一个锁定文件, 锁定文
    件是一种用来控制存取文件, 设备或资源的标记文件. 锁定文件就像一个标记一样被使用,
     如果特定的文件, 设备, 或资源正在被一个特定的进程所使用("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<rojy bug>
    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" 剧情.<rojy bug>(译者注:巫师的厢房?没看懂)
          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 <filename
 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 <vojtech@suse.cz>"
     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'脚本包的一部分, <miquels@drinkel.nl.mugnet.org>.
 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=`<file1`      #  将"file1"的内容放到"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 <stdio.h>
   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 #  http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz
 8 #  http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz
 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 <filename
  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 <filename 命令会将stdin重定向到文件中. 从这句开始, 后边的输入就都来自于这个文
件了, 而不是标准输入了(通常都是键盘输入). 这样就提供了一种按行读取文件的方法, 并且
可以使用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 <<InputComesFromHERE
   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 <<LimitString
   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 <<zzz23EndOfMessagezzz23
 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 <message-file
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 <<x23LimitStringx23
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 <Esc>就行,
28 #+ 事实上它是<Esc>键.
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 <<EOF
  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 <<End-of-message
 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 <<End-of-message
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 <<Endofmessage
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 <<End-Of-Session
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 <<SETVAR
   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 <<RECORD001
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 : <<TESTVARIABLES
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 : <<COMMENTBLOCK
 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 : <<DEBUGXXX
24 for file in *
25 do
26  cat "$file"
27 done
28 DEBUGXXX
29
30 exit 0
################################End Script#########################################

注意: 关于这种小技巧的另一个应用就是能够产生自文档化(self-documenting)的脚本.

返回顶部

发表评论:

Powered By Z-BlogPHP 1.7.3


知识共享许可协议
本作品采用知识共享署名 3.0 中国大陆许可协议进行许可。
网站备案号粤ICP备15104741号-1