程式扎記: [Perl 學習手冊] CH13 : 目錄操作

標籤

2010年9月13日 星期一

[Perl 學習手冊] CH13 : 目錄操作


前言 :
現代的作業系統都提供以目錄來組織檔案的功能, 讓我們可以分門別類存放我們的檔案.

在目錄樹中移度 :
程式執行時會以自己的工作目錄 (working directory) 做為相對路徑的起點. 也就是說當我們說你參照了 john 這個檔案, 其實是指 "位於現行工作目錄下的 john".
你可以用 chdir 函式(change your current working directory) 來改變目前工作目錄. 它和 Unix shell 的 cd 命令差不多 :
chdir "/etc" or die "Can't chdir to /etc directory!\n : $!";

因為這是一個系統呼叫, 所以發生錯誤, 便會設定純量變數 $! 的值. 由於 Perl 程式啟動的所有行程都會承接 Perl 程式的工作目錄. 因此當 Perl 更改工作目錄後將無法影響調用 Perl 的行程. 如果省略 chdir 的參數, Perl 會盡可能使用你目前的主目錄當作目前的工作目錄. 而這是省略參數時不以 $_ 做為預設參數的少數情形之一. 另外有些 shell 允許你使用波浪號 (~) 讓你在呼叫 cd 時可以回到使用者的家目錄, 因為這個功能來自 shell, 作業系統並未提供 ; 因為 Perl 會直接叫用作業系統, 所以 chdir 無法接受目錄開頭的波浪號.

Glob 操作 :
一般來說 shell 會將你命令列裡的檔名樣式展開成所有吻合的檔名. 這就稱為 "Glob 操作". 比方說, 假設你將 *.pm 這個檔名樣式交給 echo 命令, shell 會將它展開成名稱相符的檔案清單 :
echo *.pm
a.pm b.pm c.pm

echo 命令不必知道如何展開 *.pm, 因為 shell 已經將它展開了. 這也適用於 Perl 的程式 :
linux-tl0r:~/perlPrac/learningPerl # cat > [color=blue]show-args # 輸入以下程式碼, 按 Ctrl+D 結束輸入
foreach $arg (@ARGV) {
print "Has $arg\n";
}[/color]linux-tl0r:~/perlPrac/learningPerl # perl show-args *.pl
Has CH01_HelloWorld.pl
Has CH01_P17.pl
Has CH02_P25.pl
...

請注意 show-args 完全不必了解如何進行 Glob 操作 - 這些名稱已經在 @ARGV 裡展開了. 不過有時候可能在程式遇到 *.pm 之類的樣式. 我們可以利用上面的做法將之轉成對應相符的檔案清單嗎? 只要使用 glob 函式 (expand filenames using wildcards) :
  1. my @all_files = glob "*";  
  2. my @pl_files = glob "*.pl";  
其中 @all_files 會取得當前目錄底下所有的檔案 (按字母排序過), 但不包括以點號開頭的檔案 ; 這與 shell 的做法一致. 其實 glob 也可以處理多個樣式 (以空格隔開) :
my @all_files_including_dot = glob "* .*";

其中我們加上了 "點星號" (.*) 參數已取得所有的檔名, 無論它們是否以點號開頭. 並且在引號裡的空格是有意義的 ; 它分隔了兩個要進行 Glob 操作的項目.

Glob 操作的另一種表示法 :
雖然我們談了不少 Glog 操作, 但可能在許多 Glob 操作程式裡, 你可能完全看不到 glob 這個字. 那是因為過去大部分的程式在 glob 函式出現前是使用 "角括號語法" (angle-bracket syntax) 來叫用此功能, 看起來就跟讀取檔案代號差不多 :
my @all_files = <*>; ## 和 glob "*" 完全一樣

