整篇文章循序漸進,從最常用的文件名測試項開始步步深入,到第六節基本講完find處理文件的規則,再之后的章節是一些常用表達式的說明。
(此篇中所有選項及例子基於GNU find version 4.2.28)
(一)Get Start
最簡單的find用法莫過於如此:
$ find .
查找當前目錄下的所有文件。
find命令的一般格式為:
find [-H] [-L] [-P] [path...] [expression]
其中,'-H' '-L' '-P'三個選項主要是用來處理符號連接,'-H'表示只跟隨命令行中指定的符號連接,'-L'表示跟隨所有的符號連接,'-P'是默認的選項,表示不跟隨符號連接。
例如,在我的當前目錄下有一個符號連接e1000,現在我想查找文件名中最后一個字母是數字的源文件,那么
$ find -H . -name "*[0-9].c" -print
./2234.c
像上面這樣寫只能查找出當前目錄下符合要求的文件,卻找不出e1000下的文件。因此可以這么寫:
$ find -H e1000 . -name "*[0-9].c" -print
或者使用 '-L'選項
$ find -L . -name "*[0-9].c" -print
格式中的[path...]部分表示以此目錄為根目錄進行搜索。
格式中的[expression]是一個表達式。最基本的表達式分為三類:設置項(option)、測試項(test)、動作項(action),這三類又可以通過邏輯運算符(operator)組合在一起形成更大更復雜的表達式。設置項(如-depth,-maxdepth等)針對這次查找任務,而不是僅僅針對某一個文件,設置項總是返回true;測試項(test)則不同,它針對具體的一個文件進行匹配測試,如-name,-num,-user等,返回true或者false;動作項(action)則是對某一個文件進行某種動作(最常見的如-print),返回true或者false。
正是[expression]部分的豐富,才使得find如此強大。此部分較復雜,后面慢慢說明。
(二)文件名
根據文件名來查找一個文件是大家經常遇到的事情,第一節中的'-name'正是解決此問題的。
-name屬於表達式中的測試項(test),它按照文件名模式來匹配文件,若匹配則返回true,否則返回false。最好用引號將文件名模式引起來,防止shell自己解析要匹配的字符串。(可以用單引號也可以用雙引號,單引號和雙引號在shell環境中的區別見后續部分)
例如,想要的當前目錄及子目錄中查找文件名以一個大寫字母開頭或者以小寫a或b開頭的文件,可以用:
$ find . -name "[A-Za-b]*" -print
./a_book_of_c.chm
./TMP1234
如果想在當前目錄查找文件名不以大寫字母開頭,之后跟一個小寫字母,再之后是兩個數字,最后是.txt的文件,可以這么用:
$ find . -name "[^A-Z][a-z][0-9][0-9].txt" -print
./@y38.txt
注意:此處的模式匹配並不符合正則表達式。
-name對大小寫字母敏感,如果想匹配時不考慮大小寫可以使用-iname測試項。'i'可以加在許多選項前面,比如-ipath,-iregex,-iwholename等等,都是表示大小寫不敏感。
(三)正則表達式
使用上面的-name測試項能解決許多問題,但是有些還是不太好辦,比如:查找當前目錄下名稱全部為數字的c源代碼文件,這時就該'-regex'出手了。正則表達式絕對值得你去好好研究一下,在unix系統下太有用了,這里不做過多說明,請讀者自行學習。
-regex同樣屬於測試項。使用-regex時有一點要注意:-regex不是匹配文件名,而是匹配完整的文件名(包括路徑)。例如,當前目錄下有一個文件"abar9",如果你用"ab.*9"來匹配,將查找不到任何結果,正確的方法是使用".*ab.*9"或者".*/ab.*9"來匹配。
針對上面的那個查找c代碼的問題,可以這么寫:
$ find . -regex ".*/[0-9]*/.c" -print
./2234.c
還有一個設置項(option)'-regextype',可以讓你根據自己的喜好選擇使用的正則表達式類型,大家可以試試。
(四)wholename與path
既然上一節提到了完整文件名(包括路徑名),那么這里不妨說一下-wholename和-path。
-wholename和-path都屬於測試項(test),而且功能也一樣。-path從字面上看給人一種錯覺,好像只匹配路徑名(或者目錄名),其實它也可以匹配文件名,因此-wholename這個名字更貼切一些。看看這個例子,當前目錄下有一個phone目錄,phone目錄里有一個文件名稱是puk.txt,使用-path:
$ find . -path '*phone/pu*'
./phone/puk.txt
另外要提一點:使用-path的一般格式是:find [path ...] -path pattern ...
它的意思是:在[path ...]部分指明的路徑上,使用pattern匹配所有文件的完整文件名;而不是說在類似的pattern目錄下查找文件。
(五)邏輯運算符
有了上面三個選項,你現在應該對文件名的相關匹配得心應手了,對於不是很復雜的查找應該也勝任了。但是看看這個例子,解釋一下它在做什么?
下面是當前目錄下的所有文件:
total 224
-rw-r--r-- 1 xixi admin 0 2007-11-01 17:34 0dfe.c
-rw-r--r-- 1 abc admin 0 2007-10-30 15:56 0s8a.txt
-rw-r--r-- 1 abc admin 0 2007-11-04 01:00 0TMP123
-rw-r--r-- 1 abc admin 73 2007-11-05 15:33 2234.c
-rw-r--r-- 1 abc admin 72 2007-11-05 15:34 3e10.c
-rw------- 1 abc admin 224017 2006-03-16 12:16 a_book_of_c.chm
lrwxrwxrwx 1 abc admin 15 2007-11-04 11:48 e1000 -> ../e1000-7.6.9/
-rw-r--r-- 1 abc admin 70 2007-11-05 14:57 e100.dat
lrwxrwxrwx 1 abc admin 13 2007-11-05 14:59 e100puk.txt -> phone/puk.txt
-rw-r--r-- 1 abc admin 0 2007-11-06 22:21 e680phone
drwxr-xr-x 2 abc admin 37 2007-11-06 22:24 phone
drwxr-xr-x 2 abc admin 20 2007-11-07 01:07 phone1
drwxr-xr-x 2 abc admin 6 2007-11-05 15:37 phone2
-rw-r--r-- 1 abc admin 67 2007-11-04 12:23 @y38.txt
phone$ ls -l
total 4
-rw-r--r-- 1 abc admin 0 2007-11-06 22:24 e680gphone
-rw------- 1 abc admin 38 2007-11-05 14:58 puk.txt
phone1$ ls -l
total 0
-rw-r--r-- 1 xixi admin 0 2007-11-07 01:07 hello.c
phone2$ ls -l
total 0
要想解決上面的問題就得學習一下find中的邏輯運算符。邏輯運算符主要有以下幾個,按照優先級從高到低的順序如下:
( expr )
! expr
-not expr
expr1 expr2
expr1 -a expr2
expr1 -and expr2
expr1 -o expr2
expr1 -or expr2
expr1 , expr2
正是因為有一個求值的順序,所以你才有可能見到這樣的寫法:
$ find . -name "*.txt" -o -print
表示,如果表達式-name "*.txt"為真,就不再執行另一個表達式-print,即查找所有不是以.txt結尾的文件。
再有,要查找當前目錄下,文件名中包括字母'e',在'e'之后又有數字的不是目錄文件的所有文件,可以這么寫:
$ find . -name "*e*[0-9]*" ! -type d -print
./e1000
./e100.dat
./e100puk.txt
./3e10.c
大家可以自己多舉幾個例子試一下。
(六)-prune
-prune是一個動作項,它表示當文件是一個目錄文件時,不進入此目錄進行搜索。
要理解-prune動作,首先得理解find命令的搜索規則(也可以說find命令的算法)。
find命令遞歸遍歷所指定的目錄樹,針對每個文件依次執行find命令中的表達式,表達式首先根據邏輯運算符進行結合,然后依次從左至右對表達式求值。以下面代碼為例,進行說明
find PATHP1 OPT1 TEST1 ACT1 ( TEST2 or TEST3 ) ACT2
(1) 根據OPT1設置項進行find命令的整體設置,若沒有-depth設置項,依次進行下面的步驟
(2) 令文件變量File = PATHP1
(3) 對File文件進行TEST1測試,若執行結果為false,轉(8)
(4) 對File文件進行ACT1動作,若執行結果為false,轉(8)
(5) 對File文件進行TEST2測試,若執行結果為true,轉(7)
(6) 對File文件進行TEST3測試,若執行結果為false,轉(8)
(7) 對File文件進行ACT2動作
(8) 若File文件是一個目錄,並且沒有被執行過-prune動作,則進入此目錄
(9) 當前目錄下是否還有文件,若有依次取一個文件,令File指向此文件,轉(3);
(10) 判斷當前目錄是否是PATHP1,若是則程序退出;若不是,則返回上一層目錄,轉(9)
理解了上面的流程,那么不難理解下面的代碼為什么只輸出一個'.'
$ find . -prune
.
再有,當前目錄下大於4090字節的文件有兩個,而大於4096字節的文件只有一個,如下:
$ find . -size +4090c -print
.
./a_book_of_c.chm
$ find . -size +4096c -print
./a_book_of_c.chm
那么,將上面兩個-print都替換為-prune,這兩條命令分別輸出什么?
$ find . -size +4090c -prune
.
$ find . -size +4096c -prune
./a_book_of_c.chm
這就是答案,如果你答對了,恭喜你,你已經掌握了find命令!
-prune經常和-path或-wholename一起使用,以避開某個目錄,常見的形式是:
$ find PATH (-path <don't want this path #1> -o -path <don't want this path #2>) -prune -o -path <global expression for what I do want>
注意:如果同時使用-depth設置項,那么-prune將被find命令忽略。man手冊頁中這么說:"If -depth is given, false; no effect."
說到這里,又得說說-depth設置項。網上好多資料說-depth設置項的功能是“在查找文件時,首先查找當前目錄中的文件,然后再在其子目錄中查找”,這明顯是錯誤的,man手冊頁中如是說:"-depth Process each directory's contents before the directory itself."。這有點像樹的后序遍歷,先遍歷當前節點的所有子節點,然后再訪問當前節點...
考考你:
下面的命令輸出什么?為什么?
$ find . -depth -prune -name "*.c" -print
(七)時間戳
理解了上面幾節,你已經掌握了find命令的“道” ^_^ ,下面這幾節只是介紹一些常用、好用的“招式”。這一節介紹時間戳。
文件有三個時間屬性:創建時間、最近修改時間、最近訪問時間。
最近修改時間又包括兩種,一是文件的狀態(也即權限如rwx等)最近被修改時間,一是文件的數據(也即內容)最近被修改時間。touch命令改變的即是文件數據最近被修改時間。
最近訪問時間,指的是最近一次文件數據(內容)被訪問的時間。因此,使用ls命令輸出文件的相關信息並不會修改文件的最近訪問時間。
find命令提供了針對文件的最近訪問時間、文件狀態最近被修改時間、文件數據最近被修改時間進行匹配的測試項,分別是-amin, -cmin, -mmin和-atime, -ctime, -mtime兩組,第一組基於分鍾,第二組基於天。
以-amin為例,假設當前時間tnow="2007-11-12 14:42:10"、t1="2007-11-12 14:39:10"、t2="2007-11-12 14:40:10",那么要查找最近訪問時間屬於[t1,t2]時間段的文件,可以這么寫:
$ find . -amin 3
若測試項參數是數字,則基本上都可以在數字參數前加"+"或者"-"號,表示“大於”或“小於”的意思,因此,要查找最近訪問時間屬於[t1,tnow]時間段的文件,可以這么寫:
$ find . -amin -3
"-amin n"和"-atime n"的處理方法都是:根據當前時間和文件的相應時間屬性求n值,然后比較n值和參數n,看是否符合要求。但是這個求n值的過程卻有很大不同,他們的不同也代表了兩組(基於分鍾和基於天)的不同:
"-amin n"
2、求浮點數f,用Δt除以1分鍾,f = Δt / 1min;
3、將f的小數部分入到整數部分,得到n。即,不管f是6.0102還是6.8901,n都等於7
"-atime n"
2、求浮點數f,用Δt處以24小時,f = Δt / 24hours;
3、將f的小數部分都舍掉,得到n。即,不管f是6.0102還是6.8901,n都等於6
大家可以多做實驗,試一下。
(八)權限位
很多人都在用windows,從windows系統拷過來的文件經常被加上了可執行權限,比如我現在想把主目錄下所有的后綴名為.txt .pdf .rm並且具有可執行權限位的文件查找出來,該怎么寫呢?
這里就不得不說一說權限位測試項:-perm。-perm支持符號權限位表示法也支持絕對(八進制)權限位表示法,但是最好使用八進制的權限表示法(這只是個建議 ^_^ )。
-perm基本上有下面這幾中形式:
-perm mode File's permission bits are exactly mode.
-perm -mode All of the permission bits mode are set for the file.
-perm /mode Any of the permission bits mode are set for the file.
-perm +mode (此形式已經不推薦使用,功能與/mode相同)
好好理解上面藍色部分,理解了,-perm測試項也就掌握了。
考考你:
看看下面這句話是什么意思?
find . -perm -444 -perm /222 ! -perm /111
現在再來解決本節最開始提出的問題:查找主目錄下所有的后綴名為.txt .pdf .rm並且具有可執行權限位的文件。
$ find ~ /( -name "*.txt" -o -name "*.pdf" -o -name "*.rm" /) /
-not /( -type d -o -type l /) /
-perm /111 -print
(九)文件類型
有一個問題:我只想查找符號連接文件,可是查找結果中卻包括了普通文件、目錄文件等等,不相關的東西太多了,怎么把不是符號連接文件的查找結果去掉?
-type測試項剛好可以滿足你的要求,-type c即可,其中c表示文件類型,find中支持如下類型:
b block (buffered) special
c character (unbuffered) special
d directory
p named pipe (FIFO)
f regular file
l symbolic link;
s socket
D door (Solaris)
針對上面的問題,可以這么寫:
$ find . -name "e100*" -type l -print
./e1000
./e100puk.txt
但是,不要這么寫:
$ find -L . -name "e100*" -type l -print
加上'-L'選項之后,你將查不到需要的東西,除非符號連接已經失效了。
(十)文件大小
前面一再使用-size測試項,這里簡單介紹一下。
-size測試項根據文件的大小查找文件,文件大小既可以用塊(block)來計量,也可以用字節來計量。默認情況下以塊計量文件大小,若想使用字節來計量只需要在數字參數后加c即可。find支持的其他計量方式有:
-size n[cwbkMG],分別表示
‘b’ for 512-byte blocks (this is the default if no suffix is used)
‘c’ for bytes
‘w’ for two-byte words
‘k’ for Kilobytes (units of 1024 bytes)
‘M’ for Megabytes (units of 1048576 bytes)
‘G’ for Gigabytes (units of 1073741824 bytes)
(十一)用戶、用戶組
根據用戶、用戶組來查找文件,這個沒有太多要說的,記住命令格式即可:
-uid n
-user username or uid
-nouser
-gid n
-group gname or gid
-nogroup
(十二)輸出格式
如果你不想查找到你想要的文件事單調的輸出文件名,你可以使用-printf動作項輸出你想要的格式,下面舉幾個-printf動作的參數:
%p 輸出文件名,包括路徑名
%f 輸出文件名,不包括路徑名
%m 以8進制方式輸出文件的權限
%g 輸出文件所屬的組
%h 輸出文件所在的目錄名
%u 輸出文件的屬主名
...
例如:
$ find . -user xixi -printf "%m %p //n"
644 ./phone1/hello.c
644 ./0dfe.c
其余的,看man手冊頁吧。
(十三)執行外部命令
這又是一個很容易出彩的地方。find真是強大,對查找到的文件竟然可以調用外部命令進行處理。-exec動作項就是來完成這個功能的,格式是:
find . EXPR1 -exec command
{} /;
注意:后一個花括號'}'和'/'之間有一個空格。
例如,查找當前目錄下的所有普通文件,並用ls命令輸出:
find . -type f -exec ls -l {} /;
有些操作系統中出於安全考慮只允許-exec選項執行諸如l s或ls -l這樣的命令。
也可以使用-exec動作項的安全模式:-ok動作項。它的功能和語法都跟-exec一樣,只不過它以更安全的模式運行,當要刪除文件時,它會給出提示,讓你選擇到底刪除還是不刪。
例如:
$ find logs -name "*abc*" -ok rm {} /;
使用-exec動作項處理匹配到的文件時,find命令會將所有匹配到的文件一起傳遞給exec執行。但有些系統對能夠傳遞給exec的命令長度有限制,這樣在find命令運行幾分鍾之后,就會出現溢出錯誤。錯誤信息通常是“參數列太長”或“參數列溢出”。這就是xargs命令的用處所在,特別是與find命令一起使用。
xargs的使用格式是:
find PATH EXPR1 EXPR2 | xargs command
利用管道,把find命令匹配到的文件名傳遞給xargs命令,而xargs命令每次只獲取一部分文件而不是全部。這樣它可以先處理最先獲取的一部分文件,然后是下一批,並如此繼續下去。
在有些系統中,使用-exec動作項會為處理每一個匹配到的文件而發起一個相應的進程,並非將匹配到的文件全部作為參數一次執行;這樣在有些情況下就會出現進程過多,系統性能下降的問題,因而效率不高;而使用xargs命令則只有一個進程。另外,在使用xargs命令時,究竟是一次獲取所有的參數,還是分批取得參數,以及每一次獲取參數的數目都會根據該命令的選項及系統內核中相應的可調參數來確定。
例如,要在普通文件中查找文件內容中包含"io"的文件,可以這么寫:
$ find . -type f | xargs grep "io"
Binary file ./a_book_of_c.chm matches
./2234.c:#include <stdio.h>
./3e10.c:#include <stdio.h>
find命令配合exec和xargs可以對所匹配到的文件執行幾乎所有的命令。
(十四)總結
理解並運用find,關鍵是掌握find命令的處理規則(見第五節):遞歸遍歷所指定的目錄樹,針對每個文件依次執行find命令中的表達式,表達式首先根據邏輯運算符進行結合,然后依次從左至右對表達式求值。把這個理解了,需要什么功能查一下man就可以了。
find命令還有好多功能這里沒有涉及到,具體的大家看man手冊頁吧。在任何時候,man都是一個極好的幫助工具。 ^_^
(十五)附
第五節提出的問題,答案如下:
$ find . -size +0c -wholename "*e*[0-9]*" -o /
! /( -name "." -o -name "*phone" /) -prune -name "*.c" -user xixi /
-o -name "*phone"
./e1000
./e100.dat
./phone
./phone/e680gphone
./e100puk.txt
./3e10.c
./phone1
./phone1/hello.c
./phone2
./0dfe.c
./e680phone
這篇文章斷斷續續寫了好久,今天終於基本完工。參考了man手冊頁以及一些網上的資料。
要把自己心中所想有條理的寫出來感覺真是不易,希望對大家有所幫助。
歡迎批評指正。
