常用句型


程式範例:

  1. rencase: 一次更改許多檔案名稱, 大小寫對調。 (檔名從 stdin 讀)
  2. userinfo: 印出某使用者的公開個人資訊。
  3. datefmt: 把所有 "星期幾" 與 "幾月" 的英文全部改成月份。
  4. length: 數一數每個檔案的每個非空白列各有多少字元 (進階)。

三個使用 =~ 符號的運算子

  1. $var =~ tr/.../.../; 把變數 $var 內的 ... 字元都逐一代換成 ... 字元 兩串 ... 通常長度一樣. 想成是查 "字元典" 翻譯。 (較不常用)
  2. $var =~ s/.../.../; 把變數 $var 內的第一個 ... 子字串整個代換成 ... 子字串。
  3. if ($var =~ m/.../) { ... } 詢問變數 $var 裡面有沒有 ... 這個子字串呢?

注意:

  1. 前兩項功能是破壞性的 (destructive), $var 的內容可能因而改變; 第三項是非破壞性的 (non-destructive)。
  2. 後兩項功能在現實生活中較常用到; 兩者都支援 regular expression
  3. 其實可以用其他標點符號來取代斜線, 只要一句話內前後一致就好。
  4. 其實第三項功能的 m 可以省略掉
  5. 如果 $var 是 $_ 則可以用簡寫, 把 $var =~ 全部省略掉。

代換字串 ... =~ s/.../.../ 時, 可以加上一些選項, 例如 i (ignore) 表示忽略大小寫 (比對成功的條件變得更寬鬆); g (global) 表示全面代換, 不只代換第一個比對成功的子字串。 例如 $x = "...Sea...sea...SEA...sea..." 究竟其中那幾個 sea 會被代換掉呢?

...Sea... ...sea... ...SEA... ...sea...
(都不加) v
i v
g v v
ig v v v v

神奇的內定參數 $_

在很多場合下, 參數可以省略不寫, 而此時 perl 自動以 $_ 作為內定參數. 例如:

  1. 用 <FH> 從檔案讀入的一列, 自動存在 $_ 中.
  2. 許多運算子的參數, 例如 tr, s, m.
  3. 許多函數的參數, 例如 print, chomp, split, ...
  4. foreach 的 dummy variable.

檔案讀寫

  1. 用 open 開啟, 用 close 關閉. Open 的第一個參數叫做 file handle, 是你自己命名的檔案代號. 從 open 以後, 都用這個 file handle 來傳給處理檔案函數當做參數. File handle 習慣上用大寫字串.
  2. 開啟時, 檔名前面加 < 表示要讀; 加 > 表示要寫入, 會毀掉原內容; 加 >> 也表示要寫, 但新資料接續在原有的資料之後;
  3. STDIN, STDOUT, STDERR 這三個特殊的 file handles 不必 open 與 close.
  4. 最簡單的「讀取檔案」句型: while (<ABC>) { ... } 理解為: 「每次從 ABC 這個 file handle 讀取一列, 放入 $_ 這個變數裡面, 然後 ... (處理 $_), 直到檔案讀完, 沒有資料為止 (迴圈自動結束).」 注意: <ABC> 語法脫離 while 迴圈單獨使用的狀況比較複雜, 詳見 perlop(1) 的 I/O Operators 單元. 目前請固定將它放在 while 迴圈的 (...) 當中.
  5. 寫到檔案去: print ABC "good morning ", $name, "\n"; 注意: 這裡要把整句話理解成三段: print, 檔案, 以及用逗點分開的一串資料. 檔案與資料之間不可以有逗點! 所以平常的 print 相當於 print STDOUT ...

另外, 因為有好幾次看到有些同學或朋友不約而同提出這個 (錯誤的) 寫法, 所以特別說明一下:

      錯! open F, "...";              錯!
        錯! open G, "...";              錯!
        錯! while (<F>) {               錯!
        錯!     \..   /                 錯!
        錯!     while/(<G>) {           錯!
        錯!       \ /..                 錯!
        錯!        X...                 錯!
        錯!       / \..                 錯!
        錯!     }/   \                  錯!
        錯!     /..   \                 錯!
        錯! }                           錯!
        錯! close G;                    錯!
        錯! close F;                    錯!

