位元詩人 用 Perl 撰寫命令列程式

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

簡介

開發新的命令列程式時,不建議在初期就使用編譯語言。相較之下,採用 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 中,這三個標準串流分別對應內建的 STDINSTDOUTSTDERR

參考以下實作範例:

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 作為起點,都是一個務實又高效的選擇。

關於作者

位元詩人 (ByteBard) 是資訊領域碩士,喜歡用開源技術來解決各式各樣的問題。這類技術跨平台、重用性高、技術生命長。

除了開源技術以外,位元詩人喜歡日本料理和黑咖啡,會一些日文,有時會自助旅行。

近期在學習韓文,並將語言學習的心得轉化為開源專案,回饋社群。

這裡是位元詩人的 GitHub 個人頁