Advanced Bash-Scripting Guide: An in-depth exploration of the art of shell scripting | ||
---|---|---|
Prev | Chapter 12. External Filters, Programs and Commands | Next |
这些工具通过用户指定的范围和增量来产生一系列的整数.
每个产生出来的整数一般都占一行, 但是可以使用 -s 选项来改变这种设置.
bash$ seq 5 1 2 3 4 5 bash$ seq -s : 5 1:2:3:4:5 |
jot 和 seq 命令都经常用在 for 循环中.
Example 12-49. 使用 seq 来产生循环参数
1 #!/bin/bash 2 # 使用 "seq" 3 4 echo 5 6 for a in `seq 80` # 或者 for a in $( seq 80 ) 7 # 与 " for a in 1 2 3 4 5 ... 80 "相同 (少敲了好多字!). 8 # 也可以使用 'jot' (如果系统上有的话). 9 do 10 echo -n "$a " 11 done # 1 2 3 4 5 ... 80 12 # 这也是一个通过使用命令的输出 13 # 来产生 "for"循环中 [list] 列表的例子. 14 15 echo; echo 16 17 18 COUNT=80 # 当然, 'seq' 也可以使用一个可替换的参数. 19 20 for a in `seq $COUNT` # 或者 for a in $( seq $COUNT ) 21 do 22 echo -n "$a " 23 done # 1 2 3 4 5 ... 80 24 25 echo; echo 26 27 BEGIN=75 28 END=80 29 30 for a in `seq $BEGIN $END` 31 # 传给 "seq" 两个参数, 从第一个参数开始增长, 32 #+ 一直增长到第二个参数为止. 33 do 34 echo -n "$a " 35 done # 75 76 77 78 79 80 36 37 echo; echo 38 39 BEGIN=45 40 INTERVAL=5 41 END=80 42 43 for a in `seq $BEGIN $INTERVAL $END` 44 # 传给 "seq" 三个参数从第一个参数开始增长, 45 #+ 并以第二个参数作为增量, 46 #+ 一直增长到第三个参数为止. 47 do 48 echo -n "$a " 49 done # 45 50 55 60 65 70 75 80 50 51 echo; echo 52 53 exit 0 |
一个简单些的例子:
1 # 产生10个连续扩展名的文件, 2 #+ 名字分别是 file.1, file.2 . . . file.10. 3 COUNT=10 4 PREFIX=file 5 6 for filename in `seq $COUNT` 7 do 8 touch $PREFIX.$filename 9 # 或者, 你可以做一些其他的操作, 10 #+ 比如 rm, grep, 等等. 11 done |
Example 12-50. 字母统计
1 #!/bin/bash 2 # letter-count.sh: 统计一个文本文件中字母出现的次数. 3 # 由 Stefano Palmeri 编写. 4 # 经过授权使用在本书中. 5 # 本书作者做了少许修改. 6 7 MINARGS=2 # 本脚本至少需要2个参数. 8 E_BADARGS=65 9 FILE=$1 10 11 let LETTERS=$#-1 # 制定了多少个字母 (作为命令行参数). 12 # (从命令行参数的个数中减1.) 13 14 15 show_help(){ 16 echo 17 echo Usage: `basename $0` file letters 18 echo Note: `basename $0` arguments are case sensitive. 19 echo Example: `basename $0` foobar.txt G n U L i N U x. 20 echo 21 } 22 23 # 检查参数个数. 24 if [ $# -lt $MINARGS ]; then 25 echo 26 echo "Not enough arguments." 27 echo 28 show_help 29 exit $E_BADARGS 30 fi 31 32 33 # 检查文件是否存在. 34 if [ ! -f $FILE ]; then 35 echo "File \"$FILE\" does not exist." 36 exit $E_BADARGS 37 fi 38 39 40 41 # 统计字母出现的次数. 42 for n in `seq $LETTERS`; do 43 shift 44 if [[ `echo -n "$1" | wc -c` -eq 1 ]]; then # 检查参数. 45 echo "$1" -\> `cat $FILE | tr -cd "$1" | wc -c` # 统计. 46 else 47 echo "$1 is not a single char." 48 fi 49 done 50 51 exit $? 52 53 # 这个脚本在功能上与 letter-count2.sh 完全相同, 54 #+ 但是运行得更快. 55 # 为什么? |
getopt 命令将会分析以破折号开头的命令行选项. 这个外部命令与Bash的内建命令 getopts 作用相同. 通过使用 -l 标志, getopt 可以处理长(多字符)选项, 并且也允许参数重置.
Example 12-51. 使用getopt来分析命令行选项
1 #!/bin/bash 2 # 使用 getopt. 3 4 # 尝试使用下边的不同的方法来调用这脚本: 5 # sh ex33a.sh -a 6 # sh ex33a.sh -abc 7 # sh ex33a.sh -a -b -c 8 # sh ex33a.sh -d 9 # sh ex33a.sh -dXYZ 10 # sh ex33a.sh -d XYZ 11 # sh ex33a.sh -abcd 12 # sh ex33a.sh -abcdZ 13 # sh ex33a.sh -z 14 # sh ex33a.sh a 15 # 解释上面每一次调用的结果. 16 17 E_OPTERR=65 18 19 if [ "$#" -eq 0 ] 20 then # 脚本需要至少一个命令行参数. 21 echo "Usage $0 -[options a,b,c]" 22 exit $E_OPTERR 23 fi 24 25 set -- `getopt "abcd:" "$@"` 26 # 为命令行参数设置位置参数. 27 # 如果使用 "$*" 来代替 "$@" 的话会发生什么? 28 29 while [ ! -z "$1" ] 30 do 31 case "$1" in 32 -a) echo "Option \"a\"";; 33 -b) echo "Option \"b\"";; 34 -c) echo "Option \"c\"";; 35 -d) echo "Option \"d\" $2";; 36 *) break;; 37 esac 38 39 shift 40 done 41 42 # 通常来说在脚本中使用内建的 'getopts' 命令, 43 #+ 会比使用 'getopt' 好一些. 44 # 参见 "ex33.sh". 45 46 exit 0 |
参见 Example 9-12 , 这是对 getopt 命令的一个简单模拟.
run-parts 命令 [1] 将会执行目标目录中所有的脚本, 这些将本会以 ASCII 的循序进行排列. 当然, 这些脚本都需要具有可执行权限.
yes 命令的默认行为是向 stdout 中连续不断的输出字符 y,每个y占一行.使用control-c来结束运行. 如果想换一个输出字符的话, 可以使用 yes 其他的字符串, 这样就会连续不同的输出你指定的字符串. 那么这样的命令究竟能做什么呢? 在命令行或者脚本中, yes的输出可以通过重定向或管道来传递给一些需要用户输入进行交互的命令. 事实上, 这个命令可以说是 expect 命令(译者注: 这个命令本书未介绍, 一个自动实现交互的命令)的一个简化版本.
yes | fsck /dev/hda1 将会以非交互的形式运行fsck(因为需要用户输入的 y 全由yes命令搞定了)(小心使用!).
yes | rm -r dirname 与 rm -rf dirname 效果相同(小心使用!).
将会把字符串用一个 ASCII 字符(默认是 '#')来画出来(就是将多个'#'拼出一副字符的图形).可以作为硬拷贝重定向到打印机上(译者注: 可以使用-w 选项设置宽度).
对于某个特定的用户, 显示出所有的 环境变量.
bash$ printenv | grep HOME HOME=/home/bozo |
lp 和 lpr 命令将会把文件发送到打印队列中, 并且作为硬拷贝来打印. [2] 这些命令会纪录它们名字的起始位置并传递到行打印机的另一个位置.<rojy bug>
bash$ lp file1.txt 或者 bash lp <file1.txt
通常情况下都是将pr的格式化的输出传递到 lp.
bash$ pr -options file1.txt | lp
格式化的包, 比如 groff 和 Ghostscript 就可以将它们的输出直接发送给 lp.
bash$ groff -Tascii file.tr | lp
bash$ gs -options | lp file.ps
还有一些相关的命令, 比如 lpq, 可以查看打印队列, lprm, 可以用来从打印队列中删除作业.
[UNIX 从管道行业借来的主意.]
这是一个重定向操作, 但是有些不同. 就像管道中的"三通"一样, 这个命令可以将命令或者管道命令的输出抽出到一个文件中,而且并不影响结果. 当你想将一个正在运行的进程的输出保存到文件中时, 或者为了debug而保存输出记录的时候, 这个命令就非常有用了.
(重定向) |----> to file | ==========================|==================== command ---> command ---> |tee ---> command ---> ---> output of pipe =============================================== |
1 cat listfile* | sort | tee check.file | uniq > result.file |
这个不大引人注意的命令可以创建一个命名管道, 并产生一个临时的先进先出的buffer用来在两个进程间传输数据. [3] 典型的使用是一个进程向FIFO中写数据, 另一个进程读出来. 参见 Example A-15.
这个命令用来检查文件名的有效性. 如果文件名超过了最大允许长度(255 个字符), 或者它所在的一个或多个路径搜索不到, 那么就会产生一个错误结果.
不幸的是,并不能够返回一个可识别的错误码, 因此它在脚本中几乎没有什么用. 一般都使用文件测试操作.
这也是一个不太出名的工具, 但却是一个令人恐惧的 "数据复制" 命令. 最开始, 这个命令是被用来在UNIX 微机和IBM大型机之间通过磁带来交换数据, 这个命令现在仍然有它的用途. dd 命令只不过是简单的拷贝一个文件 (或者 stdin/stdout), 但是它会做一些转换. 下边是一些可能的转换, 比如 ASCII/EBCDIC, [4] 大写/小写, 在输入和输出之间的字节对的交换, 还有对输入文件做一些截头去尾的工作. dd --help 列出了所有转换, 还有这个强力工具的一些其他选项.
1 # 将一个文件转换为大写: 2 3 dd if=$filename conv=ucase > $filename.uppercase 4 # lcase # 转换为小写 |
Example 12-52. 一个拷贝自身的脚本
1 #!/bin/bash 2 # self-copy.sh 3 4 # 这个脚本将会拷贝自身. 5 6 file_subscript=copy 7 8 dd if=$0 of=$0.$file_subscript 2>/dev/null 9 # 阻止dd产生的消息: ^^^^^^^^^^^ 10 11 exit $? |
Example 12-53. 练习 dd
1 #!/bin/bash 2 # exercising-dd.sh 3 4 # 由Stephane Chazelas编写. 5 # 本文作者做了少量修改. 6 7 input_file=$0 # 脚本本身. 8 output_file=log.txt 9 n=3 10 p=5 11 12 dd if=$input_file of=$output_file bs=1 skip=$((n-1)) count=$((p-n+1)) 2> /dev/null 13 # 从脚本中把位置n到p的字符提取出来. 14 15 # ------------------------------------------------------- 16 17 echo -n "hello world" | dd cbs=1 conv=unblock 2> /dev/null 18 # 垂直的 echo "hello world" . 19 20 exit 0 |
为了展示dd的多种用途, 让我们使用它来记录按键.
Example 12-54. 记录按键
1 #!/bin/bash 2 # dd-keypress.sh: 记录按键, 不需要按回车. 3 4 5 keypresses=4 # 记录按键的个数. 6 7 8 old_tty_setting=$(stty -g) # 保存老的终端设置. 9 10 echo "Press $keypresses keys." 11 stty -icanon -echo # 禁用标准模式. 12 # 禁用本地 echo. 13 keys=$(dd bs=1 count=$keypresses 2> /dev/null) 14 # 如果不指定输入文件的话, 'dd' 使用标准输入. 15 16 stty "$old_tty_setting" # 恢复老的终端设置. 17 18 echo "You pressed the \"$keys\" keys." 19 20 # 感谢 Stephane Chazelas, 演示了这种方法. 21 exit 0 |
dd 命令可以在数据流上做随即存取.
1 echo -n . | dd bs=1 seek=4 of=file conv=notrunc 2 # "conv=notrunc" 选项意味着输出文件不能被截短. 3 4 # Thanks, S.C. |
dd 命令可以将数据或磁盘镜像拷贝到设备中, 也可以从设备中拷贝数据或磁盘镜像, 比如说磁盘或磁带设备都可以 (Example A-5). 通常用来创建启动盘.
dd if=kernel-image of=/dev/fd0H1440
同样的, dd 可以拷贝软盘的整个内容(甚至是其他操作系统的磁盘格式) 到硬盘驱动器上(以镜像文件的形式).
dd if=/dev/fd0 of=/home/bozo/projects/floppy.img
dd 命令还有一些其他用途, 包括可以初始化临时交换文件 (Example 28-2) 和 ramdisks(内存虚拟硬盘) (Example 28-3). 它甚至可以做一些对整个硬盘分区的底层拷贝, 虽然不建议这么做.
一些(可能是比较无聊的)人总会想一些关于 dd 命令的有趣的应用.
Example 12-55. 安全的删除一个文件
1 #!/bin/bash 2 # blot-out.sh: 删除一个文件所有的记录. 3 4 # 这个脚本会使用随即字节交替的覆盖 5 #+ 目标文件, 并且在最终删除这个文件之前清零. 6 # 这么做之后, 即使你通过传统手段来检查磁盘扇区 7 #+ 也不能把文件原始数据重新恢复. 8 9 PASSES=7 # 破坏文件的次数. 10 # 提高这个数字会减慢脚本运行的速度, 11 #+ 尤其是对尺寸比较大的目标文件进行操作的时候. 12 BLOCKSIZE=1 # 带有 /dev/urandom 的 I/O 需要单位块尺寸, 13 #+ 否则你可能会获得奇怪的结果. 14 E_BADARGS=70 # 不同的错误退出码. 15 E_NOT_FOUND=71 16 E_CHANGED_MIND=72 17 18 if [ -z "$1" ] # 没指定文件名. 19 then 20 echo "Usage: `basename $0` filename" 21 exit $E_BADARGS 22 fi 23 24 file=$1 25 26 if [ ! -e "$file" ] 27 then 28 echo "File \"$file\" not found." 29 exit $E_NOT_FOUND 30 fi 31 32 echo; echo -n "Are you absolutely sure you want to blot out \"$file\" (y/n)? " 33 read answer 34 case "$answer" in 35 [nN]) echo "Changed your mind, huh?" 36 exit $E_CHANGED_MIND 37 ;; 38 *) echo "Blotting out file \"$file\".";; 39 esac 40 41 42 flength=$(ls -l "$file" | awk '{print $5}') # 5 是文件长度. 43 pass_count=1 44 45 chmod u+w "$file" # Allow overwriting/deleting the file. 46 47 echo 48 49 while [ "$pass_count" -le "$PASSES" ] 50 do 51 echo "Pass #$pass_count" 52 sync # 刷新buffer. 53 dd if=/dev/urandom of=$file bs=$BLOCKSIZE count=$flength 54 # 使用随机字节进行填充. 55 sync # 再刷新buffer. 56 dd if=/dev/zero of=$file bs=$BLOCKSIZE count=$flength 57 # 用0填充. 58 sync # 再刷新buffer. 59 let "pass_count += 1" 60 echo 61 done 62 63 64 rm -f $file # 最后, 删除这个已经被破坏得不成样子的文件. 65 sync # 最后一次刷新buffer. 66 67 echo "File \"$file\" blotted out and deleted."; echo 68 69 70 exit 0 71 72 # 这是一种真正安全的删除文件的办法, 73 #+ 但是效率比较低, 运行比较慢. 74 # GNU 的文件工具包中的 "shred" 命令, 75 #+ 也可以完成相同的工作, 不过更有效率. 76 77 # 使用普通的方法是不可能重新恢复这个文件了. 78 # 然而 . . . 79 #+ 这个简单的例子是不能够抵抗 80 #+ 那些经验丰富并且正规的分析. 81 82 # 这个脚本可能不会很好的运行在日志文件系统上.(译者注: JFS) 83 # 练习 (很难): 像它做的那样修正这个问题. 84 85 86 87 # Tom Vier的文件删除包可以更加彻底 88 #+ 的删除文件, 比这个简单的例子厉害得多. 89 # http://www.ibiblio.org/pub/Linux/utils/file/wipe-2.0.0.tar.bz2 90 91 # 如果想对安全删除文件这一论题进行深度的分析, 92 #+ 可以参见Peter Gutmann的页面, 93 #+ "Secure Deletion of Data From Magnetic and Solid-State Memory". 94 # http://www.cs.auckland.ac.nz/~pgut001/pubs/secure_del.html |
od(octal dump)过滤器, 将会把输入(或文件)转换为8进制或者其他进制. 在你需要查看或处理一些二进制数据文件或者一个不可读的系统设备文件的时候, 这个命令非常有用, 比如/dev/urandom,或者是一个二进制数据过滤器. 参见 Example 9-28 和 Example 12-13.
对二进制文件进行 16进制, 8进制, 10进制, 或者 ASCII 码的查阅动作. 这个命令大体上与上边的od命令作用相同, 但是远不及 od 命令有用.
显示编译后的2进制文件或2进制可执行文件的信息, 以16进制的形式显示, 或者显示反汇编列表(使用-d选项).
bash$ objdump -d /bin/ls /bin/ls: file format elf32-i386 Disassembly of section .init: 080490bc <.init>: 80490bc: 55 push %ebp 80490bd: 89 e5 mov %esp,%ebp . . . |
这个命令会产生一个"magic cookie", 这是一个128-bit (32-字符) 的伪随机16进制数字, 这个数字一般都用来作为X server的鉴权"签名". 这个命令还可以用来在脚本中作为一种生成随机数的手段, 当然这是一种"小吃店"(虽然不太正统, 但是很方便)的风格.
1 random000=$(mcookie) |
当然, 完成同样的目的还可以使用 md5 命令.
1 # 产生关于脚本本身的 md5 checksum. 2 random001=`md5sum $0 | awk '{print $1}'` 3 # 使用 'awk' 来去掉文件名. |
mcookie 还给出了产生"唯一"文件名的另一种方法.
Example 12-56. 文件名产生器
1 #!/bin/bash 2 # tempfile-name.sh: 临时文件名产生器 3 4 BASE_STR=`mcookie` # 32-字符的 magic cookie. 5 POS=11 # 字符串中随便的一个位置. 6 LEN=5 # 取得 $LEN 长度连续的字符串. 7 8 prefix=temp # 最终的一个临时文件. 9 # 如果想让这个文件更加唯一, 10 #+ 可以对这个前缀也使用下边的方法来生成. 11 12 suffix=${BASE_STR:POS:LEN} 13 # 提取从第11个字符之后的长度为5的字符串. 14 15 temp_filename=$prefix.$suffix 16 # 构造文件名. 17 18 echo "Temp filename = "$temp_filename"" 19 20 # sh tempfile-name.sh 21 # Temp filename = temp.e19ea 22 23 # 与使用 'date' 命令(参考 ex51.sh)来创建唯一文件名 24 #+ 的方法相比较. 25 26 exit 0 |
这个工具用来在不同的计量单位之间互相转换. 当你在交互模式下正常调用时, 会发现在脚本中 units 也很有用.
Example 12-57. 将米转换为英里
1 #!/bin/bash 2 # unit-conversion.sh 3 4 5 convert_units () # 通过参数取得需要转换的单位. 6 { 7 cf=$(units "$1" "$2" | sed --silent -e '1p' | awk '{print $2}') 8 # 除了真正需要转换的部分保留下来外,其他的部分都去掉. 9 echo "$cf" 10 } 11 12 Unit1=miles 13 Unit2=meters 14 cfactor=`convert_units $Unit1 $Unit2` 15 quantity=3.73 16 17 result=$(echo $quantity*$cfactor | bc) 18 19 echo "There are $result $Unit2 in $quantity $Unit1." 20 21 # 如果你传递了两个不匹配的单位会发生什么? 22 #+ 比如分别传入英亩和英里? 23 24 exit 0 |
一个隐藏的财宝, m4 是一个强力的宏处理过滤器, [5] 差不多可以说是一种语言了. 虽然最开始这个工具是用来作为 RatFor 的预处理器而编写的, 但是后来证明 m4 作为独立的工具也是非常有用的. 事实上, m4 结合了许多工具的功能, 比如 eval, tr, 和 awk, 除此之外, 它还使得宏扩展变得容易.
在 2004年4月的 Linux Journal 的问题列表中有一篇关于 m4 命令用法得非常好的文章.
doexec 命令允许将一个随便的参数列表传递到一个二进制可执行文件中. 特别的, 甚至可以传递 arg[0] (相当于脚本中的 $0 ), 这样可以使用不同的名字来调用这个可执行文件, 并且通过不同的调用的名字, 可以让这个可执行文件执行不同的动作. 这也可以说是一种将参数传递到可执行文件中的比较绕圈子的做法.
比如, /usr/local/bin 目录可能包含一个 "aaa" 的二进制文件. 使用 doexec /usr/local/bin/aaa list 可以 列出 当前工作目录下所有以 "a" 开头的的文件, 而使用 doexec /usr/local/bin/aaa delete 将会删除这些文件.
可执行文件的不同行为必须定义在可执行文件自身的代码中, 可以使用如下的shell脚本作类比:
|
dialog 工具集提供了一种从脚本中调用交互对话框的方法. dialog 的更好的变种版本是 -- gdialog, Xdialog, 和 kdialog -- 事实上是调用的 X-Windows 的界面工具集. 参见 Example 33-19.
sox 命令, "sound exchange" (声音转换)命令, 可以进行声音文件的转换. 事实上,可执行文件 /usr/bin/play (现在不建议使用) 只不过是 sox 的一个 shell 包装器而已.
举个例子, sox soundfile.wav soundfile.au 将会把一个 WAV 声音文件转换成一个 (Sun 音频格式) AU 声音文件.
Shell 脚本非常适合于使用 sox 的声音操作来批处理声音文件. 比如, 参见 Linux Radio Timeshift HOWTO 和 MP3do Project.
[1] | 这个工具事实上是从 Debian Linux 发行版中的一个脚本借鉴过来的. | |
[2] | 打印队列 就是"在线等待"打印的作业组. | |
[3] | 对于本话题的一个完美的介绍, 请参见 Andy Vaught 的文章, 命名管道的介绍, (http://www2.linuxjournal.com/lj-issues/issue41/2156.html), 这是Linux Journal (http://www.linuxjournal.com/)1997年9月的一个问题. | |
[4] | EBCDIC (发音是 "ebb-sid-ick") 是单词 (Extended Binary Coded Decimal Interchange Code) 的首字母缩写. 这是 IBM 的数据格式, 现在已经不常见了. dd 命令的 conv=ebcdic 选项的一个比较奇异的使用方法是对一个文件进行快速而且容易但不太安全的编码.
| |
[5] | 宏 是一个符号常量, 将会被扩展成一个命令字符串或者一系列的参数操作. |