可以看出程式作者希望同時處理兩個檔案, 每從 F 讀一列, 就要從頭到尾把 G 掃描一遍; 再從 F 讀一列, 又把 G 掃描一遍。 這個邏輯本身沒有問題, 就像兩個 while 迴圈或兩個 for 迴圈疊在一起一樣。 問題是: G 只 open 一次! 於是讀到 F 的第二列時, G 的檔案指標還在最尾巴, 沒有回頭。 所以從此以後, 內層迴圈每執行必 false -- 都執行零次。 解決之道有二。 其一是將 G 的 open 與 close 移到 F 的迴圈內。 不過這樣重複讀檔, 效率可能比較低。 較佳的方式是: 看看兩個檔案通常是誰比較小? 把較小的檔案一口氣讀入一個陣列, 然後只對較大的檔案用 while, 像這樣:

      open G, "...";
        @G_data = <G>;
        close G;
        open F, "...";
        while (<F>) {
            foreach (@G_data) {
                ...
            }
        }
        close F;

不指定檔案, 讓 perl 替你傷腦筋

這句話 while (<>) { ... } 與 while (<ABC>) 這類句型意義類似, 但後者只針對 ABC 這個單一的檔案處理; 而前者則不指定要處理那個檔案, 作用是:

  1. 若使用者未在命令列上給參數, 則你程式的效果相當於 while (<STDIN>) { ... } 也就是「癡癡地等, 每次從鍵盤上讀取一列 ...」
  2. 若命令列上有參數, 則把命令列上的每個參數當做一個檔案名稱, 從第一個檔案的第一列開始讀起, 每個檔案讀完後, 依序讀下個檔案, ... 彷彿所有檔案的內容串成一個檔案一樣, 迴圈一直執行到最後一個檔案的最後一列讀完為止. Perl 會自動幫你 open/close 每個檔案, 而 $ARGV 內則存有 "目前正在處理的檔案" 的名稱.

聽起來很複雜; 用起來很簡單: 這樣的安排可以讓我們寫的 perl 程式與許多系統工具一樣 (例如 sort, grep, ...), 既可處理一般檔案, 又可當做 filter 放在 pipe 當中, 而程式設計師 (我們) 卻不需要操心如何分開處理這兩種不同的狀況. 此外, 處理一般檔案時, 我們不必多費心, 自然就可以一次處理很多個檔案.

隱含迴圈

  1. 若在命令列上加上 -n 選項, 就彷彿在你的程式最外面包上一個 while (<>) { ... } 迴圈一樣. 換句話說, 你只需要寫迴圈裡面的部分, 專心思考「如何處理一列」就好了.
  2. 若在命令列上加上 -p 選項, 就彷彿在你的程式最外面包上一個 while (<>) { ... print; } 迴圈一樣. 換句話說, 效果類似 -n, 但在迴圈最底部更把 $_ 的值印出來. 以上說明稍微簡化, 不完全正確; 詳請參閱手冊 perlrun(1).
  3. 因此我們經常可以用 perl -ne ... 來取代 shell 底下的 grep 命令; 而用 perl -pe ... 來取代 sed 命令.
  4. 使用 -p 或 -n 時, 如果需要在進入迴圈之前/出了迴圈之後, 先/再多做一些事, 可以用 BEGIN { ... }END{ ... } 例如宣告變數, 可能就需要放在 BEGIN 之內; 列印最後統計的結果, 可能就需要放在 END 之內. 詳見 perlmod(1)
  5. Q: 本篇最前面的範例程式 length, 如果這麼使用: ./length this is a book 會印出什麼? 下例中的迴圈版求和程式, 如果這麼使用: perl -ne '...' 23 45 99 會發生什麼事? 會印出幾個總和?

幾個範例: 左邊是 「完整版」, 右邊是 「隱含迴圈版」

#!/usr/bin/perl -w
while (<STDIN>) {
    print length($_),"\n";       perl -ne 'print length($_), "\n"' < 檔案
}

----------------------------------------------------------------------

#!/usr/bin/perl -w
while (<STDIN>) {
    $sum += $_;                  perl -ne '$sum+=$_;END{print"$sum\n";}' < 檔案
}
print "$sum\n";

----------------------------------------------------------------------

#!/usr/bin/perl -w               #!/usr/bin/perl -wn
while (<STDIN>) {
    chomp $_;                    chomp $_;
    $oldfn = $_;                 $oldfn = $_;
    $_ =~ tr/a-zA-Z/A-Za-z/;     $_ =~ tr/a-zA-Z/A-Za-z/;
    rename $oldfn, $_;           rename $oldfn, $_;
}

Here Document

