12.2. 复杂命令

更高级的用户命令

find

-exec COMMAND \;

在每一个find 匹配到的文件执行 COMMAND 命令. 命令序列以 ; 结束( ";"转义符 以保证 shell 传递到find命令中的字符不会被解释为其他的特殊字符).

 bash$ find ~/ -name '*.txt'
 /home/bozo/.kde/share/apps/karm/karmdata.txt
 /home/bozo/misc/irmeyc.txt
 /home/bozo/test-scripts/1.txt
 	      

如果 COMMAND 中包含 {}, 那么 find 命令将会用所有匹配文件的路径名来替换 "{}" .

   1 find ~/ -name 'core*' -exec rm {} \;
   2 # 从用户的 home 目录中删除所有的 core dump文件.

   1 find /home/bozo/projects -mtime 1
   2 #  列出最后一天被修改的
   3 #+ 在/home/bozo/projects目录树下的所有文件.
   4 #
   5 #  mtime = last modification time of the target file
   6 #  ctime = last status change time (via 'chmod' or otherwise)
   7 #  atime = last access time
   8 
   9 DIR=/home/bozo/junk_files
  10 find "$DIR" -type f -atime +5 -exec rm {} \;
  11 #                                      ^^
  12 #  大括号就是"find"命令用来替换目录的地方.
  13 #
  14 #  删除至少5天内没被存取过的
  15 #+ "/home/bozo/junk_files" 中的所有文件.
  16 #
  17 #  "-type filetype", where
  18 #  f = regular file
  19 #  d = directory, etc.
  20 #  ('find' 命令的 man页有完整的选项列表.)

   1 find /etc -exec grep '[0-9][0-9]*[.][0-9][0-9]*[.][0-9][0-9]*[.][0-9][0-9]*' {} \;
   2 
   3 # 在/etc 目录中的文件找到所所有包含 IP 地址(xxx.xxx.xxx.xxx) 的文件.
   4 # 可能会查找到一些多余的匹配. 我们如何去掉它们呢?
   5 
   6 # 或许可以使用如下方法:
   7 
   8 find /etc -type f -exec cat '{}' \; | tr -c '.[:digit:]' '\n' \
   9 | grep '^[^.][^.]*\.[^.][^.]*\.[^.][^.]*\.[^.][^.]*$'
  10 #
  11 #  [:digit:] 是一种字符类.is one of the character classes
  12 #+ 关于字符类的介绍见 POSIX 1003.2 标准化文档. 
  13 
  14 # Thanks, Stéphane Chazelas. 

Note

find 命令的 -exec 选项不应该与shell中的内建命令 exec 相混淆.


Example 12-3. 删除当前目录下文件名中包含一些特殊字符(包括空白)的文件..

   1 #!/bin/bash
   2 # badname.sh
   3 # 删除当前目录下文件名中包含一些特殊字符的文件.
   4 
   5 for filename in *
   6 do
   7   badname=`echo "$filename" | sed -n /[\+\{\;\"\\\=\?~\(\)\<\>\&\*\|\$]/p`
   8 # badname=`echo "$filename" | sed -n '/[+{;"\=?~()<>&*|$]/p'`  这句也行.
   9 # 删除文件名包含这些字符的文件:     + { ; " \ = ? ~ ( ) < > & * | $
  10 #
  11   rm $badname 2>/dev/null
  12 #             ^^^^^^^^^^^ 错误消息将被抛弃.
  13 done
  14 
  15 # 现在, 处理文件名中以任何方式包含空白的文件.
  16 find . -name "* *" -exec rm -f {} \;
  17 # "find"命令匹配到的目录名将替换到{}的位置.
  18 # '\' 是为了保证 ';'被正确的转义, 并且放到命令的结尾.
  19 
  20 exit 0
  21 
  22 #---------------------------------------------------------------------
  23 # 这行下边的命令将不会运行, 因为 "exit" 命令.
  24 
  25 # 这句是上边脚本的一个可选方法:
  26 find . -name '*[+{;"\\=?~()<>&*|$ ]*' -exec rm -f '{}' \;
  27 # (Thanks, S.C.)


