目錄

名稱

Filter::Simple - 簡化的原始碼過濾

語法

# in MyFilter.pm:

    package MyFilter;

    use Filter::Simple;

    FILTER { ... };

    # or just:
    #
    # use Filter::Simple sub { ... };

# in user's code:

    use MyFilter;

    # this code is filtered

    no MyFilter;

    # this code is not

說明

問題

原始碼過濾是 Perl 最近版本中一個非常強大的功能。它允許使用者擴充語言本身(例如 Switch 模組),簡化語言(例如 Language::Pythonesque),或完全重新建構語言(例如 Lingua::Romana::Perligata)。實際上,它允許使用者將 Perl 的全部功能當作它自己的巨集語言,遞迴地應用。

Filter::Util::Call 模組(由 Paul Marquess 所撰寫)提供了一個可用的 Perl 介面來進行原始碼過濾,但它通常太過強大,而且不如它可能的那麼簡單。

若要使用此模組,必須執行下列步驟

  1. 下載、建置並安裝 Filter::Util::Call 模組。(如果您有 Perl 5.7.1 或更新版本,則已為您完成此步驟。)

  2. 設定一個執行 `use Filter::Util::Call` 的模組。

  3. 在該模組中,建立一個 `import` 子程式。

  4. 在 `import` 子程式中,呼叫 `filter_add`,傳遞一個子程式參考給它。

  5. 在子程式參考中,呼叫 `filter_read` 或 `filter_read_exact` 來使用來自將 `use` 您模組的原始檔的原始碼資料來「啟動」$_。檢查傳回的狀態值,以查看是否實際讀取任何原始碼。

  6. 處理 $_ 的內容,以按照所需的方式變更原始碼。

  7. 傳回狀態值。

  8. 如果取消匯入您的模組(透過 `no`)的動作會導致原始碼過濾停止,請建立一個 `unimport` 子程式,並讓它呼叫 `filter_del`。請確定步驟 5 中對 `filter_read` 或 `filter_read_exact` 的呼叫不會意外地讀取超過 `no`。實際上,這會將原始碼過濾限制為逐行操作,除非 `import` 子程式對它過濾的原始碼執行一些精巧的預先解析。

例如,以下是 BANG.pm 模組中一個最小的原始碼過濾器。它只會將 `BANG\s+BANG` 順序的每個出現位置轉換為在 `use BANG;` 陳述式之後的任何程式碼中的順序 `die 'BANG' if $BANG`(直到下一個 `no BANG;` 陳述式,如果有)。

package BANG;

use Filter::Util::Call ;

sub import {
    filter_add( sub {
    my $caller = caller;
    my ($status, $no_seen, $data);
    while ($status = filter_read()) {
        if (/^\s*no\s+$caller\s*;\s*?$/) {
            $no_seen=1;
            last;
        }
        $data .= $_;
        $_ = "";
    }
    $_ = $data;
    s/BANG\s+BANG/die 'BANG' if \$BANG/g
        unless $status < 0;
    $_ .= "no $class;\n" if $no_seen;
    return 1;
    })
}

sub unimport {
    filter_del();
}

1 ;

這種精緻程度讓許多程式設計師無法使用過濾。

一個解決方案

Filter::Simple 模組提供了一個簡化的 Filter::Util::Call 介面;它足以應付大多數常見案例。

使用 Filter::Simple,設定原始碼篩選器的任務會簡化為以下步驟,而非上述流程:

  1. 下載並安裝 Filter::Simple 模組。(如果您有 Perl 5.7.1 或更新版本,這項工作已經完成。)

  2. 設定一個模組,執行 use Filter::Simple,然後呼叫 FILTER { ... }

  3. 在傳遞給 FILTER 的匿名子常式或區塊中,處理 $_ 的內容,以所需方式變更原始碼。

換句話說,先前的範例會變成

package BANG;
use Filter::Simple;

FILTER {
    s/BANG\s+BANG/die 'BANG' if \$BANG/g;
};

1 ;

請注意,原始碼會傳遞為單一字串,因此任何使用 ^$ 來偵測行界線的正規表示式,都需要 /m 旗標。

停用或變更 <no> 行為

預設情況下,已安裝的篩選器只會篩選到包含下列三個標準原始碼「終結符號」之一的行:

no ModuleName;  # optional comment

__END__

__DATA__

但是,可以透過傳遞第二個引數給 use Filter::SimpleFILTER 來變更(請記住:當您使用 FILTER 時,初始區塊後沒有逗號)。

第二個引數可以是 qr 的正規表示式(然後用於比對終結符號行),或定義為 false 的值(表示不應尋找終結符號行),或對雜湊的參考(如果這樣,終結符號就是與金鑰 'terminator' 關聯的值)。

例如,若要讓先前的篩選器只篩選到下列形式的行:

GNAB esu;

您會這樣寫:

package BANG;
use Filter::Simple;

FILTER {
    s/BANG\s+BANG/die 'BANG' if \$BANG/g;
}
qr/^\s*GNAB\s+esu\s*;\s*?$/;