角括號裡的值會比照雙引號內的字串, 進行安插 ; 這表示在進行 Glob 操作之前, 角括號內的 Perl 變數會先展開成它們當前的值 :
my $dir = "/etc";
my @dir_files = <$dir/* $dir/.*>;


此處因為 $dir 會被展開成它當前的值, 所以會取得所指定目錄下任何檔案的清單 (含點號開頭的檔案). 這樣說來假如角括號既表示從檔案代號讀取, 又代表 Glob 操作, 那 Perl 如何決定要用哪一種解釋呢? 檔案代號必須是個 Perl 識別字, 所以如果角括號內的項目滿足 Perl 識別字的條件, 就是從檔案代號讀取 ; 否則它代表的就是 Glob 操作.
上述規則唯一的例外就是當角括號內式簡單的純量變數 (不是雜湊或是陣列的元素) 時, 那麼它就是 "間接式檔案代號讀取". 其中變數的值便是所要讀取的檔案代號名稱 :
my $name = "JOHN";
my @lines = <$name>;
 # 從 檔案代號 JOHN 進行間接式檔案代號讀取

Perl 會在編譯時期決定它是 Glob 操作或是從檔案代號讀取, 因此和變數的內容無關.

目錄代號 :
若要從目錄取得檔案清單, 還可以使用 目錄代號 (directory handle). 目錄代號看起來像檔案代號, 使用起來也沒多大差別. 你可以開啟它 (以 opendir 代替 open) 接著讀取它的內容 (以 readdir 代替 readline) , 然後將它關閉. (以 closedir 代替 close ). 不過你所讀取的是目錄裡面的檔名而不是檔案的內容. 例如 :
  1. my $dir_to_process = "/etc";  
  2. opendir DH, $dir_to_process or die "Can't open $dir_to_process: $!";  
  3. foreach $file (readdir DH) {  
  4.     print "$dir_to_process has File:$file \n";  
  5. }  
  6. closedir DH;  
和檔案代號一樣, 目錄代號會在程式結束自動關閉, 或是在重新開啟另一個目錄時自動關閉. 目錄代號和 Glob 操作不同的是它絕對不會啟動另一個行程. 所以對於極度需要電腦運算能力的程式來說, 可以提供更佳的效能. 不過目錄代號也是個低階操作運算符, 意謂我們必須多做點功.
比方說使用目錄代號傳回的名稱串列並未按照特定順序排序, 此外串列將包含所有檔案 (含點號開頭的檔案) 與不能進行樣式的比對. 因此如果我們想要取得 *.pl 的檔案, 則必須在迴圈內進行處理 :
while($name = readdir DIR) {
next unless $name =~ /\.pm$/;

... more actions...
}

上面利用正規表示式來進行樣式比對, 而不是 Glob 操作, 若想要取得不以點號開頭的檔案, 我們可以這麼寫 :
next if $name =~ /^\./;

接下來要說明的是讓最多人困惑的地方, readdir 函式傳回來的檔名並不包含路徑名稱, 它們只是目錄裡的名稱而以. 所以我們不會看到 /etc/passwd, 而是見到 passwd. (這是另一個與 Glob 操作的差別). 所以你得接上路徑名稱, 才有辦法得到檔案全名 :
  1. opendir SOMEDIR, $dirname or die "Can't not open $dirname: $!";  
  2. while(my $name = readdir SOMEDIR) {  
  3.     next if $name =~ /^\./;  #忽略點號檔  
  4.     $name = "$dirname/$name";  #接上路徑  
  5.     next unless -f $name and -r _; # 只留下可讀取的檔案  
  6.     #... more actions.  
  7. }  
若沒有接上路徑, 檔案測試運算符會檢查當前目錄裡的檔案, 而不是在 $dirname 所指的目錄底下. 這是使用目錄代號時容易犯的錯誤.

以遞迴方式列出目錄 :
Perl 隨附有 File::Find 這個好用的程式庫, 可以寫出漂亮的遞迴式目錄處理. 下面是簡單的範例代碼 :
  1. use 5.010;  
  2. use File::Find;  
  3.   
  4. sub wanted{  
  5.     $dir = $File::Find::dir;  
  6.     $fname = $File::Find::name;  
  7.     $name = $_;  
  8.     if(-f $name) {  
  9.         print "Dir > $dir\n";      
  10.         print "Name > $name\n";  
  11.         print "Full Name > $fname\n";  
  12.     }  
  13. }  
  14. push @directories"E:/Temp";  
  15. find(\&wanted, @directories);  
移除檔案 :
在 Unix shell 下我們可以鍵入 rm 命令來移除一個以上的檔案. 而在 Perl 裡我們用的是 unlink 函式( remove one link to a file) :
unlink "john", "peter", "ken"

這會將傳入的三個檔案刪除. 既然 unlink 的引數是一個串列, 而 glob 函式會傳回一個串列, 因此只要我們結合此兩函式就可以一次刪除多個檔案 :
unlink glob "*.o";

這跟我們在 shell 下鍵入 rm *.o 很像, 但是不必另外啟動額外的 rm 行程. 最後 unlink 的傳回值代表成功了刪除多少個檔案. 所以上面的執行結果如果為 3 代表三個檔案都被成功刪除. 但也有可能只有兩個檔案被刪除 (傳回值為2). 而我們沒辦法知道是哪一個無法被刪除, 這時你只能透過迴圈一個個刪除並檢查 $! 才知道為什麼. (unlink 無法刪除目錄, 刪除目錄請使用rmdir)

將檔案更名 :
想為現存檔案取個新名, 使用 rename 函式 是個很方便的做法 :
rename "old", "new";

跟 Unix 的 mv 命令一樣, 這會將名為 old 的檔案更名為同一個目錄下名為 new 的檔案, 你甚至可以將檔案移到別的目錄裡. rename 執行失敗的傳回結果為假, 並且會將系統錯誤訊息傳到 $!裡 , 讓你能夠利用 or die (或 or warn) 來像使用者回報狀況. 底下是一個簡單範例, 讓你將所有 .old 結尾的檔案改名為 .new 結尾 :
  1. foreach my $file(glob "*.old") {  
  2.     my $newfile = $file;  
  3.     $newfile =~s/\.old/.new/;  
  4.     if(-e $newfile) {  
  5.         print "Can't rename while File:$newfile exist!\n";  
  6.     } elsif(rename($file, $newfile)) {  
  7.         # Success  
  8.     } else {  
  9.         warn "Fail to rename $file!($!)\n";  
  10.     }  
  11. }  
建立及移除目錄 :
在現存的目錄下建立新目錄是件很容易的事. 只要調用 mkdir 函式 就行了 :
mkdir "john", 0755 or warn "Can't build john directory: $!";

和前面一樣, 傳回直為真表示成功, 失敗時則會設定 $!. 而第二個參數 0755 代表目錄建立時的權限設定. 將它寫成八進制數值是因為它會被解釋成三位一組的 Unix 權限值. 就算是在 Windows 或 MacPerl 上, 你也須略懂Unix 權限值, 才有辦法使用好 mkdir 函式. 0755 是個不錯的設定, 因為它賦予你完整的權限, 並讓其他人只能讀取並且無法修改.
mkdir 函式並不要求你用八進制寫這個值 - 它只是需要某個數字 (從 Perl 5.6 以後, 你可以省略第二個參數). 但是除非你能快速算出來八進制 0755 等於十進制 493, 否則還是讓 Perl 來算比較方便. 這裡要注意的是當成數字來用的字串即使以 0 開頭, 也不會被解釋成八進制的數值. 所以底下是不通的 :
  1. my $name = "john";  
  2. my $permissions = "0755"; #Error permission value  
  3. mkdir $name, $permissions;  
當然在程式直接指定權限時, 只要用數值代替字串就行了. 通常是在使用者鍵入權限值時, 才會需要額外的 oct 函式(convert a string to an octal number). 舉例來說, 假設我們從命令列取得引數 :
my ($name, $perm) = @ARGV; # 前兩個參數代表目錄名稱以及權限設定
mkdir $name, oct($perm) or die "Can't build $name directory:$!";

$perm 的值一開始會被當成字串處理, 所以 oct 函式將會將它正確解釋成通用的八進制表示法. 想移除空的目錄時, 可以用 rmdir 函式. 它的用法跟 unlink 函式很像. 但是它一次只能移除一個目錄 :
  1. foreach my $dir (qw(john ken peter)) {  
  2.     rmdir $dir or warn "Can't remove directory $dir: $!";  
  3. }  
如果目錄不是空的, rmdir 呼叫會失敗. 你可以先用 unlink 刪除目錄的內容, 再試著移除應該已經清空的目錄. 但是如果目錄裡有子目錄, 使用 unlink 是無法清除的. 這時後你可以參考 Perl 隨附的 File::Path 模組, 裡面的 rmtree 函式提供比較方便的解決方案.

修改權限 :
Unix 的 chmod 命令可用來修改檔案或是目錄的權限. Perl 裡也有類似的 chmod 函式(changes the permissions on a list of files) , 能進行一樣的操作 :
chmod 0755, "john", "ken";

和其他作業系統介面函式一樣, chmod 會傳回成功更改的項目數量 ; 假如只有一個引數, 它也會在失敗時將 $! 設成合理的錯誤訊息. 而第一個引數代表 Unix 的權限值. 這個值通常會被寫成八進制, 理由和 mkdir 相同. 最後 Unix 的 chmod 命令接受以符號表示的權限值 (例如 +x 或 go=u-x), 但是 Perl 的 chmod 函式並不接受.

更改隸屬關係 :
只要作業系統許可, 你可以用 chown 函式來更改一串檔案的擁有者, 以即它們所屬的群組. chown 可同時更改擁有者與所屬群組, 而且必須以數值形式的使用者識別碼及群組識別碼來指定, 例如 :
  1. my $user = 1004;  
  2. my $group = 100;  
  3. chown $user, $group, glob "*.o";  
但如果要處理的不是數值, 而是像 john 這樣的字串呢? 只要利用 getpwnam 函式(get passwd record given user login name) 將名稱轉成數值與相應的 getgrname 函式(get group record given group name) 將群組名稱轉成數值就行了 :
  1. defined(my $user = getpwnam "john") or die "Wrong user name.\n";  
  2. defined(my $group = getgrnam "users") or die "Wrong group name.\n";  
  3. chown $user, $group, glob "/home/john/*";  
chown 函式會回傳受影響的檔案數量, 在錯誤發生時也會設定 $! 的值.

更改時間戳記 :
假如你想要修改某個檔案最近的更改或是修改時間, 你可以使用 utime 函式. 它的前兩個引數是存取時間與更改時間, 後面接的是要些改時間戳記的檔名串列. 時間的格式採用的是內部時間戳記的格式 (也就是前面提到的 stat 函式的傳回格式) . 當前面兩個引數都是 undef, 後面的檔案存取時間與更改時間都會被設成當前的時間. 如果你要讓你的檔案看起來像是前一天更改的, 卻是在當前存取的話, 可以這麼寫 :
  1. my $now = time;  
  2. my $ago = $now - 24 * 60 * 60; # 一天前  
  3. utime $now, $ago, glob "*";  
This message was edited 12 times. Last update was at 06/09/2010 16:38:21

沒有留言:

張貼留言

網誌存檔