程式扎記: [Perl 學習手冊] CH12 : 檔案測試

標籤

2010年9月4日 星期六

[Perl 學習手冊] CH12 : 檔案測試


前言 :
有些時候你可能想要檢查檔案是否已經開啟, 或是確認一些檔案是否過大與已經一段時間沒有被修改等等. 對於這類資訊的取得, Perl 有一套完整的檔案測試運算符可供使用.

檔案測試運算符 :
在你產生新檔案前, 可能你會需要檢查所指定的檔案是否已經存在, 以免我們誤把有價值的資料給覆寫掉了. 要達到此目的, 我們可以用 -e 檔案測試運算符 :
die "Warn! File name $filename exist!\n" if -e $filename;

另外假設我們測試一個已開啟的檔案代號 (filehandle) : CONFIG, 並假設某個程式設定檔需要一個星期或兩周就更新一次. 如果檔案在過去 28 天內都沒動過, 可能就有問題 :
warn " Configuration expired!\n" if -M CONFIG >28;

接著第三個例子就複雜多了. 假設我們的硬碟快滿, 但是不想買顆新硬碟, 於是我們打算找出既大又很久沒有用到的檔案, 將它們移到備份磁帶上. 所以我們將檔案瀏覽過一遍, 看看哪些是大於 100KB 的檔案, 即使檔案超過此大小, 我們還得確定它已經超過 90 天沒被存取過, 然後才將它搬走 :
  1. my @original_files = qw/ john peter betty wilma pebbles dino bamm-bamm /;  
  2. my @big_old_files;  
  3. foreach my $filename(@original_files){  
  4.     push @big_old_files, $filename   
  5.     if -s $filename > 100_000 and -A $filename > 90;  
  6. }  
檔案測試符組成自連字符和某個字母, 後面接著所要測試的檔案或檔案代號. 大部分的測試都會傳回真或假值, 但也有一些會傳回比較有趣的結果. 請參考下表完整列表 :
檔案測試運算符 | 意義
-r | 檔案或目錄, 對目前 (有效的) 使用者或群組來說, 是可讀的.
-w | 檔案或目錄, 對目前 (有效的) 使用者或群組來說, 是可寫的.
-x | 檔案或目錄, 對目前 (有效的) 使用者或群組來說, 是可執行的.
-o | 檔案或目錄, 對目前 (有效的) 使用者所擁有
-R | 檔案或目錄, 對實際的使用者或群組來說, 是可讀的.
-W | 檔案或目錄, 對實際的使用者或群組來說, 是可寫的.
-X | 檔案或目錄, 對實際的使用者或群組來說, 是可執行的.
-O | 檔案或目錄, 對實際的使用者所擁有的.
-e | 檔案或目錄是存在的
-z | 檔案存在且沒有內容 (對目錄永遠為假)
-s | 檔案或目錄存在而且有內容 (傳回值是以位元組為單位的檔案大小)
-f | 檔案代號是文字檔
-d | 檔案代號是目錄
-l | 檔案代號是符號鏈結
-S | 檔案代號是 socket
-p | 檔案代號是具名導管 (fifo).
-b | 檔案代號是區塊式檔案 (像是一個可掛載的磁碟裝置)
-c | 檔案代號是字符式檔案 (像是一個 I/O 裝置)
-u | 檔案或目錄具有 setuid 屬性
-g | 檔案或目錄具有 setgid 屬性
-k | 檔案或目錄設定了 stiky 位元.
-t | 檔案代號是 TTY 裝置.
-T | 檔案看起來像是 "文字" 檔
-B | 檔案看起來像是 "二元" 檔
-M | 檔案上一次被更改到現在幾天了?
-A | 檔案上一次被存取到現在幾天了?
-C | 檔案的 inode 被更改到現在幾天了?

