16.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将用所选文件的完整路径来替换"{}"。

find ~/ -name 'core*' -exec rm {} \;
# 从用户主目录中删除所有核心转储文件。
find /home/bozo/projects -mtime -1
#                               ^   注意减号标志!
#  列出所有在/home/bozo/projects目录树下且
#  在昨日(当日-1日)修改过的文件。
#
find /home/bozo/projects -mtime 1
#  和上面一样,但是 *恰巧* 在一天前修改过的文件。
#  mtime = 目标文件的最后修改时间
#  ctime = 上次状态更改时间 (通过“chmod”或其他方式)
#  atime = 上次访问时间

DIR=/home/bozo/junk_files
find "$DIR" -type f -atime +5 -exec rm {} \;
#                          ^           ^^
#  大括号是由"find"输出的路径名的占位符。
#
#  删除在"/home/bozo/junk_files"下
#  *至少* 5日内没有访问过(加号 ... +5)的文件。
#
#  "-type filetype", filetype取以下值分别代表
#  f = 常规文件
#  d = 目录
#  l = 符号链接,等
#
#  (‘find’命令man手册和info手册有着完整详尽的选项列表)。
find /etc -exec grep '[0-9][0-9]*[.][0-9][0-9]*[.][0-9][0-9]*[.][0-9][0-9]*' {} \;

# 在/etc目录下找到所有IP地址(xxx.xxx.xxx.xxx)
# 有一些无关紧要的信息。它们能够被过滤掉吗?

# 可能的解决方案如下:

find /etc -type f -exec cat '{}' \; | tr -c '.[:digit:]' '\n' \
| grep '^[^.][^.]*\.[^.][^.]*\.[^.][^.]*\.[^.][^.]*$'
#
#  [:digit:]是POSIX 1003.2标准引入的字符类之一。

# 感谢Stéphane Chazelas。

样例 16-3. Badname, 消除当前目录中包含错误字符和空格的文件名。

#!/bin/bash
# badname.sh
# 删除当前目录中包含错误字符的文件名。

for filename in *
do
  badname=`echo "$filename" | sed -n /[\+\{\;\"\\\=\?~\(\)\<\>\&\*\|\$]/p`
# badname=`echo "$filename" | sed -n '/[+{;"\=?~()<>&*|$]/p'` 同样有效。
# 删除包含这些污点的文件:     + { ; " \ = ? ~ ( ) < > & * | $
#
  rm $badname 2>/dev/null
#             ^^^^^^^^^^^ 抛弃错误信息.
done

# 现在,处理包含各种空白的文件。
find . -name "* *" -exec rm -f {} \;
# find命令找到的文件路径名将替换"{}"
# 从字面上,'\'确保';'解释该命令结束

exit 0

#---------------------------------------------------------------------
# 由于exit命令,在这条虚线下的命令将不会被执行。

# 上述脚本的替代方案:
find . -name '*[+{;"\\=?~()<>&*|$ ]*' -maxdepth 0 \
-exec rm -f '{}' \;
#  “-maxdepth 0”选项确保find命令不会搜索当前目录($PWD)以下的子目录。

# (感谢 S.C.)

样例 16-4. 通过inode编号删除一个文件

#!/bin/bash
# idelete.sh: 删除一个文件通过其inode编号。

#  当文件名以非法字符开头时这很有用,
#  比如?或-。

ARGCOUNT=1                      # 文件名参数必须传递给脚本.
E_WRONGARGS=70
E_FILE_NOT_EXIST=71
E_CHANGED_MIND=72

if [ $# -ne "$ARGCOUNT" ]
then
  echo "Usage: `basename $0` filename"
  exit $E_WRONGARGS
fi  

if [ ! -e "$1" ]
then
  echo "File \""$1"\" does not exist."
  exit $E_FILE_NOT_EXIST
fi  

inum=`ls -i | grep "$1" | awk '{print $1}'`
# inum = inode (索引节点) 文件数
# -----------------------------------------------------------------------
# 每个文件都有一个inode,一个用于保存其物理地址信息的记录。
# -----------------------------------------------------------------------

echo; echo -n "Are you absolutely sure you want to delete \"$1\" (y/n)? "
# rm命令的-v选项也会同样询问这个问题。
read answer
case "$answer" in
[nN]) echo "Changed your mind, huh?"
      exit $E_CHANGED_MIND
      ;;