Example 12-4. 通过文件的 inode 号来删除文件

   1 #!/bin/bash
   2 # idelete.sh: 通过文件的inode号来删除文件.
   3 
   4 #  当文件名以一个非法字符开头的时候, 这就非常有用了,
   5 #+ 比如 ? 或 -.
   6 
   7 ARGCOUNT=1                      # 文件名参数必须被传递到脚本中.
   8 E_WRONGARGS=70
   9 E_FILE_NOT_EXIST=71
  10 E_CHANGED_MIND=72
  11 
  12 if [ $# -ne "$ARGCOUNT" ]
  13 then
  14   echo "Usage: `basename $0` filename"
  15   exit $E_WRONGARGS
  16 fi  
  17 
  18 if [ ! -e "$1" ]
  19 then
  20   echo "File \""$1"\" does not exist."
  21   exit $E_FILE_NOT_EXIST
  22 fi  
  23 
  24 inum=`ls -i | grep "$1" | awk '{print $1}'`
  25 # inum = inode (索引节点) 号.
  26 # --------------------------------------------------------
  27 # 每个文件都有一个inode号, 这个号用来记录文件物理地址信息.
  28 # --------------------------------------------------------
  29 
  30 echo; echo -n "Are you absolutely sure you want to delete \"$1\" (y/n)? "
  31 # 'rm' 命令的 '-v' 选项也会问这句话.
  32 read answer
  33 case "$answer" in
  34 [nN]) echo "Changed your mind, huh?"
  35       exit $E_CHANGED_MIND
  36       ;;
  37 *)    echo "Deleting file \"$1\".";;
  38 esac
  39 
  40 find . -inum $inum -exec rm {} \;
  41 #                           ^^
  42 #        大括号就是"find"命令
  43 #+       用来替换文本输出的地方.
  44 echo "File "\"$1"\" deleted!"
  45 
  46 exit 0

Example 12-27, Example 3-4, 和 Example 10-9 这些例子展示了使用 find 命令. 对于这个复杂而有强大的命令来说, 查看man页可以获得更多的细节.

xargs

这是给命令传递参数的一个过滤器, 也是组合多个命令的一个工具.它把一个数据流分割为一些足够小的块, 以方便过滤器和命令进行处理. 由此这个命令也是后置引用的一个强有力的替换. 在一般使用过多参数的命令替换失败的时候,用xargs 来替换它一般都能成功. [1] 通常情况下, xargs 从管道或者stdin中读取数据, 但是它也能够从文件的输出中读取数据.

xargs的默认命令是 echo. 这意味着通过管道传递给xargs的输入将会包含换行和空白, 不过通过xargs的处理, 换行和空白将被空格取代.
 bash$ ls -l
 total 0
 -rw-rw-r--    1 bozo  bozo         0 Jan 29 23:58 file1
 -rw-rw-r--    1 bozo  bozo         0 Jan 29 23:58 file2
 
 
 
 bash$ ls -l | xargs
 total 0 -rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58 file1 -rw-rw-r-- 1 bozo bozo 0 Jan 29 23:58 file2
 
 
 
 bash$ find ~/mail -type f | xargs grep "Linux"
 ./misc:User-Agent: slrn/0.9.8.1 (Linux)
 ./sent-mail-jul-2005: hosted by the Linux Documentation Project.
 ./sent-mail-jul-2005: (Linux Documentation Project Site, rtf version)
 ./sent-mail-jul-2005: Subject: Criticism of Bozo's Windows/Linux article
 ./sent-mail-jul-2005: while mentioning that the Linux ext2/ext3 filesystem
 . . .
 	      

ls | xargs -p -l gzip 使用gzips 压缩当前目录下的每个文件, 一次压缩一个, 并且在每次压缩前都提示用户.

Tip

一个有趣的 xargs 选项是 -n NN, NN 是限制每次传递进来参数的个数.

ls | xargs -n 8 echo 以每行8列的形式列出当前目录下的所有文件.

Tip

另一个有用的选项是 -0, 使用 find -print0grep -lZ 这两种组合方式. 这允许处理包含空白或引号的参数.

find / -type f -print0 | xargs -0 grep -liwZ GUI | xargs -0 rm -f

grep -rliwZ GUI / | xargs -0 rm -f

上边两行都可用来删除任何包含 "GUI" 的文件. (Thanks, S.C.)