-r, -w, -x 與 -o 這幾個運算符會測試是對於有效的使用者或群組來檢查相關屬性是否為真. 所為 "有效的" 指的是 "負責" 執行這支程式的人. 上述測試會回報作業系統是否允許我們做某些事, 但系統允許的事不見得就真的可行. 比如說對某個放在 CDROM 之上的檔案做 -w 測試可能為真, 但實際上你無法寫入 CDROM ; 對某個檔案進行 -x 測試時, 也可能的得到可執行的結果, 但是空檔案要怎麼執行?
如果檔案內容不是空的, -s 會傳回一個另外有意義的值, 表示檔案的大小, 單位是位元組 ; 它的值不會是零, 所以為真.
在 Unix 檔案系統中, 實際上只有七種項目, 分別由七種檔案測試運算符來代表 : -f, -d, -l, -S, -p, -b, 和 -c. 任何的項目都應該符合其中一種. 但如果你有個指向某個檔案的符號鏈結, 那麼 -f 和 -l 都會為真. 所以如果你想知道某個檔案是否為符號鏈結, 最好先進行 -l 測試.
檔案時間測試運算符 -M, -A 及 -C (注意到, 它們都是大寫) , 分別會傳回 "自檔案最後一次遭修改, 存取或者 inode 遭改變" 直到現在的天數. 這個時間是個浮點數, 所以兩天又一秒之前被修改的, 可能會回傳像 2.00001 這樣的值. 在檢查檔案的時間記錄時, 可能會的到像 -1.2 這樣的負值, 這表示檔案最後一次存取的時間戳記是在未來 30 小時以後! 實際上檢查時間的原點是被設成在程式開始執行的那一刻, 所以負值可能表示檔案是在主程式執行之後產生的. 或者是被測試檔案時間被設改成未來時間造成.
至於-T 與 -B 則是測試某個檔案是文字檔還是二元檔. 但是對檔案系統有一點經驗的人都會知道, 沒有任何位元會告訴你它是二元檔或是文字檔. 那麼 Perl 是如何辦到的? 答案是 Perl 做弊 : 它會開啟檔案, 檢查檔案開頭的幾千個位元組, 然後做出一個合理的猜測. 如果他看到很多空位元組與不尋常的控制字元, 而且還有設定了高位元, 那麼它看來就像是個"二元"檔. 反之則為文字檔. 因此這種猜測並不完美. 不過如果你只想區分編譯過的檔案與原始檔, 或是將 HTML 檔與 PNG 檔分開, 那麼這兩個測試運算符還算準確與夠用.
你可能以為 -T 與 -B 出現結果必定相反, 因為檔案若不是文字檔, 就該是二元檔. 但是有兩種情況會讓這兩個運算符結果相同 ; 如果檔案不存在, 兩者都會回傳假值, 因為它既不是文字檔也不是二元檔. 在空檔案的情況下, 兩者都會回傳真值, 因為它既是空的文字檔, 也是空的二元檔.
如果檔案測試運算符後面沒寫檔名或是檔案代號, 預設是使用 $_ 裡的檔案名稱. 所以如果要測試一連串的檔名, 以找出哪些是可讀的話, 可以這麼做 :
  1. foreach(@lots_of_filenames) {  
  2.     print "$_ is readable.\n" if -r; #as -r $_  
  3. }  
但是如果你忽略參數, 請特別注意 : 接在檔案測試符之後的, 不論看起來像不像是參數, 都會被視為參數. 比方說你想知道以 KB 為單位的檔案大小, 那麼你可能會這麼做 :
# 檔名放在 $_ 裡
my $size_in_k = -s / 1000; # Error

當 Perl 的解譯器看到斜線時, 它不會認為那是一個除法運算符, 因為它在尋覓 -s 運算符的運算元 (參數), 對它而言那看起來像是正則表示符之雙斜線的起始斜線. 有個簡單方法可以避免此類問題, 那就是使用括號 :
my $size_in_k = (-s) / 1024; # 以 $_ 為預設值

- 測試同一個檔案的多個屬性
我們可以對同一個檔案使用多個檔案測試運算符, 而建立一個複雜的邏輯條件式. 假設我們只想操作具有可讀與可寫屬性的檔案. 我們可以這樣做 :
if(-r $file and -w $file) {...}

然而這是一個代價昂貴的操作, 每當你進行檔案測試時, Perl 會向檔案系統要求該檔案的全部資訊 (事實上, Perl 會呼叫 stat 函式) 儘管當我們進行 -r 測試時已經取得該資訊, 進行 -w 測試時 Perl 會再次像檔案系統要求相同的資訊. 如果你想要對多個檔案測試多個屬性, 這會對性能造成顯著的影響. Perl 有一個特殊的簡寫可以讓我們不要這麼浪費. 虛擬檔案代號 _ (就只是一個底線) 可以使用上一次檔案查詢所取得的資訊. 現在 Perl 只需要進行一次檔案查詢 :
if(-r $file and -w _){...}

其實我們不必將多個檔案測試符與使用 _ 放在一起 :
  1. if(-r $file) {  
  2.     print "Readable!\n";  
  3. }  
  4. if(-w _) {  
  5.     print "Writable!\n";  
  6. }  