*)    echo "Deleting file \"$1\".";;
esac

find . -inum $inum -exec rm {} \;
#                           ^^
#        大括号是"find"所输出文本的占位符。
echo "File "\"$1"\" deleted!"

exit 0

不使用-exec选项的find命令同样有效。

#!/bin/bash
#  寻找suid root文件。
#  一个奇怪的suid文件可能代表一个安全漏洞,
#  甚至是系统入侵。

directory="/usr/sbin"
# 您也可以尝试/sbin, /bin, /usr/bin, /usr/local/bin等等。
permissions="+4000"  # root用户suid (危险!)


for file in $( find "$directory" -perm "$permissions" )
do
  ls -ltF --author "$file"
done

您可以查看使用find命令的样例 16-30样例 3-4样例 11-10。它的man手册提供了关于这个复杂且强大的命令的更多细节。

xargs

这是将参数传递给命令的过滤器,同时也是一个用于组装命令本身的工具。它将数据流分解成足够小的块,以供过滤器和命令进行处理。可以将它视为backquotes的强大替代品。在由于参数数量过多而导致的命令替换失败的情况下,切换使用xargs通常有效。[1]通常,xargs从标准输入(stdin)或者管道(pipe)中读取数据,但它也可以从文件输出中读取。

xargs的默认命令是echo。这意味着通过管道输入到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...



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 gzipgzips当前目录下的所有文件,在每次操作前均提示一下。

bash$ find /usr/bin | xargs file
/usr/bin:          directory
 /usr/bin/foomatic-ppd-options:          perl script text executable
 . . .

一个有趣的xargs选项是-n NN,它将传递参数的数量限制为NN

ls | xargs -n 8 echo 该条命令以8列的形式列出在当前目录下的文件。

另一个有用的选项是-0,结合find -print0 grep -lZ。这允许处理包含空格或引号的参数。

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

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

以上两条命令都起到移除所有包含"GUI"的文件。(感谢 S.C.)

或者:

cat /proc/"$pid"/"$OPTION" | xargs -0 echo
#  格式化输出:               ^^^^^^^^^^^^^^^
#  来自Han Holl对"/dev and /proc"一章中
#  "get-commandline.sh"脚本的修正。

xargs-P选项允许并行(parallel)运行进程。这能够提升多核CPU计算机中的执行速度。

#!/bin/bash

ls *gif | xargs -t -n1 -P2 gif2png
# 将当前目录中的所有gif图像转换为png。

# 选项:
# =======
# -t    将命令输出到标准错误(stderr)。
# -n1   每个命令行最多1个参数。
# -P2   最多同时运行2个进程。

# 感谢Roberto Polli的启发。

样例 16-5. Logfile: 使用xargs来监视系统日志

#!/bin/bash

# 从/var/log/messages的末尾
# 来生成当前目录中的日志文件。

# 注意: 如果普通用户调用此脚本,
# 则/var/log/messages必须是全局可读的。
#         #root chmod 644 /var/log/messages

LINES=5

( date; uname -a ) >>logfile
# 时间和设备名
echo ---------------------------------------------------------- >>logfile
tail -n $LINES /var/log/messages | xargs | fmt -s >>logfile
echo >>logfile
echo >>logfile

exit 0

#  注意:
#  ----
#  正如Frank Wang指出,
#  源文件中不匹配的引号 (单引号或双引号) 可能会导致xargs无法识别

#
#  他建议第15行采用如下替换方案:
#  tail -n $LINES /var/log/messages | tr -d "\"'" | xargs | fmt -s >>logfile



#  练习:
#  --------
#  修改此脚本,以20分钟的间隔跟踪/var/log/messages中的更改。
#  提示:使用"watch"命令

find相同,大括号对用作替换文本的占位符。

样例 16-6. 将当前目录下的文件复制到另一个目录

#!/bin/bash
# copydir.sh

#  将当前目录($PWD)下的所有文件复制(详细的)
#  到命令行中指定的目录。