Example 12-5. Logfile: 使用 xargs 来监控系统 log

   1 #!/bin/bash
   2 
   3 # 从 /var/log/messagesGenerates 的尾部开始
   4 # 产生当前目录下的一个lof 文件.
   5 
   6 # 注意: 如果这个脚本被一个一般用户调用的话,
   7 # /var/log/messages 必须是全部可读的.
   8 #         #root chmod 644 /var/log/messages
   9 
  10 LINES=5
  11 
  12 ( date; uname -a ) >>logfile
  13 # 时间和机器名
  14 echo --------------------------------------------------------------------- >>logfile
  15 tail -$LINES /var/log/messages | xargs |  fmt -s >>logfile
  16 echo >>logfile
  17 echo >>logfile
  18 
  19 exit 0
  20 
  21 #  注意:
  22 #  -----
  23 #  像 Frank Wang 所指出,
  24 #+ 在原文件中的任何不匹配的引号(包括单引号和双引号)
  25 #+ 都会给xargs造成麻烦.
  26 #
  27 #  他建议使用下边的这行来替换上边的第15行:
  28 #     tail -$LINES /var/log/messages | tr -d "\"'" | xargs | fmt -s >>logfile
  29 
  30 
  31 
  32 #  练习:
  33 #  -----
  34 #  修改这个脚本, 使得这个脚本每个20分钟
  35 #+ 就跟踪一下 /var/log/messages 的修改记录.
  36 #  提示: 使用 "watch" 命令. 

在find命令中, 一对大括号就一个文本替换的位置.


Example 12-6. 把当前目录下的文件拷贝到另一个文件中

   1 #!/bin/bash
   2 # copydir.sh
   3 
   4 #  拷贝 (verbose) 当前目录($PWD)下的所有文件到
   5 #+ 命令行中指定的另一个目录下.
   6 
   7 E_NOARGS=65
   8 
   9 if [ -z "$1" ]   # 如果没有参数传递进来那就退出.
  10 then
  11   echo "Usage: `basename $0` directory-to-copy-to"
  12   exit $E_NOARGS
  13 fi  
  14 
  15 ls . | xargs -i -t cp ./{} $1
  16 #            ^^ ^^      ^^
  17 #  -t 是 "verbose" (输出命令行到stderr) 选项.
  18 #  -i 是"替换字符串"选项.
  19 #  {} 是输出文本的替换点.
  20 #  这与在"find"命令中使用{}的情况很相像.
  21 #
  22 #  列出当前目录下的所有文件(ls .),
  23 #+ 将 "ls" 的输出作为参数传递到 "xargs"(-i -t 选项) 中,
  24 #+ 然后拷贝(cp)这些参数({})到一个新目录中($1).
  25 #
  26 #  最终的结果和下边的命令等价,
  27 #+   cp * $1
  28 #+ 除非有文件名中嵌入了"空白"字符.
  29 
  30 exit 0


Example 12-7. 通过名字Kill进程

   1 #!/bin/bash
   2 # kill-byname.sh: 通过名字kill进程.
   3 # 与脚本kill-process.sh相比较.
   4 
   5 #  例如,
   6 #+ 试一下 "./kill-byname.sh xterm" --
   7 #+ 并且查看你系统上的所有xterm都将消失.
   8 
   9 #  警告:
  10 #  -----
  11 #  这是一个非常危险的脚本.
  12 #  运行它的时候一定要小心. (尤其是以root身份运行时)
  13 #+ 因为运行这个脚本可能会引起数据丢失或产生其他一些不好的效果.
  14 
  15 E_BADARGS=66
  16 
  17 if test -z "$1"  # 没有参数传递进来?
  18 then
  19   echo "Usage: `basename $0` Process(es)_to_kill"
  20   exit $E_BADARGS
  21 fi
  22 
  23 
  24 PROCESS_NAME="$1"
  25 ps ax | grep "$PROCESS_NAME" | awk '{print $1}' | xargs -i kill {} 2&>/dev/null
  26 #                                                       ^^      ^^
  27 
  28 # -----------------------------------------------------------
  29 # 注意:
  30 # -i 参数是xargs命令的"替换字符串"选项.
  31 # 大括号对的地方就是替换点.
  32 # 2&>/dev/null 将会丢弃不需要的错误消息.
  33 # -----------------------------------------------------------
  34 
  35 exit $?