然而我們必須確定我們知道上一次所查詢的是哪一個檔案. 如果兩個檔案測試運算之間我們進行了其他工作, 則原先的 "_" 檔案代號則可能不是我們預期的第一個檔案的屬性.

- 疊接檔案運算符
假設我們想要同時測試一個檔案是否可供讀寫, 我們必須分開讀與寫的測試 :
if(-r $file and -w _) {print "The file is both readable and writable!\n";}

如果能夠一次進行所有的測試會簡單許多. Perl 5.10 允許我們在檔案名稱之前疊接檔案測試運算符 :
  1. use 5.010;  
  2. if(-w -r $file) {print "The file is both readable and writable!\n";}  
這個疊接的動作如同先前的例子, 只是語法不同. 儘管看起來檔案測試的順序顛倒了, 不過 Perl 首先會先進行最接近檔案名稱的檔案測試. 疊接檔案測試適合賦雜的情況, 假設我們想要列出使用者所擁有之具可讀取, 可寫入與可執行等屬性的目錄. 我們需要的只是一組正確的檔案測試運算符 :
  1. use 5.010;  
  2. if(-r -w -x -d -o) {print "My directory is readable, writable and executable!\n";}  
如果想要在比較運算符使用傳回值 (也就是傳回是真或是假以外的值) , 則疊接檔案測試運算符就不適用. 我們可能會認為下面的程式碼首先測試 $file 是否為目錄, 然後測試它的大小是否小於 512 位元組, 但事實並非如此 :
  1. use 5.010;  
  2. if(-s -d $file < 512) {print "The directory is less than 512 bytes!\n";} # Don't do this  
事實上你也可能是寫成下面的方式 :
if( (-d $file and -s _) < 512 ) {print "The directory is less than 512 bytes!\n";}

當 -d 傳回假值, Perl 會比較假值與 512, 比較結果為真, 因為假值為 0(0<512), 以上的寫法都會造成維護的盲點, 比較好的方法是將它們寫成獨立的檔案測試可避免預期外的結果, 參考代碼如下 :
if( -d $file and -s _ < 512) {print "The directory is less than 512 bytes!\n";}

