簡介
開發新的命令列程式時,不建議在初期就使用編譯語言。相較之下,採用 Perl 或 Python 等高階腳本語言是更理想的選擇。在原型開發階段,核心目標是快速驗證想法可行性,而非追求執行速度。
本文將說明如何使用 Perl 撰寫命令列程式。選擇 Perl 的主因在於它是 Unix 系統的實質標準(de facto standard),絕大多數的 Unix-like 作業系統皆已內建,具備極佳的部署便利性。
單檔案模式
在此模式下,腳本本身即為執行檔。此方法利用了系統的原生特性,因此僅適用於 Unix / Unix-like 系統。
在 Unix 慣例中,命令列程式不需要也不建議加上副檔名(例如 .pl)。
腳本的第一行必須指定 Shebang:
#!/usr/bin/env perl
雖然在某些老舊的 Unix 系統上可能需要更複雜的 Wrapper 寫法,但實務上不建議為了少數極端情境增加複雜度。上述寫法已足以應對大多數現代系統。
接著,為腳本賦予可執行權限:
$ chmod +x cli
完成此步驟後,該腳本即成為可直接運行的命令列程式。
只要程式總行數在數百行以內、程式碼便於上下捲動閱讀時,都適用單檔案模式。一旦程式規模超過此範圍,建議將其重構為專案模式,詳見下一節說明。
專案模式
當程式規模擴大,應將其建構為完整的專案架構。根據專案需求,通常會包含 bin/、lib/、share/ 等子目錄。
專案的執行入口放在 bin/。值得注意的是,入口程式不一定非得用 Perl 撰寫,也可以使用 POSIX sh 腳本,甚至是相容 Windows 環境的 Batch 批次檔。
若入口程式採用非 Perl 的腳本,則可以將核心的 Perl 腳本置於 libexec/。此目錄代表將 Perl 腳本視為內部組件,僅供入口程式呼叫。
相依性管理建議使用 Carton,這能確保專案的套件環境獨立,避免污染目標系統的原生 Perl 環境。
為了方便理解,我們建立了一個範例專案,讀者可直接參考其目錄配置與實作方式。
命令列參數
當程式啟動時,命令列參數會自動傳入 Perl 的內建陣列 @ARGV 中。後續可透過手動走訪或利用現成的函式庫進行解析。
以下是手動解析命令列參數的實作範例:
use v5.36;
for my $arg (@ARGV) {
if ($arg eq '-v' or $arg eq '--version') {
say '0.1.0';
exit;
}
elsif ($arg eq '-h' or $arg eq '--help') {
say "Usage: $0 [option] ...";
exit;
}
elsif (index($arg, '-') == 0) {
say { \*STDERR } "Unknown CLI argument: $arg";
exit 1;
}
# Parse other CLI argument(s) here.
}
# Implement the core logic here.
標準輸入輸出
命令列程式經常需要處理標準輸入(Standard Input)、標準輸出(Standard Output)與標準錯誤(Standard Error),以實現文字資料的串流與管線操作(Pipeline)。在 Perl 中,這三個標準串流分別對應內建的 STDIN、STDOUT 與 STDERR。
參考以下實作範例:
use v5.36;
sub print_help($stream) {
say $stream "Usage: $0 [option] ...";
}
if (scalar(@ARGV) < 1) {
print_help(\*STDERR);
exit 1;
}
在此範例中,print_help 函式接受一個 Typeglob 引用作為參數。透過這種傳遞方式,程式在輸出說明文件時,便能靈活控制並切換文字要導向至 STDOUT 還是 STDERR。
處理標準輸入
在 Perl 中,可以使用 -t STDIN 表達式來檢查標準輸入是來自於互動式的終端機(TTY),還是來自於非互動式的管線(Pipe)或重新導向。
以下範例實作了一個自動將輸入轉換為大寫字母的小程式,它會根據標準輸入的來源(終端機或管線)動態調整運作行為:
use v5.36;
use English;
use Term::ReadKey;
local $OUTPUT_AUTOFLUSH = 1;
# Check whether STDIN is interactive.
my $is_tty = -t STDIN;
if ($is_tty) {
ReadMode('raw');
}
while (1) {
my $c;
if ($is_tty) {
$c = ReadKey(0);
} else {
sysread(STDIN, $c, 1);
}
last if !defined($c) || $c eq '';
# Press ctrl-c to quit the loop.
if ($is_tty) {
last if ord($c) == 3;
}
print uc($c);
}
if ($is_tty) {
ReadMode('restore');
}
在實作互動式輸入時,建議使用 Term::ReadKey 模組。它完美封裝了不同作業系統在處理終端機輸入模式時的底層差異,大幅提升程式的跨平台相容性。
結束狀態碼
命令列程式在執行完畢時,會回傳一個整數作為結束狀態碼(Exit Code)給作業系統。按照 Unix 的通用慣例,0 代表程式正常執行完畢;1 或其他非零值則代表程式異常結束或發生錯誤。
在 Perl 中,可以直接使用 exit; 敘述,在不帶任何參數的情況下預設會回傳 0;若要明確傳回錯誤狀態,則使用 exit 1;(或相應的錯誤代碼)。
移植命令列程式
使用 C 語言開發命令列程式往往較為耗時。如果該程式屬於一次性需求、內部自動化或單一用途的腳本,建議保留 Perl 版本即可,無需大費周章改寫。只有具備高通用性、需要分發給廣大用戶,或是對效能有要求的工具,才具備移植的價值。
若決定進行移植但不想使用 C 語言,C++、Golang 或 Rust 都是優秀的替代方案。開發者可根據團隊的技術棧、生態系支援以及個人喜好,自由選擇最適合的語言。
結語
使用 Perl 開發命令列程式,憑藉的是其在 Unix 系統的高普及率,以及高階語言帶來的開發效率。在初期階段,利用 Perl 快速建構原型並驗證核心邏輯,是具成本效益的作法。
當程式隨著需求增長時,透過合理的專案目錄規劃,並引進 Carton 等現代相依性管理工具,Perl 同樣能勝任中大型命令列工具的開發。不論未來是否需要移植到其他編譯型語言,以 Perl 作為起點,都是一個務實又高效的選擇。