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的模块中的最小源代码过滤器。它只是将任何use BANG;
语句(直到下一个no BANG;
语句,如果有的话)之后的任何代码段中的BANG\s+BANG
序列转换为die 'BANG' if $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
'd 正则表达式(然后用于匹配终止符行),也可以是定义的假值(表示不应查找终止符行),也可以是哈希的引用(在这种情况下,终止符是与键'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 引号(包括 here 文档)。
完整的备选方案列表是
"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::SimpleFilter::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`。对于大多数其他问题,请联系 <[email protected]>。
CPAN 版本的维护者是 Steffen Mueller <[email protected]>。对于 CPAN 模块打包方面的技术问题,请联系他。
对模块的赞扬、鲜花和礼物仍然送给作者 Damian Conway <[email protected]>。
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.