linux命令行工具妙用

Fork from:http://mingxinglai.com/cn/2012/08/toos-of-bash/

**前言:**虽然现在各种动态脚本语言发展得热火朝天,一片欣欣向荣的景象,而且动态脚本语言如python确实学习成本低,入门快,虽然unix 下各种工具比我们很多人年龄都大,但是我还是相信一些有用的小工具在可预见的时间内不会消失,因为灵活运用它们能够快速解决很多小问题,而且你不大可能写出更短的代码来。

1. 一行命令算术运算

seq 100 | echo $[ $(tr '\n' '+') 0]
seq 5 3 20 | echo $[ $(tr '\n' '*') 1]

seq 用于产生一组等差的数列,默认首项和公差都是1,所以上面的第一行代码会产生1 -- 100 的整数,且每个数占一行,使用tr 将换行替换成+号以后,形成如1+2+......+100+的一行字符串,所以需要在字符串最后加一个0,最后使用echo $[ ]进行算数运算,也可以通过管道使用bc来计算seq 100 | echo $(tr '\n' '+') 0 | bc

2. 100的阶乘末尾有几个零?

这是计算机专业面试的一个比较有名的题目,正确的解法答案是 n/5 + n/5/5 + n/5/5/5…… 下面我们写一行代码来验证一下

echo {1..100}|tr ' ' '*'|bc|tr '1-9' '\n'|sort -r|
head -1|awk '{print length($0) }'

这里{1..100}属于通配符,例如你要新建100个文件,就可以touch test{1..100}.txt,还可以touch test{a..z}.txt。 算后计算出1乘到100的结果,将结果中非零的数字替换成换行符在1乘到100的结果中,必然是末尾的0最多 所以我们只需要在tr '1-9' '\n'之后,找出最长的那一行就可以了,这里使用的方法是先将结果逆序排序sort -r,然后打印第一行head -,计算出第一行的长度awk '{print length($0)}'

有没有更简单的办法来实现这个功能呢,或者说有没有更短的程序能实现同样的功能,我们几乎可以肯定的是,在shell脚本中代码越少执行越快,看下面的代码:

seq 100 | echo  $(tr '\n' '*') 1 | bc 
| tr -C '0' '\n' | wc -L

前面3步虽然用了不一样的方法,但是功能都是一样的,效率也没什么区别,但是后面这种实现方式不需要排序,这就省下了很多时间。所以效率更高是毫无疑问的,而实现后者的关键是要知道wc提供了一个参数 -L ( wc -L) 用于统计最长的行 。

3. 统计源程序的行数

假如你有一天心血来潮,想统计自己写了多少代码,或者说你也跟我一样看到了《解放军“网络尖兵”90天写40万行代码》这条新闻,觉得不可思议,那么你应该做的就是统计代码的行数,假如你最喜欢的动态脚本语言是python,那么你很可能会用python的os.walk()来遍历目录,然后过滤文件后缀名为源代码的(如c、cpp和h),再按行读取每个符合的文件,合计所有文件的行数。有没有更简单的办法呢?答案是肯定的。我们先不考虑文件的后缀名,很容易的就能写出下面的代码:

find $path -exec wc -l {} \; | awk '{lines += $1} 
END {print lines}'

虽然结果是拿到了,可总感觉效率低了点。而且需要我们手工的将每个文件的行数加起来,对于这么懒的你,这怎么可以忍受呢?于是想到可以直接把所有文件合并起来,当成一个整体传给wc:

find $path | xargs cat | wc -l

这下效率果然高多了,瞬间就得到了结果,不过内存占用或许会比较多。于是又看了下wc的文档,发现它可以接受多个文件参数,最后会输出总行数,于是cat也就可以省略了:

find $path | xargs wc -l

我们能不能写得更短一点呢?如果再交换下wc和find的位置,连xargs也能省略了:

wc -l \`find $path\`

最后你写出来的一行代码应该跟下面的差不多:

wc -l \`find $path -regex ".*\.\(py\|c\|cpp\|h\)"\`

此示例参考网址:www.keakon.net

假入你的home 目录下有一个code子目录,用于存放你平时做的一些算法练习,而且你也熟悉git,那么你很可能会会写出下面的脚本

## !/bin/bash
cd ~/code
git add .
git push

然后使用cron 让计算机每晚9点帮你备份数据,但是很可能你的~/code 下还有很多a.out 文件,很明显这是无用的文件,我们要珍惜 github 上宝贵的300M 空间,所以要把所有的a.out 文件删除,该怎么做呢?

一种方法是我们可以通过find 提供的-exec 参数来对找到的文件执行操作,格式如下:

find . -name a.out -exec rm -rf {} \;

还有更简单的办法就是使用find 提供的-delete 参数,

find . -type f -name "a.out" -delete

当然了,方法总是多多的,我们还可以将找到的文件名通过xargs 传给下一个命令,不过需要注意的是,正确的用法应该是

find . -type f -name "filename" -print0 | xargs -0 rm -f

上面的-print0 就是明确说明用\0 作为输入的定界,而不是默认的空格或回车,按照下面的方法也是正确的,但是当遇到文件名包含空格的时候就可能会出错。如你当前文件夹下面有三个文件,分别是empty、file 和 empty file. 你现在要删除empty file .

find . -name "empty file" | xargs rm -rf 

结果不是我们期望的那样的,这条命令会删除empty 和 file 文件,保留empty file 文件,所以为了以防万一,只要我们把find 的输出作为xargs 的输入,就必须将 -print0 与find 结合使用,以字符null 来分割输出。

5. 分析散文中使用的单词

下面我们来看一个由《Programming Pearls》的作者Bentley提出的问题:写一个文字处理程序,找出n个出现最频繁的单词,并在输出结果的列表上加入它们的出现次数,按照次数从小到大排序,据说著名科学家Donald Kunth 和David Hanson 分别回应了两个聪明的程序,而每个程序都花了分别数小时写出来,最后Bentley 给出了一个只有一行的脚本代码,而这行代码对于熟悉linux 命令行工具的普通程序员来说都没有一点难度。可见linux 命令行工具的强大。下面我们来看具体是怎么实现的。

cat $* | tr -cs A-Za-z '\n'| tr A-Z a-z |
sort | uniq -c | sort -r -n | sed 25q

解释如下:1.连接所有输入文件;2.让每行包含一个单词,办法是把字母表以外的字符(-c)翻译成新行(\n)去掉重复的空白行(-s);3.把大写翻译成小写;4.排序,以便把相同的单词归并在一起;5.把连续的相同的单词换成一个单词及其计数(-c);6.按照数值(-n)递减(-r)顺序来排序;7.经过一个流编辑器,在打印25行后退出。

cat hamlet.txt | tr -cs A-Za-z '\n' |tr A-Z a-z|
sort | uniq -c | sort -nr | head -n 25

如果你真的去执行了上面的代码,你很可能会觉得有些结果很诡异,比如说s,d,怎么会出现这么多呢?d我没想到是为什么,但是s 很明显,如what's当我们把单引号去掉的时候,它就变成了两个单词whats,也就是说结果不是很正确,那我们需要把s这行去掉,方法如下:

cat hamlet.txt | tr -cs A-Za-z '\n' |
tr A-Z a-z | sort | uniq -c | sort -nr |
grep -v '^ *[0-9]* s$'| head -n 25

还有你可能觉得,单词出现次数在前面,而单词在后面很怪,希望将它们换下顺序也不是不可以:

cat hamlet.txt | tr -cs A-Za-z '\n' | tr A-Z a-z |
sort | uniq -c | sort -nr | grep -v '^ *[0-9]* s$'|
head -n 25 | awk '{printf("%10s\t%5d\n", $2, $1)}'

有了上面的分析,再要获得其他结果就很容易了,比如你想知道莎士比亚在《哈姆雷特》这本书里共用了几个单词,你就可以这样:

cat hamlet.txt | tr -cs A-Za-z '\n' | tr A-Z a-z |
sort | uniq -c | sort -nr | grep -v '^ *[0-9]* s$'| wl -l

你也可以分析《哈姆雷特》这本书有多少字:

cat hamlet.txt | tr -cs A-Za-z '\n' | tr A-Z a-z | wc -l

如果你觉得《哈姆雷特》这本书非常具有代表性,你想通过这本书来知道哪些单词是英语中常用的单词,你就可以这样:

cat hamlet.txt | tr -cs A-Za-z '\n' | tr A-Z a-z |
sort | uniq -c | sort -nr | grep -v '^ *[0-9]* s$'|
awk '$1 >= 5{print $0}'

你还可以知道哪些单词只出现了一次

cat hamlet.txt | tr -cs A-Za-z '\n' | tr A-Z a-z |
sort | uniq -c | sort -nr | grep -v '^ *[0-9]* s$'|
awk '$1 == 1{print $0}'

你还想知道什么?别忘了,知道得越多越危险。

6. 文件查找与文件列表

find是Unix/Linux命令行工具箱最棒的工具之一,这个命令对编写shell脚本很有帮助,但是多数人由于对它缺乏认识,并不能有效的使用它。:-),包括我自己,刚开始极少使用find,在知道find的很多功能以后时常需要用到,但是由于find的参数实在太多,每次都需要翻书查阅,特地在此记录,方便下次查阅,这部分例子主要来自《Linux Shell脚本攻略》,略有补充,我在此向看到这篇博客的童鞋强烈推荐此书。

要列出当前目录及子目录下的文件和文件夹,可以采用下面的写法:

find base_path
find . -print
find .. -print0

bash_path可以是任意位置,如(/home/slynx),find会从该位置开始向下查找。.指定当前目录,..指定父目录,这是Unix文件系统的约定用法。-print指明打印出匹配文件的文件名,当使用-print时,'\n'作为用于分隔文件的定界符,-print0指明使用作为’\0'用于分隔文件的定界符。

作为一个强大的命令行工具,find命令包含了诸多值得留意的选项,接下来让我们来看一下find命令的一些其他选项。

(1). 根据文件名或者正则表达式匹配搜索

选项-name的参数指定了文件名所必须匹配的字符串。我们可以将同配符作为参数使用。如下所示:

find . -name "*.txt"
find . -iname "example*"

find命令还有一个选项-iname(忽略大小写),该选项的作用和-name类似,只不过在匹配名字的时候会忽略大小写。

我们还可以使用OR条件来一次匹配多个:

find . \( -name "*.txt" -o -name "*.pdf" \) -print

上面的代码会打印出所有的txt和pdf文件。选项-path可以使用通配符来匹配文件路径或者文件。-name总是用给定的文件名进行匹配。-path则将文件路径作为一个整体进行匹配。如下所示:

find ~ -path "*git*" -print

选项-regex-path相似,不过-regex是基于正则表达式来匹配文件路径的。下面的命令匹配.py或者.sh文件。

find -regex ".*\(\.py\|\.sh\)$"

类似于-name,-iregex用于忽略正则表达式的大小写。

## ## (2). 否定参数

find可以用”!“否定参数的含义,例如:

find . ! -name "*.txt" -print

(3). 基于深度的搜索

find命令在使用时会遍历所有的子目录。我们可以采用一些深度参数来限制find命令遍历的深度。-maxdepth-mindepth就是这类参数。如:

find . -maxdepth 1 -type f 

该命令只列出当前目录下的所有普通文件,即使有子目录,也不会被打印或遍历,与此类似,-maxdepth 2 最多向下遍历两级子目录。

-maxdepth-mindepth应该作为find的第3个参数出现,如果作为第4个参数或者之后的参数,就可能会影响到find的效率,因为它不得不进行一些不必要的检查。

(4). 根据文件类型搜索

-type可以对文件搜索进行过滤。借助这个选项,我们可以为find命令指明特定的文件匹配类型。如下所示:

只列出链接:

find . -type l

只列出目录:

find . -type d

-atime(访问时间),-mtime(修改时间)-ctime(变化时间)可以作为find的时间参数。它们用整数值给出,单位是天数,这些整数还可以带有正负号,如下所示:

打印出最近7天内被访问过的所有文件:

find . -type f -atime -7 -print

打印恰好7天前被访问的所有文件:

find . -type f -atime 7 -print

打印出访问时间超过7天的所有文件

find . -type f -atime +7 -print

类似的,我们可以根据修改时间,用-mtime进行搜索,也可以根据变化时间-ctime进行搜索。与此同时,我们还可以使用-amin,-mmin,-cmin做为参数,即以分钟作为计量的单位。举例如下:

find . -type f -amin +7 -print

上面这条语句会打印出所有访问时间超过7分钟的文件。

find 另一个漂亮的参数是-newer ,使用-newer ,我们可以指定一个用于比较时间戳(修改时间)的参考文件,然后找出比该文件更新的所有文件,例如,找出比file.txt 修改时间更长的所有文件:

find . -type f -newer file.txt -print

(6). 基于文件大小进行搜索

根据文件的大小,可以这样搜索:

大于2KB的文件

find . -type f -size +2k

小于2KB的文件

find . -type f -size -2k

等于2KB的文件

find . -type f -size 2k

除了k之外,我们还可以使用其他文件大小单元

(7). 删除匹配的选项

我们已经在第4部分介绍了如何删除垃圾文件,在此不在熬述。

(8). 基于文件权限和所有权的匹配

文件匹配可以根据文件权限进行。列出具有特定权限的所有文件:

find . -type f -perm 644 -print

以Appache Web服务器为例。Web服务器上的PHP文件需要具有合适的执行权限。我们可以用下面的方法找出那些没有设置好权限的PHP文件:

find . -type f -name "*.php" ! -perm 644 -print

用选项-user USER就能够找出某个特定用户所拥有的文件,参数USER可以是用户名也可以是UID

(9). 结合find执行命令或动作

find命令可以借助选项-exec与其他命令进行结合。-exec算得上是find最强大的特性之一。下面看看应该如何使用-exec选项。

下面,我们将某位用户(比如说是root)的全部文件的所有权更改成另一位用户,那么我们就可以用-user找出root拥有的所有文件,然后用-exec更改所有权。

find . -type f -user root -exec chown YourName {} \;

在这个命令中,{}是一个特殊的字符串,与-exec选项结合使用。对于每一个匹配的文件,{},会被替换成相应的文件名。例如:find命令找到两个文件test1.txt和test2.txt,其所有者均为root,那么find将会执行chown YourName {} 它会被解析为chown YourName test1.txtchown YourName test2.txt.

另一个例子是将指定目录中的所有C程序文件拼接起来,写入带个文件all_c_files.txt,我们可以用find 找到所有的C文件,然后结合-exec 使用cat命令:

find . -type f -name "*.c" -exec cat {} \; >all_files.txt

-exec之后可以接任何命令。{}表示一个匹配。对于任何一个匹配的文件名,{}会被该文件名所替代。

我们使用>操作符将来自find的数据重定向到all_c_files.txt文件,没有使用»的原因是因为find命令全部处处只是一个单数据流,而只有当多个数据流被追加到单个文件中的时候才有必要使用»。

例如,用下列命令将10天前的.txt文件复制到OLD目录中:

find . -type f -mtime +10 -name "*.txt" -exec cp {} OLD \;

find命令同样可以采用类似的方法与其他命令结合起来。

有一点值得注意的是,我们无法在-exec参数中直接使用多个命令。它只能够接受单个命令,不过我们可以耍一个小花招。把多个命令写到一个shell脚本中,例如(commands.sh)然后在-exec中使用这个脚本:

-exec ./commands.sh {} \;

-exec能够同printf结合来生成有用的输出信息。例如:

find . -type f -name "*.txt" -exec
print "Text file: %s\n" {} \;

(10). 让find跳过特定目录

在搜索目录并执行某些操作的时候,有时为了提高效率,需要跳过一些子目录。例如:

find . \( -name ".git" -prune \) -o
\( -type f -name "*.html" \)

以上命令打印出不包括在.git目录中的所有文件的名称。 还有一种方法可以跳过特定目录,例如,当前文件夹包含很多文件和子文件夹,我们现在需要把除了某一文件以外的其他文件都删除,应该怎么做?(这是一个我经常碰到的问题,比如我从github 上下载了git 的教程,但是该教程下面有各种语言版本,显然我只需要保留简体中文版,所以我需要删除除了zh_cn 外的所有文件和文件夹)

find . -maxdepth 1 ! -name "zh_cn" -exec rm -rf {} \;

首先我们指定搜索深度为1,这样才不会把zh_cn子目录下的文件给找出来,然后我们通过!来对文件名取反,再删除之。

comments powered by Disqus