E_NOARGS=85

if [ -z "$1" ]   # 如果没有提供参数则退出。
then
  echo "Usage: `basename $0` directory-to-copy-to"
  exit $E_NOARGS
fi  

ls . | xargs -i -t cp ./{} $1
#            ^^ ^^      ^^
#  -t 是“详细的”(输出命令行到标准错误stderr)选项。
#  -i 是“替换字符串”选项。
#  {} 是输出文本的占位符。
#  这类似于在 “find”命令中使用的大括号对。

#
#  列出当前目录中的文件(ls .),
#  将 “ls” 命令的输出作为参数传递给 “xargs” (-i -t选项),
#  并且将这些参数({})复制(cp)到新的目录($1)。
#
#  最终结果完全等同于
#    cp * $1
#  除非某个文件名包含 “空格” 字符。

exit 0

样例 16-7. 通过进程名杀死进程

#!/bin/bash
# kill-byname.sh: 通过进程名杀死进程。
# 您可以与kill-process.sh脚本作比较。

#  例如,
#  试图执行"./kill-byname.sh xterm"
#  然后眼睁睁地看着你桌面上的xterm都云消雾散。

#  警告:
#  -------
#  这是一个相当危险的脚本。
#  不在意地(尤其是root用户)运行它可能会导致数据丢失和其他不良影响。

E_BADARGS=66

if test -z "$1"  # 你难道不提供命令行参数吗?
then
  echo "Usage: `basename $0` Process(es)_to_kill"
  exit $E_BADARGS
fi


PROCESS_NAME="$1"
ps ax | grep "$PROCESS_NAME" | awk '{print $1}' | xargs -i kill {} 2&>/dev/null
#                                                       ^^      ^^

# ---------------------------------------------------------------
# 注意:
# -i 是xargs命令的“替换字符串”选项。
# 大括号是替换的占位符。
# 2&>/dev/null丢弃不需要的错误消息。
#
# 可以把grep "$PROCESS_NAME"替换成pidof "$PROCESS_NAME"吗?
# ---------------------------------------------------------------

exit $?

#  “killall” 命令与该脚本具有相同的效果,
#  但是使用它并不具有教育意义。

样例 16-8. 使用xargs进行词频分析

#!/bin/bash
# wf2.sh: 文本文件的粗词频分析。

# 使用"xargs"将文本行分解为单个单词。
# 将此样例与稍后的"wf.sh"脚本进行比较。

# 在命令行检查输入文件。
ARGS=1
E_BADARGS=85
E_NOFILE=86

