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 介面來進行原始碼過濾,但它通常太過強大,而且不如它可能的那麼簡單。
若要使用此模組,必須執行下列步驟
下載、建置並安裝 Filter::Util::Call 模組。(如果您有 Perl 5.7.1 或更新版本,則已為您完成此步驟。)
設定一個執行 `use Filter::Util::Call` 的模組。
在該模組中,建立一個 `import` 子程式。
在 `import` 子程式中,呼叫 `filter_add`,傳遞一個子程式參考給它。
在子程式參考中,呼叫 `filter_read` 或 `filter_read_exact` 來使用來自將 `use` 您模組的原始檔的原始碼資料來「啟動」$_。檢查傳回的狀態值,以查看是否實際讀取任何原始碼。
處理 $_ 的內容,以按照所需的方式變更原始碼。
傳回狀態值。
如果取消匯入您的模組(透過 `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,設定原始碼篩選器的任務會簡化為以下步驟,而非上述流程:
下載並安裝 Filter::Simple 模組。(如果您有 Perl 5.7.1 或更新版本,這項工作已經完成。)
設定一個模組,執行 use Filter::Simple
,然後呼叫 FILTER { ... }
。
在傳遞給 FILTER
的匿名子常式或區塊中,處理 $_ 的內容,以所需方式變更原始碼。
換句話說,先前的範例會變成
package BANG;
use Filter::Simple;
FILTER {
s/BANG\s+BANG/die 'BANG' if \$BANG/g;
};
1 ;
請注意,原始碼會傳遞為單一字串,因此任何使用 ^
或 $
來偵測行界線的正規表示式,都需要 /m
旗標。
預設情況下,已安裝的篩選器只會篩選到包含下列三個標準原始碼「終結符號」之一的行:
no ModuleName; # optional comment
或
__END__
或
__DATA__
但是,可以透過傳遞第二個引數給 use Filter::Simple
或 FILTER
來變更(請記住:當您使用 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
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
陳述式之前。
同樣地,如果您使用 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」套件,兩個自動建構的子常式:import
和 unimport
,它們會處理所有令人討厭的細節。
此外,產生的 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 perlbug
或 man 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.