常用句型


程式範例:

  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 /\// ...