if [ $# -ne "$ARGS" ]
# 是否传递给脚本正确的参数数量?
then
  echo "Usage: `basename $0` filename"
  exit $E_BADARGS
fi

if [ ! -f "$1" ]       # 判断文件存在吗?
then
  echo "File \"$1\" does not exist."
  exit $E_NOFILE
fi



#####################################################
cat "$1" | xargs -n1 | \
#  列出文件,每行一个单词。
tr A-Z a-z | \
#  把大写字母转换为小写字母。
sed -e 's/\.//g'  -e 's/\,//g' -e 's/ /\
/g' | \
#  过滤掉句号和逗号,
#  并将单词之间的空格更改为换行,
sort | uniq -c | sort -nr
#  最后删除重复项,并前缀计数和数字排序。
#####################################################

#  该脚本与"wf.sh"做了相同的工作,
#  但是更麻烦了一点,它运行得更慢 (为什么?)。

exit $?

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+1y=((y+1))等效。这是一个算数扩展的例子。

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

从$position开始,提取$length长度的子字符串。

样例 16-9. 使用 expr

#!/bin/bash

# 演示'expr'命令的一些用途
# =======================================

echo

# 算数运算符
# ---------- ---------

echo "Arithmetic Operators"
echo
a=`expr 5 + 3`
echo "5 + 3 = $a"

a=`expr $a + 1`
echo
echo "a + 1 = $a"
echo "(incrementing a variable)"

a=`expr 5 % 3`
# 取模
echo
echo "5 mod 3 = $a"

echo
echo

# 逻辑运算符
# ------- ---------

#  真返回1,假返回0,
#  与正常的Bash惯例相反。

echo "Logical Operators"
echo

x=24
y=25
b=`expr $x = $y`         # 测试是否相等。
echo "b = $b"            # 0  ( $x -ne $y )
echo

a=3
b=`expr $a \> 10`
echo 'b=`expr $a \> 10`, therefore...'
echo "If a > 10, b = 0 (false)"
echo "b = $b"            # 0  ( 3 ! -gt 10 )
echo

b=`expr $a \< 10`
echo "If a < 10, b = 1 (true)"
echo "b = $b"            # 1  ( 3 -lt 10 )
echo
# 注意运算符的转义。

b=`expr $a \<= 3`
echo "If a <= 3, b = 1 (true)"
echo "b = $b"            # 1  ( 3 -le 3 )
# 还有一个 “\>=” 运算符 (大于或等于)。


echo
echo



# 字符串运算符
# ------ ---------

echo "String Operators"
echo

a=1234zipper43231
echo "The string being operated upon is \"$a\"."

# length: 字符串长度
b=`expr length $a`
echo "Length of \"$a\" is $b."

# index: 子字符串中与原字符串中匹配的第一个字符的位置。
b=`expr index $a 23`
echo "Numerical position of first \"2\" in \"$a\" is \"$b\"."

# substr: 提取子字符串,起始位置和长度需要指定
b=`expr substr $a 2 6`
echo "Substring of \"$a\", starting at position 2,\
and 6 chars long is \"$b\"."


#  "match"操作的默认行为是
#  从字符串的起始位置搜索匹配的字符串。
#  
#       使用正则表达式 ...
b=`expr match "$a" '[0-9]*'`               #  数值型字符计数。
echo Number of digits at the beginning of \"$a\" is $b.
b=`expr match "$a" '\([0-9]*\)'`           #  请注意,转义括号
#                   ==      ==             #  触发子字符串匹配。
echo "The digits at the beginning of \"$a\" are \"$b\"."

echo

exit 0
#!/bin/bash

echo
echo "String operations using \"expr \$string : \" construct"
echo "==================================================="
echo

a=1234zipper5FLIPPER43231

echo "The string being operated upon is \"`expr "$a" : '\(.*\)'`\"."
#     转义括号为分组运算符。                                ==  ==

#       ***************************
#              转义括号
#            匹配子字符串
#       ***************************


#  如果没有转义括号 ...
#  那么'expr'会将字符串操作数转换为整数。

echo "Length of \"$a\" is `expr "$a" : '.*'`."   # 字符串长度

echo "Number of digits at the beginning of \"$a\" is `expr "$a" : '[0-9]*'`."

# ------------------------------------------------------------------------- #

echo

echo "The digits at the beginning of \"$a\" are `expr "$a" : '\([0-9]*\)'`."
#                                                             ==      ==
echo "The first 7 characters of \"$a\" are `expr "$a" : '\(.......\)'`."
#         =====                                          ==       ==
# 同样,转义的括号强制子字符串匹配。
#
echo "The last 7 characters of \"$a\" are `expr "$a" : '.*\(.......\)'`."
#         ====                  字符串运算符的末尾         ^^
#  (实际上,这意味着跳过一个或多个任何字符,
#  直到找到指定的子字符串。)

echo

exit 0

上面的脚本解释了expr是如何使用转义括号 -- ( ... ) -- 分组运算符与正则表达式解析一起使用来匹配字符串。以下是另一个 “现实生活”中的案例。

# 去除开头和结尾的空格。
LRFDATE=`expr "$LRFDATE" : '[[:space:]]*\(.*\)[[:space:]]*$'`

#  摘自Peter Knowles的"booklistgen.sh"脚本
#  该脚本用于将文件转换为Sony Librie/PRS-50X格式。
#  (http://booklistgensh.peterknowles.com)

Perlsed,和awk具有更为优秀的字符串解析功能。脚本中简短的sed或者awk"子例程(subroutine)"(参见章节 36.2)相比expr是更优的替代方案。

有关在字符串操作中使用expr的更多信息请参见章节 10.1

注记

[1]即使不是绝对需要xargs,它也可以加快涉及多个文件的批处理指令的执行速度。

最后更新于