FILTER {
    s/BANG\s+BANG/die 'BANG' if \$BANG/g;
}
{ terminator => qr/^\s*GNAB\s+esu\s*;\s*?$/ };

若要防止篩選器以任何方式關閉:

package BANG;
use Filter::Simple;

FILTER {
    s/BANG\s+BANG/die 'BANG' if \$BANG/g;
}
"";    # or: 0

FILTER {
    s/BANG\s+BANG/die 'BANG' if \$BANG/g;
}
{ terminator => "" };

請注意,不論您將終結符號模式設定為什麼,實際終結符號本身必須包含在單一原始碼行中。

全合一介面

將 Filter::Simple 的載入

use Filter::Simple;

與篩選設定分開

FILTER { ... };

很有用,因為它允許在呼叫篩選器之前定義其他程式碼(通常是剖析器支援程式碼或快取變數)。不過,通常不需要這種分隔。

在這些情況下,只要將篩選子常式和任何終結符號規格直接附加到載入 Filter::Simple 的 use 陳述式,如下所示,會比較容易:

use Filter::Simple sub {
    s/BANG\s+BANG/die 'BANG' if \$BANG/g;
};

這與

use Filter::Simple;
BEGIN {
    Filter::Simple::FILTER {
        s/BANG\s+BANG/die 'BANG' if \$BANG/g;
    };
}

完全相同,除了 FILTER 子常式未由 Filter::Simple 匯出。

僅過濾原始碼的特定組成部分

像這樣的過濾器其中一個問題是

use Filter::Simple;

FILTER { s/BANG\s+BANG/die 'BANG' if \$BANG/g };

它會不加區別地將指定的轉換套用於原始程式碼的全部文字。因此,像這樣的內容

warn 'BANG BANG, YOU'RE DEAD';
BANG BANG;

將會變成

warn 'die 'BANG' if $BANG, YOU'RE DEAD';
die 'BANG' if $BANG;

在過濾原始碼時,通常只想要將過濾器套用於程式碼的非字元字串部分,或者反過來說,套用於字元字串。

Filter::Simple 支援這種類型的過濾,方法是自動匯出 FILTER_ONLY 子常式。

FILTER_ONLY 會取得一系列規格說明,這些說明會安裝不同的 (且可能多個) 過濾器,這些過濾器只會作用於原始碼的部分。例如

use Filter::Simple;

FILTER_ONLY
    code      => sub { s/BANG\s+BANG/die 'BANG' if \$BANG/g },
    quotelike => sub { s/BANG\s+BANG/CHITTY CHITTY/g };

"code" 子常式將只用於過濾原始碼中不是類似引號、POD 或 __DATA__ 的部分。quotelike 子常式只會過濾 Perl 類似引號 (包括文件字串)。

完整的選項清單如下

"code"

只過濾原始碼中不是類似引號、POD 或 __DATA__ 的區段。

"code_no_comments"

只過濾原始碼中不是類似引號、POD、註解或 __DATA__ 的區段。

"executable"

只過濾原始碼中不是 POD 或 __DATA__ 的區段。

"executable_no_comments"

只過濾原始碼中不是 POD、註解或 __DATA__ 的區段。

"quotelike"

只過濾 Perl 類似引號 (由 &Text::Balanced::extract_quotelike 解釋)。

"string"

