独立解析Perl
在我编程生涯的几年里,我参与了一个为一家庞大的全球IT公司而做的相当不寻常的Web项目。由于一些奇怪的平台问题,我们只能用Perl编写项目内部网络的一半,而几乎完全相同的公共互联网的一半只能用Java编写。
在我努力学习足够的Java以便帮助我的Perl代码与Java团队代码交互的过程中,我偶然发现了一个名为JetBrains IntelliJ IDEA的相对较新的编辑器。
多么愉快!它使学习Java变得绝对愉快,具有全面的选项卡完成、轻量级且简单的API文档、轻松探索Java类库,以及不显眼的指示器,它会告诉我我的错误并建议修复它们。简而言之,它有很多智能和快速、干净的界面。
IntelliPerl在哪里?
尽管我仅重度使用了几个月,但自从那时起,它就成了我的黄金标准,以及我对“优秀”编辑器的定义。我安装了遇到的每一个新的Perl编辑器和IDE,希望能有一天Perl能有一个像Java多年来那样的优秀编辑器。
这些优秀的编辑器正在传播。Java现在有1.5个(Eclipse几乎很棒,但似乎还不够“轻松”)。Dreamweaver多年前就为HTML人员提供了优秀的编辑器,我听说Python现在可能也有类似的。
有趣的是,这些优秀的编辑器似乎有一个共同的主要特点。
如何构建一个优秀的编辑器
而不是依赖于语言的解析器来检查代码,优秀的编辑器似乎实现了自己的特殊解析器。这些解析器将文件视为更通用的文档(碰巧也是代码)。
这是一个关键的区别,提供了两个关键功能。
首先,它创建了一个“往返”功能,将文件解析成内部模型,然后再解析出来,而不会移动一个空格字符。即使文件的一部分是损坏的或格式错误,你仍然可以更改文件的其他部分并正确保存它,而不会更改你没有更改的内容。
其次,它使解析器非常安全和容错。任何在编辑器中打开的代码都有其存在的理由——通常是因为它还没有完成,是损坏的,或者需要更改。文档解析器可以遇到问题,标记它,跌跌撞撞地前进一个字符或几个字符,直到找到它认识的内容,然后继续。
将解析视为代码是完全不同的任务,通常不适合这些类型的错误。
例如,看看以下。
print "Hello World!\n";
}
MyModule->foobar;
对于使用Perl本身的编辑器来理解这段代码,一旦它遇到裸露的闭合花括号,就游戏结束了,因为代码是无效的。如果没有了解括号下面的内容,你将失去解析器所需的所有智能:语法高亮、模块检查、有用的提示,等等。
这根本不是构建编辑器的合理方式,在这种情况下,文件既可以是未完成的,也可能有数十个错误。
构建Perl的文档解析器
即使还没有编辑器来放置它(目前还没有),Perl的文档解析器对于各种任务都非常有用。然而,在当时,我真正想要的只是一个非常准确的HTML语法高亮器。
2002年初的一个下午,无聊之余,我第一次尝试解决这个问题。鉴于我看到其他人尝试同样的方法时出现的模式,结果是可以预见的。它基于正则表达式,对于任何有趣的事情来说都是无用的。
从那时到2004年12月《Perl基金会》资助项目开始,我每个月花一到两天的时间在这个问题上,重写和丢弃代码。我废弃了两个标记化器、一个词法分析器、一个分析包、三个语法高亮器、一个混淆包、一个引用引擎,以及当前对象树中一半的类。
现在,终于,除了一些小功能和测试之外,《PPI》已经完成。它是100%双向安全,并且已经针对CPAN中38,000个(非Acme)模块进行了压力测试,除了28个最损坏和奇怪的外,都能处理。
它做什么?
PPI应该是任何需要解析、分析或操作Perl的任务的基础,它最终提供了一个平台,可以充分发挥这些任务的全部潜力。这涵盖了大量可能的任务;太多以至于无法在此深入讨论。
对于这篇文章,我想展示PPI如何改善现有的工具,这些工具目前只做非常基础的工作,而潜在的可能性如此之大。
其中一个就是PAR应用程序打包模块的一部分。当PAR将模块打包到内部包含目录中时,它会尝试通过删除POD来减小模块的大小。当然,更好的做法是删除所有多余的东西,进一步减小PAR文件的大小。
这是一种压缩形式,但考虑到使用像“Compress::Perl”这样的名字可能会引起混淆,我选择了自己的术语。我宣布将这个术语定为“Squish”。一个压缩后的模块将尽可能少地占用空间,因为已经移除了冗余字符。它将会非常小,尽管看起来可能有点“压缩”的样子 :)
Perl::Squish
与其向您展示最终项目,我更愿意展示压缩单个模块的过程。
# Where is File::Spec on our system?
use Class::Inspector;
my $filename = Class::Inspector->resolved_filename( 'File::Spec' );
# Load File::Spec as a document
use PPI;
my $Document = PPI::Document->new( $filename );
您使用PPI做的一切都始于并结束于PPI::Document对象。如果您直接使用词法分析器,您可能正在做错事。
我该从哪里开始削减冗余?首先,许多核心模块都有一个__END__
部分。
# Get the (one and only) __END__ section
my $End = $Document->find_first( 'Statement::End' );
# Delete it from the document
$End->delete if $End;
PPI提供了一组搜索方法,您可以在任何有子元素的对象上使用这些方法。find_first
是一个安全的猜测,因为只有一个__END__
部分。搜索方法实际上接收像File::Find这样的&wanted
函数,所以'Statement::End'
实际上是以下语法糖:
sub wanted {
my ($Document, $Element) = @_;
$Element->isa('PPI::Statement::End');
}
当然,有一种更快的方法来做同样的事情。prune
方法找到并立即删除所有符合特定条件的元素。
# Delete all comments and POD
$Document->prune( 'Token::Pod' );
$Document->prune( 'Token::Comment' );
作为一个更严重的例子,下面是如何从->method()
中去除非必需的大括号:
# Remove useless braces
$Document->prune( sub {
my $Braces = $_[1];
$Braces->isa('PPI::Structure::List') or return '';
$Braces->children == 0 or return '';
my $Method = $Braces->sprevious_sibling or return '';
$Method->isa('PPI::Token::Word') or return '';
$Method->content !~ /:/ or return '';
my $Operator = $Method->sprevious_sibling or return '';
$Operator->isa('PPI::Token::Operator') or return '';
$Operator->content eq '->' or return '';
return 1;
} );
虽然有点罗嗦,但写起来相对简单。只需添加条件并逐步丢弃。您可以获取其他元素,进行任何计算或调用子搜索。
完成之后,请务必保存文件。
# Save the file
$Document->save( "$filename.squish" );
总结
现在您需要做的只是将所有这些包裹在典型的模块样板中。
package Perl::Squish;
use strict;
use PPI;
our $VERSION = '0.01';
# Squish a file in place
# Perl::Squish->file( $filename )
sub file {
my ($class, $file) = @_;
my $Document = PPI::Document->new( $file ) or return undef;
$class->document( $Document ) or return undef;
$Document->save( $file );
}
# Squish a document object
# Perl::Squish->document( $Document );
sub document {
my ($squish, $Document) = @_;
# Remove the stuff we did earlier
$Document->prune('Statement::End');
$Document->prune('Token::Comment');
$Document->prune('Token::Pod');
$Document->prune( sub {
my $Braces = $_[1];
$Braces->isa('PPI::Structure::List') or return '';
$Braces->elements == 0 or return '';
my $Method = $Braces->sprevious_sibling or return '';
$Method->isa('PPI::Token::Word') or return '';
$Method->content !~ /:/ or return '';
my $Operator = $Method->sprevious_sibling or return '';
$Operator->isa('PPI::Token::Operator') or return '';
$Operator->content eq '->' or return '';
return 1;
} );
# Let's also do some whitespace cleanup
my @whitespace = $Document->find('Token::Whitespace');
foreach ( @whitespace ) {
$_->{content} = $_->{content} =~ /\n/ ? "\n" : " ";
}
1;
}
1;
最后,最后一步是将所有这些包裹成一个合适的模块。您可以在CPAN::Squish中看到经过PPI语法高亮器美化的最终产品。我在上述基本代码中添加了一些额外的功能,但您已经明白了。有关更多详细信息,请参阅Perl::Squish。
在15分钟内,我创建了一个非常简单的模块,它显著提高了你可以在没有PPI这样的工具的情况下完成的事情。现在想象一下它使可能实现的困难事情。
标签
反馈
这篇文章有什么问题吗?请通过在GitHub上打开一个问题或拉取请求来帮助我们。