當你發現你的程式寫成一連串的 print "..." 時, 可以用 here document 來化簡, 直接寫出要印的東西就好, 省略掉重複的 print 敘述和一大堆引號.

  1. 在第一個 print 之後用 <<"name" (name 是你自己隨便取的一個名字) 從此以下都當成要印的資料 (而不是要執行的程式), 一直到 name 再度出現為止.
  2. << 與 name 之間不可以有空格.
  3. 標示結束的 name 必須單獨出現在一列, 前後不可以有空格.
  4. 這當中大部分東西都會原封不動地印出來, 但遇到 $... 及 @... 還是會造成變數代換. 如果當初是用 <<'name' 那麼就連變數代換都不做了.
  5. 詳見 perlfaq4(1) 的 "Why don't my <<HERE documents work?" 與 perldata(1) 的 "here-doc"

其他常識

  1. 不論是 array 或 hash, 設定初始值時都是用 小括弧!
  2. 現在知道 "Use of uninitialized value at line ... chunk ..." 這個錯誤訊息的意思了嗎? line 與 chunk 是指錯誤發生時執行到程式的第幾列, 正在處理資料的第幾列. 務必養成 從錯誤訊息當中學習 的習慣, 進步才快. 如果你用到一個未曾設定初始值的變數, 就會出現這個訊息. 但是 ++, --, +=, -=, ... .= 等等運算子容許未設定初始值的變數 (想想也蠻合理的). 要判斷一個變數是否有定義, 可以用 defined. 見 perlfunc(1).
  3. 如果你的程式只讀一個資料檔, 用 while (<>) 或許比「把程式開啟的檔案名稱寫死」要好, 因為
    1. 使用者可以自由決定要選用 (處理) 那一個資料檔, 甚或不要用檔案當做輸入資料, 而是用來自 pipe 的資料.
    2. 我們不必自己開啟/關閉檔案, perl 會代勞
    3. 自動可以一次處理多個檔案
    4. 有 -n 與 -p 可以幫助我們隱藏迴圈
  4. split 真的非常好用. 不只是字元可以用來分隔欄位, 字串也可以. 也不只有固定的字串才能用, 甚至可以是 regular expression. 但要注意: 若寫 split / /, ... 則連續的兩個空格之間算做有一個空欄位; 若寫 split /\s+/, ... 則連續的空格算做一個分隔符號; 若寫 split " ", ... 意思與 split /\s+/ 相同, 而且字串開頭的地方如果有空格會被自動忽略. 總之, 比較常用的兩種寫法是: split /:/, ... (分隔符號不是空格時) 與 split " ", ... (分隔符號是空格時)
  5. 與 split 功能正好相反的是 join, 可以把一個陣列的所有元素串在一起, 變成一個字串.

作業

  1. 請試著用老實的方法模擬 while (<>) { ... } "如果命令列上沒有參數... 如果有 ..." 你就會了解 while (<>) { ... } 的語法幫你省了多少程式碼.
  2. 請解釋這句話的意義: perl -ne 'print if /abc/' data.txt 提示: 先把它改寫成完整的 perl 程式.
  3. 請解釋這句話的意義: perl -pe 's/abc/xy/g' data.txt 提示: 先把它改寫成完整的 perl 程式.
  4. 請解釋 上一篇 當中, 精簡版 get_field 的意義。
  5. 寫一個程式分析 last 命令的輸出, 統計 (月初以來) 每個使用者曾經上機多少次, 總共上機多少時間. 提示: 可能要用到 split(" ", ...) 與 substr
  6. 寫一個程式, 將文字檔當中所有的全形標點符號改成半形標點符號。
  7. 請分析 rpm -qa --qf '%12{SIZE} %-12{NAME} %{URL}\n' 根據套件來源網址的最高層網域 (網址尾巴, 例如 .edu .org .com ... 等等) 統計來自每個網域的套件個數, 與該網域所有套件大小總和。 提示: 要用到三次 split; 又, 因為 / 與 . 都有特殊意義, 所以拿它們當分隔符號時, 前面必須加上倒斜線, 像這樣: ... split /\// ...

Perl 語言

  1. 新手上路
  2. 基本要素
  3. 餵資料
  4. 常用句型
  5. regexp
  6. 詳談變數
  7. 一語中的
  8. 副程式
  9. 模組
  10. 外界對話

附錄

  1. 參考資料
  2. scripting
  3. Windows
  4. 圖形介面
  5. big-5 碼