Example 12-8. 使用xargs分析单词出现的频率

   1 #!/bin/bash
   2 # wf2.sh: Crude word frequency analysis on a text file.
   3 
   4 # 使用 'xargs' 将文本行分解为单词.
   5 # 于后边的 "wf.sh" 脚本相比较.
   6 
   7 
   8 # 检查命令行上输入的文件.
   9 ARGS=1
  10 E_BADARGS=65
  11 E_NOFILE=66
  12 
  13 if [ $# -ne "$ARGS" ]
  14 # 纠正传递到脚本中的参数个数?
  15 then
  16   echo "Usage: `basename $0` filename"
  17   exit $E_BADARGS
  18 fi
  19 
  20 if [ ! -f "$1" ]       # 检查文件是否存在.
  21 then
  22   echo "File \"$1\" does not exist."
  23   exit $E_NOFILE
  24 fi
  25 
  26 
  27 
  28 #################################################################
  29 cat "$1" | xargs -n1 | \
  30 #  列出文件, 每行一个单词.
  31 tr A-Z a-z | \
  32 #  将字符转换为小写.
  33 sed -e 's/\.//g'  -e 's/\,//g' -e 's/ /\
  34 /g' | \
  35 #  过滤掉句号和逗号,
  36 #+ 并且将单词间的空格修改为换行,
  37 sort | uniq -c | sort -nr
  38 #  最后统计出现次数,把数字显示在第一列,然后显示单词,并按数字排序.
  39 #################################################################
  40 
  41 #  这个例子的作用与"wf.sh"的作用是一样的,
  42 #+ 但是这个例子比较臃肿, 并且运行起来更慢一些(为什么?).
  43 
  44 exit 0

expr

通用求值表达式: 通过给定的操作(参数必须以空格分开)连接参数,并对参数求值.可以使算术操作, 比较操作, 字符串操作或者是逻辑操作.

expr 3 + 5

返回 8

expr 5 % 3

返回 2

expr 1 / 0

返回错误消息, expr: division by zero

不允许非法的算术操作.

expr 5 \* 3

返回 15

在算术表达式expr中使用乘法操作时, 乘法符号必须被转义.

y=`expr $y + 1`

增加变量的值, 与 let y=y+1 y=$(($y+1)) 的效果相同. 这是使用算术表达式的一个例子.

z=`expr substr $string $position $length`

在位置$position上提取$length长度的子串.


Example 12-9. 使用 expr

   1 #!/bin/bash
   2 
   3 # 展示一些 'expr'的使用
   4 # =====================
   5 
   6 echo
   7 
   8 # 算术 操作
   9 # ---- ----
  10 
  11 echo "Arithmetic Operators"
  12 echo
  13 a=`expr 5 + 3`
  14 echo "5 + 3 = $a"
  15 
  16 a=`expr $a + 1`
  17 echo
  18 echo "a + 1 = $a"
  19 echo "(incrementing a variable)"
  20 
  21 a=`expr 5 % 3`
  22 # 取模操作
  23 echo
  24 echo "5 mod 3 = $a"
  25 
  26 echo
  27 echo
  28 
  29 # 逻辑 操作
  30 # ---- ----
  31 
  32 #  true返回 1 ,false 返回 0 ,
  33 #+ 而Bash的使用惯例则相反.
  34 
  35 echo "Logical Operators"
  36 echo
  37 
  38 x=24
  39 y=25
  40 b=`expr $x = $y`         # 测试相等.
  41 echo "b = $b"            # 0  ( $x -ne $y )
  42 echo
  43 
  44 a=3
  45 b=`expr $a \> 10`
  46 echo 'b=`expr $a \> 10`, therefore...'
  47 echo "If a > 10, b = 0 (false)"
  48 echo "b = $b"            # 0  ( 3 ! -gt 10 )
  49 echo
  50 
  51 b=`expr $a \< 10`
  52 echo "If a < 10, b = 1 (true)"
  53 echo "b = $b"            # 1  ( 3 -lt 10 )
  54 echo
  55 # Note escaping of operators.
  56 
  57 b=`expr $a \<= 3`
  58 echo "If a <= 3, b = 1 (true)"
  59 echo "b = $b"            # 1  ( 3 -le 3 )
  60 # 也有 "\>=" 操作 (大于等于).
  61 
  62 
  63 echo
  64 echo
  65 
  66 
  67 
  68 # 字符串 操作
  69 # ------ ----
  70 
  71 echo "String Operators"
  72 echo
  73 
  74 a=1234zipper43231
  75 echo "The string being operated upon is \"$a\"."
  76 
  77 # 长度: 字符串长度
  78 b=`expr length $a`
  79 echo "Length of \"$a\" is $b."
  80 
  81 # 索引: 从字符串的开头查找匹配的子串,
  82 #       并取得第一个匹配子串的位置.
  83 b=`expr index $a 23`
  84 echo "Numerical position of first \"2\" in \"$a\" is \"$b\"."
  85 
  86 # substr: 从指定位置提取指定长度的字串.
  87 b=`expr substr $a 2 6`
  88 echo "Substring of \"$a\", starting at position 2,\
  89 and 6 chars long is \"$b\"."
  90 
  91 
  92 #  'match' 操作的默认行为就是
  93 #+ 从字符串的开始进行搜索,并匹配第一个匹配的字符串.
  94 #
  95 #        使用正则表达式
  96 b=`expr match "$a" '[0-9]*'`               #  数字的个数.
  97 echo Number of digits at the beginning of \"$a\" is $b.
  98 b=`expr match "$a" '\([0-9]*\)'`           #  注意需要转义括号
  99 #                   ==      ==              + 这样才能触发子串的匹配.
 100 echo "The digits at the beginning of \"$a\" are \"$b\"."
 101 
 102 echo
 103 
 104 exit 0

Important

: 操作可以替换 match. 比如, b=`expr $a : [0-9]*`与上边所使用的 b=`expr match $a [0-9]*` 完全等价.

   1 #!/bin/bash
   2 
   3 echo
   4 echo "String operations using \"expr \$string : \" construct"
   5 echo "==================================================="
   6 echo
   7 
   8 a=1234zipper5FLIPPER43231
   9 
  10 echo "The string being operated upon is \"`expr "$a" : '\(.*\)'`\"."
  11 #     转义括号对操作.                                   ==  ==
  12 
  13 #       ***************************
  14 #+              转移括号对
  15 #+           用来匹配一个子串
  16 #       ***************************
  17 
  18 
  19 #  如果不转义括号的话...
  20 #+ 那么 'expr' 将把string操作转换为一个整数.
  21 
  22 echo "Length of \"$a\" is `expr "$a" : '.*'`."   # 字符串长度
  23 
  24 echo "Number of digits at the beginning of \"$a\" is `expr "$a" : '[0-9]*'`."
  25 
  26 # ------------------------------------------------------------------------- #
  27 
  28 echo
  29 
  30 echo "The digits at the beginning of \"$a\" are `expr "$a" : '\([0-9]*\)'`."
  31 #                                                             ==      ==
  32 echo "The first 7 characters of \"$a\" are `expr "$a" : '\(.......\)'`."
  33 #         =====                                          ==       ==
  34 # 再来一个, 转义括号对强制一个子串匹配.
  35 #
  36 echo "The last 7 characters of \"$a\" are `expr "$a" : '.*\(.......\)'`."
  37 #         ====                  end of string operator  ^^
  38 #  (最后这个模式的意思是忽略前边的任何字符,直到最后7个字符,
  39 #+  最后7个点就是需要匹配的任意7个字符的字串)
  40 
  41 echo
  42 
  43 exit 0

上边的脚本展示了expr是如何使用转义的括号对 -- \( ... \) --正则表达式 一起来分析和匹配子串. 下边是另外一个例子, 这次的例子是真正的应用用例.
   1 # 去掉字符串开头和结尾的空白.
   2 LRFDATE=`expr "$LRFDATE" : '[[:space:]]*\(.*\)[[:space:]]*$'`
   3 
   4 #  来自于 Peter Knowle的 "booklistgen.sh" 脚本
   5 #+ 用来将文件转换为Sony Librie格式.
   6 #  (http://booklistgensh.peterknowles.com)

Perl, sed, 和 awk 是更强大的字符串分析工具. 在脚本中嵌入一段比较短的 sedawk 操作 (见 Section 33.2) 比使用 expr 更加有吸引力.

Section 9.2 将会有更多使用 expr 进行字符串操作的例子.

注意事项:

[1]

即使在不必非得强制使用 xargs 的时候, 使用 xargs 也可以明显地提高多文件批处理执行命令的速度.