只過濾 Perl 類似引號的字串文字部分 (亦即字串文字的內容、tr/// 的任一半、s/// 的後半)。

"regex"

只過濾 Perl 類似引號的樣式文字部分 (亦即 qr//m// 的內容、s/// 的前半)。

"all"

過濾所有內容。效果與 FILTER 相同。

除了 FILTER_ONLY code => sub {...} 之外,每個組成過濾器都會重複呼叫一次,針對原始碼中找到的每個組成呼叫一次。

請注意,你也可以在單一 FILTER_ONLY 中套用兩個或多個相同類型的過濾器。例如,以下是只套用於正規表示式的簡單巨集預處理器,最後一個偵錯步驟會列印結果原始碼

use Regexp::Common;
FILTER_ONLY
    regex => sub { s/!\[/[^/g },
    regex => sub { s/%d/$RE{num}{int}/g },
    regex => sub { s/%f/$RE{num}{real}/g },
    all   => sub { print if $::DEBUG };

僅過濾原始碼的程式碼部分

當原始碼被分解成字串常數和正規表示式之間的部分時,大多數的原始碼將不再是語法正確的。因此,'code''code_no_comments' 元件篩選器與前一節所述的其他部分篩選器的行為略有不同。

'code...' 部分篩選器並非針對每個單獨的程式碼部分(即引號之間的位元)呼叫指定的處理器,而是針對整個原始碼運作,但會將引號位元(以及在 'code_no_comments' 的情況下,註解)「空白化」。

也就是說,'code...' 篩選器會以佔位符取代每個引號字串、引號、正規表示式、POD 和 __DATA__ 區段。此佔位符的分隔符號是在套用篩選器時 $; 變數的內容(通常為 "\034")。其餘四個位元組是正在取代的元件的唯一識別碼。

這種方法使得撰寫程式碼預處理器變得相對容易,而無需擔心字串、正規表示式等的格式或內容。

為方便起見,在 'code...' 篩選操作期間,Filter::Simple 會提供一個套件變數($Filter::Simple::placeholder),其中包含一個預編譯的正規表示式,用於比對任何佔位符...並擷取佔位符內的識別碼。佔位符可以視需要在原始碼中移動和重新排序。

此外,第二個套件變數(@Filter::Simple::components)包含 $_ 的各種部分清單,這些部分最初被拆分以允許插入佔位符。

套用篩選後,原始字串、正規表示式、POD 等會重新插入程式碼中,方法是將每個佔位符替換為對應的原始元件(來自 @components)。請注意,這表示 @components 變數在篩選器中必須非常小心地處理。@components 陣列會儲存插入 $_ 的每個佔位符的「反向翻譯」,以及佔位符之間的插入原始碼。如果 @components 中的佔位符反向翻譯被變更,那麼在篩選完成後從 $_ 中移除佔位符時,它們也會被類似地變更。

例如,下列篩選器會偵測連接的字串/引號對,並反轉它們連接的順序

package DemoRevCat;
use Filter::Simple;

FILTER_ONLY code => sub {
    my $ph = $Filter::Simple::placeholder;
    s{ ($ph) \s* [.] \s* ($ph) }{ $2.$1 }gx
};

因此,下列程式碼

use DemoRevCat;

my $str = "abc" . q(def);

print "$str\n";

會變成

my $str = q(def)."abc";

print "$str\n";

並因此列印

defabc

使用 Filter::Simple 搭配明確的 import 子常式

Filter::Simple 會為您的模組產生一個特殊的 import 子常式(請參閱 "運作方式"),它通常會取代您可能明確宣告的任何 import 子常式。

不過,Filter::Simple 夠聰明,可以注意到您現有的 import 並對其執行正確的動作。也就是說,如果您在使用 Filter::Simple 的套件中明確定義 import 子常式,則在您安裝的任何篩選器之後,仍會立即呼叫該 import 子常式。

您唯一要記住的是,import 子常式必須在安裝篩選器之前宣告。如果您使用 FILTER 安裝篩選器

package Filter::TurnItUpTo11;

use Filter::Simple;

FILTER { s/(\w+)/\U$1/ };

這幾乎不會造成問題,但如果您透過將篩選子常式直接傳遞給 use Filter::Simple 陳述式來安裝篩選子常式

package Filter::TurnItUpTo11;

use Filter::Simple sub{ s/(\w+)/\U$1/ };

則您必須確保您的 import 子常式出現在該 use 陳述式之前。

同時使用 Filter::Simple 和 Exporter

同樣地,如果您使用 Exporter,Filter::Simple 也夠聰明,可以執行正確的動作

package Switch;
use base Exporter;
use Filter::Simple;

@EXPORT    = qw(switch case);
@EXPORT_OK = qw(given  when);

FILTER { $_ = magic_Perl_filter($_) }

在將篩選器套用到來源後,Filter::Simple 會立即將控制權傳遞給 Exporter,讓它也能發揮作用。

當然,Filter::Simple 在套用篩選器之前,也必須知道您正在使用 Exporter。這幾乎不會造成問題,但如果您對此感到不安,您可以透過確保您的 use base Exporter 永遠在您的 use Filter::Simple 之前,來保證事情會正確運作。

運作方式

Filter::Simple 模組會匯出到呼叫 FILTER(或直接 use 它)的套件中,例如上述範例中的「BANG」套件,兩個自動建構的子常式:importunimport,它們會處理所有令人討厭的細節。

此外,產生的 import 子常式會將其自己的引數清單傳遞給篩選子常式,因此 BANG.pm 篩選器可以輕鬆地變成參數化的

package BANG;

use Filter::Simple;

FILTER {
    my ($die_msg, $var_name) = @_;
    s/BANG\s+BANG/die '$die_msg' if \${$var_name}/g;
};

# and in some user code:

use BANG "BOOM", "BAM";  # "BANG BANG" becomes: die 'BOOM' if $BAM

每次遇到 use BANG 時,就會呼叫指定的篩選子常式,並傳遞該呼叫之後的所有來源程式碼,直到下一個 no BANG;(或您設定的任何終止符)或來源檔案的結尾,以先發生的為準。預設情況下,任何 no BANG; 呼叫都必須單獨出現在單獨一行上,否則將會被忽略。

作者

Damian Conway

聯絡方式

Filter::Simple 目前由 Perl5-Porters 維護。請透過 perl 附帶的 perlbug 工具提交錯誤報告。如需使用說明,請閱讀 perldoc perlbugman perlbug。對於其他事項,請聯絡 <perl5-porters@perl.org>。

CPAN 發布的維護者為 Steffen Mueller <smueller@cpan.org>。如有 CPAN 模組封裝方面的技術問題,請聯絡他。

對於模組的讚美、鮮花和禮物,仍請寄送給作者 Damian Conway <damian@conway.org>。

著作權和授權

Copyright (c) 2000-2014, Damian Conway. All Rights Reserved.
This module is free software. It may be used, redistributed
and/or modified under the same terms as Perl itself.