關於 stat 與 lstat 函式 :
前面介紹的檔案測試運算符雖然適用來測試特定檔案或檔案代號的相關屬性, 但卻沒有傳回檔案的全部資訊. 比方說沒有任何檔案測試運算符會傳回檔案的連結個數, 或是該檔案擁有者的使用者識別碼 (uid). 想知道檔案所有其他相關資訊, 只需要呼叫 stat 函式 (get a file's status information) . 這個函式會傳回相當豐富的資訊, 和使用 Unix 系統呼叫 stat 所傳回的相同. 函式 stat 的參數可以是檔案代號 (包含 "_" 虛擬檔案代號) 或是某個會傳回檔案名稱的運算式. 它所傳回的值可能是空串列. 這表示 stat 函式執行失敗 (同常是檔案不存在) ; 也可能是個含有 13 個元素的數值串列, 其內容可用底下由純量變數所構成的串列來描述 :
my {$dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks} = stat($filename);

這些變數名稱代表 stat 函式傳回值的結構. 在這裡我們只挑幾個重要的進行說明 :
$dev 和 $ino :
檔案的裝置編號以及 inode 識別碼. 這兩個編號構成了發給這個檔案的牌照(license plate). 即使它具有多個不同的名稱, 裝置編號和 inode 識別碼的組合是獨一無二的.

$mode :
檔案權限位元集合, 如果你以 Unix 命令 ls -l 檢視過詳細的檔案列表, 你會看到其中每一列都是由 -rwxr-xr-x 這樣的字串開始的. 這九個權限位元剛好對應到 $mode 最低效的九個權限位元 ; 以這個例子而言, 其實就是八進制數值 0755. 至於其他位元則指出與檔案資訊相關的其他細節.

$nlink :
這個檔案或目錄 (硬性) 連結數目, 也就是這個項目有多少個真實名稱. 這個數字對目錄來說通常是 2 以上, 對檔案來說則 (通常) 是一. 之後會再詳細介紹, 在 ls - l 結果中, 放在權限位元之後的數值就是 $nlink 的值.

$uid 和 $gid :
以數值呈現檔案擁有者的使用者識別碼與群組識別碼.

$size :
以位元組為單位的檔案大小. 和 -s 檔案測試符的傳回值相同.

$atime, $mtime 和 $ctime :
同前描述的三種時間戳記, 但在這裡是以系統的時間格式來表示 : 一個 32 位元的整數, 表示從紀元 (Epoch) 起算的秒數. 也就是從 西元 1970 標準時間午夜起算. 但不同機器可能會也所差異.

對符號式鏈結的名稱使用 stat 函式, 將會傳回符號連結所指向之物件的資訊, 而非符號本身的資訊 (除非該連結所指到的物件目前無法存取). 若你需要符號連結本身的資訊, 你可以使用lstat (stat a symbolic link) 來代替 stat. 如果 lstat 的參數不是符號連結, 它就會傳回 stat 一樣的資訊.
另外就像檔案測試運算符一樣, lstat 與 stat 的預設運算元式 $_. 也就是說底層的 stat 系統呼叫 會對純量變數 $_ 裡的檔名進行操作.

localtime 函式 :
你所取得的時間戳記值看起來通常會像 1180630097 這樣. 這對大多數人來說不大有用, 除非你想用減法來比較兩個時間戳記. 因此你可能想將它轉換成容易閱讀形式. 在 Perl 可以在純量語境使用 localtime 函式(convert UNIX time into record or string using local time) 來完成轉換 :
my $timestamp = 1180630097;
my $date = localtime $timestamp;


在串列語境下, localtime 會回傳一串數值, 結果可能出乎你意料 :
my($sec, $min, $hour, $day, $mon, $year, $wday, $yday, $isdst) = localtime $timestamp;

$mon 是範圍從 0 到 11的月份值, 很適合來索引月份名稱. 最奇怪的是 $year 是一個從 1900 起算的年數, 所以將這個數值加上 1900 就是實際年分, $wday 的範圍從 0 (星期天) 到 6 (星期六), $yday 則表示目前是今年的第幾天. 還有兩個相關函式可能也很有用. $gmtime 函式(convert UNIX time into record or string using Greenwich time format.) 與 localtime 函式一樣, 但它所傳回的是世界標準時間 (俗稱格林威治標準時間). 如果需要從系統時鐘取回當前時間戳記, 可使用 time 函式 (return number of seconds since 1970) . 不提供參數情況下, 不論 localtime 或 gmtime 函式, 預設都是使用當前時間.
my $now = gmtime; # 取得當前世界標準時間的時間戳記

逐位元操作運算符 :
如果你需要逐個位元進行運算, 像是對 stat 函式所傳回的狀態位元進行處理, 就必須用到逐位操作運算符 (bitwise operator). and 運算符 (&) 會回報在兩邊引數裡, 哪些位元都被設為 1. 例如 10 & 12 的運算結果是 8. 底下列出了相關的位元運算符, 以及它們的意義 :
運算式 : 意義
10 & 12 : and - 那些位元在兩邊都為 1 (結果為 8)
10 | 12 : or - 哪些位元在任一邊為 1 (結果為 14)
10 ^ 12 : xor - 哪些位元在一邊為 1 另一邊為 0. (結果為6)
6 << 2 : 位元左移 - 並以0 來補最低位元. (6 * 2 * 2 = 24)
25 >> 2 : 位元右移 - 25 / 2 = 12, 12 / 2 = 6.
~ 10 : 反轉位元 - 將 0/1 反轉. (結果為 0xFFFFFFF5)

底下例子示範如何對 stat 所傳回的 $mode 進行位元操作 ; 位元操作的結果可以在 chmod 裡使用 :
  1. # mode 是 stat 函式對組態檔傳回結果  
  2. warn "Every one can write into the configuration!\n" if $mode & 0002;  
  3. my $classical_mode = 0777 &$mode;  
  4. my $u_plus_x = $classical_mode | 0100;  # 將一個位元設為1  
  5. my $go_minus_r = $classical_mode & (~ 0044); # 將兩個位元設為0  
- 使用位元字串
位元操作運算符以操作位元字串 (bitstring), 也可以對整數操作. 如果運算元是整數, 結果也會是整數. 不過如果位元操作運算符的任一運算元是字串, 則 Perl 會把它當作位元字串來處理, 換句話說 , "0xAA" | "0x55" 的結果會是 "0xFF" . 注意到這個例子的值都是 "單" 位元組的字串, 而結果是八個位元都為 1 的位元組. Perl 對位元字串的長度沒有限制.
This message was edited 6 times. Last update was at 04/09/2010 15:54:01

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!