Perl 作爲命令行實用程序(轉)

http://www.oioj.net/blog/user1/1713/archives/2005/29557.shtml

Teodor Zlatanov([email protected]
程序員,Northern Light
2001 年 4 月

那些將 Perl 用作編程語言的人經常忽視了:Perl 用作命令行操作的快速而又難看的腳本編制引擎時是很有用的。通過命令行,Perl 僅用一行就可以實現大多數其它語言需要數頁代碼才能完成的任務。跟着 Teodor,他會教給您一些有用的示例。
爲了完成這一篇 how-to 文章,您需要在系統上安裝 Perl 5.6.0。您的系統最好安裝比較新(2000 或更新)的 Linux 或 Unix,但是其它操作系統也能照樣工作。所有的示例都使用 tcsh shell(儘管 bash 及其它 shell 也能工作)。雖然這些示例也許可以和較早版本的 Perl、Linux 及其它操作系統一起工作,但是如果它們不能一起工作,那麼它們無法工作的原因可以作爲練習,讓讀者去解決。

我想說的第一點是:有經驗的程序員不應迴避快速而又難看的解決方案。在其它專欄文章中,我已經強調了文檔編制和徹底性。本專欄文章將集中在編程的消極面,其中文檔編制是可選的,而咖啡因卻無從選擇。因爲我們已經身陷其中。

第二點和第一點一樣重要:快速而又難看的解決方案很難正確完成。如果您知道如何記錄、測試和調試完整的腳本,那麼您就非常有可能在一行程序中取得成功。如果您不知道怎樣做,那麼這就像是企圖用鯡魚來砍倒紅杉樹(而您的技能就是那條鯡魚)。

第一步,您應該學習 shell 的特性:Unix 將命令行參數傳遞給 Perl 的方式及這些參數的 Perl 解釋方法。

命令行的實質
在 Unix 中您將看到可執行任務的概念,一個進程通常是裝入內存的程序。除了初始進程外,進程都可以由其它進程來啓動,初始進程通常是由內核(有時由內核進程)來啓動的。就用戶的觀點而言,啓動進程需要 shell 或啓動程序。因此,當用戶在 shell 命令行輸入“xeyes”或者從啓動程序菜單(類似於 GNOME 任務欄)選擇 X Eyes 應用程序時,shell 或啓動程序創建新的進程以運行該程序。

進程獲得命令行參數。因此,例如,“perl”和“perl -w”是對同一個程序的兩種不同調用。在內部,Perl(類似於 C)將參數傳遞給它用 @ARGV 數組解釋的腳本。但是和 C 不同的是,Perl 偷偷地從腳本中“竊取”其中一些參數以用於自己的用途。例如,正在解釋的腳本看不到傳給 Perl 解釋器的“-w”參數,除非腳本看來需要它。shell 用空格字符隔開參數。

傳給 Perl 的“-e”參數告訴 Perl 獲取命令行中“-e”後的任何內容並將它當作腳本來運行。“-M”參數表示獲取其後的任何內容並將該內容作爲模塊導入,類似於正規腳本中的“use ModuleName”。請參閱 perldoc perlrun 頁面以獲取有關 Perl 必須從命令行提供的開關的更多信息。

可能最好在這裏舉些示例。根據本專欄文章的精神,讓我們使用一行程序。腳本的 -MData::Dumper -e'print Dumper -@ARGV' 部分只是打印出了 @ARGV 數組的內容。

清單 1. 命令行參數

# at the command line, type each line after the '>'
# and you'll get the output that
# follows it

# print the @ARGV contents with no program arguments
> perl -MData::Dumper -e'print Dumper /@ARGV'
$VAR1 = [];

# print the @ARGV contents with arguments "a" and "b"
> perl -MData::Dumper -e'print Dumper /@ARGV' a b
$VAR1 = [
          'a',
          'b'
        ];

# print the @ARGV contents with warnings on, and arguments "a" and "b"
> perl -w -MData::Dumper -e'print Dumper /@ARGV' a b
$VAR1 = [
          'a',
          'b'
        ];

# print the @ARGV contents with arguments "a", "b", and "-w"

# note how the -w is not stolen by Perl if it follows arguments
# that Perl knows it doesn't want

> perl -MData::Dumper -e'print Dumper /@ARGV' a b -w
$VAR1 = [
          'a',
          'b',
          '-w'
        ];
Here is the final line that includes some <angle brackets>



除非您的 shell 限制了參數的數量或長度,不然您可以向 Perl 傳遞任意數量的參數。在 Perl 中打開神奇的文件句柄(filehandle)<>,這會將傳送給 Perl 的每個參數作爲文件名打開並逐行讀取每個文件的內容。缺省情況下,$_ 變量會保存每一行。

Shell 使引號之間的所有內容都成爲一個參數。這就是爲什麼在清單 1 中我們可以寫成 -e'print Dumper /@ARGV' 並且 Perl 可以將其看成單個一行程序腳本的原因。單引號更好,因爲使用單引號後您可以在一行程序內使用雙引號。Perl 中的雙引號用於解釋雙引號之間的任何內容。另一個示例或許會有助於進一步說明這一點:

清單 2. 單引號 vs. 雙引號

# print the Perl process ID, followed by a newline
> perl -e'print "$$/n"'
2063

# error: the first two double quotes go together, the rest is passed
# to the script directly

> perl -e"print "$$/n""
Bareword found where operator expected at -e line 1, near "1895n"
        (Missing operator before n?)
syntax error at -e line 1, next token ???
Execution of -e aborted due to compilation errors.



用 bash 比用 tcsh 要好些,因爲 bash 允許內部的雙引號用 / 字符進行轉義。但是 shell 仍然在將雙引號內的 $$ 傳遞給 Perl 之前對其進行解釋。結論是:不要使用雙引號來指定以 -e 開始的一行程序腳本參數。請參閱 perldoc perlrun 以獲取更多的詳細信息,但是您主要應清楚什麼在系統上有效並堅持下去。

到目前爲止您已經瞭解了 -e 和 -M 開關所起的作用:導入模塊和運行語句。下面我列出了一些有用的其它開關;爲了不把您搞糊塗,所以省略了那些更復雜的開關。請參閱 perldoc perlrun 以獲取完整的列表和一些使用想法。

整潔性 -w 打開警告
-Mstrict 打開嚴格編譯指示(pragma)


數據 -0 (這是個零)指定輸入記錄分隔符
-a 將數據分割成名爲 @F 的數組
-F  指定分割時 -a 使用的模式(請參閱 perldoc -f split)
-i  在適當的位置編輯文件(請參閱 perldoc perlrun 以獲取大量詳細信息)
-n  使用 <> 將所有 @ARGV 參數當作文件來逐個運行
-p  和 -n 一樣,但是還會打印 $_ 的內容


執行控制 -e 指定字符串以作爲腳本(多個字符串迭加)執行
-M 導入模塊
-I 指定目錄以搜索標準位置前的模塊


文件操作
假定您在一個目錄中有一些文件需要用特定的方式重命名。例如,所有包含單詞“aaa”的文件應進行重命名,用單詞“bbb”進行代替。我們將不使用 Unix“mv”命令,因爲用 Perl 的 rename() 函數來重命名文件已經相當不錯了(請參閱 perldoc -f rename 以獲取當使用 rename() 出問題時的詳細信息)。

請參閱清單 3 以獲取將文件從 aaa 重命名爲 bbb 的一行程序腳本。

find . 命令打印出當前目錄下的所有文件和目錄列表。如果您只想要查看文件,那麼就給 find 添加“-type f”參數。獲取 find 的輸出(一個文件列表)並將其傳遞給一行程序。

一行腳本使用 -ne 參數,該意味着它會被重寫成:

清單 4. 將文件從 aaa 重命名爲 bbb(已分解)

while (<>)
{
chomp;                                 # trim the newline from the filename
next unless -e;                        # the filename ($_) must exist
$oldname = $_;                         # $oldname is now $_
s/aaa/bbb/;                            # change all "aaa" to "bbb" in $_
next if -e;                            # the new filename mustn't exist
rename $oldname, $_;                   # rename the old to the new name
}



正如您所看到的那樣,這是個相當複雜的七行腳本。-n 開關簡化了很多東西。但是儘管如此,您還是必須知道 $_ 變量和 s/// 及 -e 運算符(請參閱 perldoc perlop 頁面以獲取詳細信息)。File::Find 標準 Perl 模塊本來可以代替 Unix find 命令用於進行文件查找,但是腳本也會隨之變得太大而不再是一行程序了。

一行程序巧妙地平衡了有用性和複雜性,您必須準備好在需要時將它們重寫成實際腳本,而不應讓程序過於麻煩而無法控制。

下面是文件處理的另一個示例:用已知的命名結構瀏覽 MP3 文件的目錄並抽取專輯名。讓我們假設文件名是“Artist-Album-Track#-Song.mp3”。

清單 5. 查找 Artist-Album-Track#-Song.mp3 的專輯名

> find . -name "*.mp3" | perl -pe 's/.///w+-(/w+)-.*/$1/' | sort | uniq



這個腳本非常簡單。它依靠 find 的行爲,總是在每個文件名前打印“./”。隨後它僅用專輯名代替 $_,並且 -p 開關自動打印專輯名。最後,按順序的 sort 和 uniq 確保了重複的專輯名只打印一次。所有的 find、sort 和 uniq 調用都可以用 Perl 完成,但是在操作系統已經爲我們編寫了這一切時爲何還煩惱呢?作爲練習這會很有趣,但是實際上一行程序可能會變成 20-30 行不必要的代碼。

讓我們分解 Perl 腳本(用一種簡化的方式 - 省略 -p 開關的一些複雜性):

清單 6. 查找 Artist-Album-Track#-Song.mp3 的專輯名(已分解)

while (<>)
{
s/.///w+-(/w+)-.*/$1/;                 # extract the album name into $_
} continue
{
print;                                 # print the album name
}



此外,請注意 Perl 是如何成爲 find、sort 和 uniq 之間的中間工具的。不必嘗試用 Perl 編寫所有東西。您可以這麼做,有時也必須這麼做,但一行程序可以重用。還有,看看正則表達式是多麼的簡單。當然,如果 MP3 文件未正確命名,那麼我們可能會獲得一些異常的專輯名,但是這值得去盡力完善正則表達式嗎?如果您需要做大量工作,那麼或許該使用 CPAN MP3 ID3 標記模塊,而不是解析文件名。要明白:在什麼時候一行程序會成爲一樁麻煩事,而不是一個工具。這就是我在前面說到在開始使用一行程序之前應該非常清楚 Perl 時所指的意思。在編程方法中使用所有工具會使您成爲一名優秀的 Perl 程序員,同時也成爲一名優秀的程序員。

數據操作
上面的概念同樣適用於數據操作。您還應記住 -i 開關,因爲它讓您適當地編輯文件,極少有工具能完成這一任務。下面說的是您將如何編輯文件內容,用“bbb”代替每個“aaa”:

清單 7. 編輯文件內容以用“bbb”替換“aaa”

> cat test
aaa
bbb
ccc
ddd
aaa
> perl -pi -e's/aaa/bbb/' test
> cat test
bbb
bbb
ccc
ddd
bbb



當然我們可以使用任何正則表達式來替換“aaa”。

請注意,我們使用 -p 開關爲每行打印 $_。這是必需的,因爲 Perl 腳本的輸出就是文件的內容!這意味着我們可以玩一些有趣的小伎倆。例如:

清單 8. 在文件中插入行號

> perl -pi -e'$_ = sprintf "%04d %s", $., $_' test



這個腳本在文件中的每一行前面插入 4 位數的行號。如果您對查看語法感到頭疼,那麼盯着離您最近的人並問他們是否知道有關動物園裏兩頭駱駝的笑話。他們會用重器敲您的頭,這會暫時分散您頭疼的感覺,之後您可以重新工作了。

現在要處理更棘手的事了。我們將使用 Uri Guttman 優秀的 File::ReadBackwards 模塊反向查看日誌文件以尋找某些有趣的事件(您必須從 CPAN 安裝 File::ReadBackwards)。我們將搜索字符串“sshd”以查閱來自 sshd 守護程序的所有通知。

清單 9. 向後查看文件以尋找 sshd 消息

> perl -MFile::ReadBackwards -e'foreach my $name (@ARGV) /
   { $f = File::ReadBackwards->new($name) || next;       /
     while( $_ = $f->readline ) {print $_ if m/sshd/}}'  /
  /var/log/messages



每一行尾部的 / 字符告知 shell 後面還有內容;該行還未結束。這個 3 行腳本在您必須將其重寫成實際腳本前大約和一行程序一樣大。通過保存這個文件中的所有行並反向打印它們,可以用更少的代碼達到同樣的效果,但是這不及 File:ReadBackwards 有效,後者實際上反向讀取文件並在新行上停下來。通過命令行不太容易達到這個效果。

但是爲何在此處停下來呢?讓我們抽取 sshd 日誌消息中所提及的所有 IP 地址。

清單 10. 反向查看文件以查找 sshd 消息中的 IP

> perl -MFile::ReadBackwards -e'foreach my $name (@ARGV) /
   { $f = File::ReadBackwards->new($name) || next;       /
     while( $_ = $f->readline ) /
     {print "$1/n" if m/sshd/ && m/connection from/D*([/d.]+)/ }}' /
  /var/log/messages



這十分糟糕!我們現在就將其移到實際腳本中。

請注意,上面的正則表達式如何只捕獲“connection from”和一個非數字的字符串後面的數字和點。這還不完善,但是它在使用 IPv4 地址的實際情況中能很好地工作。您應該理解您的一行程序需要什麼,並正確執行。不要在用過就丟棄的腳本的設計上花太多的功夫。您會感到很難過。相反,要清楚腳本何時不會被丟棄,並編寫相應的代碼!

實際示例
我的妻子曾經在 Windows 中重新命名我們在假期中拍攝的大量照片。類似於“Our Christmas Tree.jpg”這樣的文件名很好。當我嘗試運行 indexpage.pl - 一個創建用於圖像集的 HTML 頁面的 Perl 腳本時,這個腳本無法工作。作者沒有仔細考慮過文件名,引號和空格引起了問題。

我使用了一個一行程序,而不是自己動手修正 indexpage.pl(這是個很好的練習,但是像我這樣的人在凌晨兩點是沒空的)。請參閱清單 11 以獲取重命名 JPG 文件的一行腳本。

這有些棘手,因爲我在腳本中不能使用單引號。最後我使用了單引號的 ASCII 值 - 39,將單引號放在 $quote 變量中,並用它進行間接置換。

這打印出了一連串的“mv”命令,我可以檢查它們以確保我做對了。最後,我將這些命令保存到一個文件中並使用 shell“source”命令運行文件中的每個命令。清單 12 顯示了正在進行 JPG 重命名。

重命名後,indexpage.pl 腳本運行得很好。

結束語
我希望您現在能明白編寫一行程序並不是那麼容易的。在您接觸一行程序之前先完善您的腳本編制技能,否則您在爲使它們正確運行而進行的工作中會遇上許多麻煩。請確保您瞭解您的正則表達式、流控制和缺省變量操作。

使功能和易讀性平衡。一行程序應該像原型那樣被丟棄。否則您會再次看到它們,就像一隻漂亮的獅子狗走掉了,回來的卻是 Cujo。

一行程序的常規使用中所遇到的一些異常是可接受的。它們是一次性的東西,並非金字塔。

您永遠不應立即運行一行程序;在您真正運行命令之前應該總是先打印出該命令會執行什麼內容。這樣您的頭上會少很多白頭髮。

請省着點使用一行程序技能。對付這樣的“野獸”時是最好不要掉以輕心。

最後,有一些趣事。一行程序是使 Perl 爲您幹苦差事的最好辦法。查看專用於 Perl 的 Usenet 新聞組和郵件列表以獲取一些看法和批評。

參考資料

通過單擊本文頂部或底部的討論參與有關本文的論壇。
CPAN 就是綜合 Perl 檔案網絡(Comprehensive Perl Archive Network)。它旨在包含您需要的所有 Perl 資料。到 2001 年 1 月爲止,它包含了 749 兆字節的信息,在全球有一百多個鏡像站點。
訪問 Perl.com 以獲取 Perl 信息和相關參考資料。這個網站包含了 Perl 社團感興趣的所有內容。
以下 perldoc 頁面很有用:perlrun、perlop。
Programming Perl,第三版,由 Larry Wall、Tom Christiansen 和 Jon Orwant 合著(O'Reilly & Associates 2000),是現今最好的 Perl 指南,現在的最新版本是 5.005 和 5.6.0。
Unix Power Tools,第二版,由 Jerry Peek、Tim O'Reilly 和 Mike Loukides 合著(O'Reilly & Associates 1997),是着手學習 UNIX shell 和相關工具的極佳指南。雖然該書有點過時了,但仍然很優秀。
訪問 O'Reilly & Associates - Programming Perl 和許多其它好書的出版社。
關於作者
Teodor Zlatanov 於 1999 年獲得了波士頓大學計算機工程的碩士學位。他從 1992 年就開始擔任程序員,使用過 Perl、Java、C 和 C++。他的興趣在於有關文本解析、3 層客戶機-服務器數據庫體系結構、UNIX 系統管理、CORBA 和項目管理的開放源碼工作。歡迎通過電子郵件提供建議和指正。可以通過 [email protected] 與 Teodor 聯繫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章