内容

名称

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_readfilter_read_exact来用源文件中的源代码数据“填充”$_,该源文件将use您的模块。检查返回的状态值以查看是否实际读取了任何源代码。

  6. 处理$_的内容以按预期方式更改源代码。

  7. 返回状态值。

  8. 如果取消导入您的模块(通过no)会导致源代码过滤停止,请创建一个unimport子例程,并让它调用filter_del。确保步骤 5 中对filter_readfilter_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,设置源代码过滤器的任务简化为

  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'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::Simple

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"——两个自动构建的子例程——`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.