更多闪电文章

使用Perl自定义Emacs

作者:Bob DuCharme

随着时间的推移,我已经积累了一份想要在有机会时实施的Emacs自定义列表。例如,我希望宏能在标记的块内执行某些全局替换,并且我希望一个宏能将Outlook格式的日期重新格式化为ISO 8609格式的日期。我对用于自定义Emacs行为的elisp语言并不感到过分害怕;我之前已经复制了elisp代码并对其进行修改以做一些调整,我在学校学过大量的Scheme和LISP编程,并且我做了大量的XSLT工作,这些都是这些古老语言的衍生语言。然而,就像很多推迟的编辑自定义工作一样,我知道我必须多次使用这些宏,它们才能收回投入创建它们的时间,因为我对基于LISP的语言中的字符串操作和其他基本操作不太熟悉。我一直在想,“如果能在Perl中完成字符串操作,这会容易得多!”

然后,我想到了如何编写调用Perl以操作标记块(或者在Emacs中的说法,“区域”)的Emacs函数。许多Emacs用户熟悉Escape+|快捷键,它调用shell-command-on-region函数。它会在minibuffer中弹出一个提示,你可以在这里输入要在标记区域上运行的命令。按回车键后,如果命令输出可以放入minibuffer中,Emacs会将输出放入minibuffer中;如果不能,则放入新的“*Shell Command Output*”缓冲区中。例如,在标记你正在编辑的HTML文件的一部分作为区域后,按Escape+|并输入wc(表示“单词计数”)到“Shell命令在区域上:”提示中,如果该命令行实用工具在您的路径中,它将把文本发送到这个命令行工具,并在minibuffer中显示区域的行数、单词数和字符数。如果你在同一个提示符中输入sort,Emacs将运行该命令而不是wc,并在缓冲区中显示结果。

在同一个提示符中输入perl /some/path/foo.pl将会在标记的区域上运行指定的Perl脚本,并适当地显示输出。如果你只是想对几段文字进行全局替换,这可能会感觉有很多快捷键,但记住:Ctrl+|调用Emacs的内置shell-command-on-region函数,你可以从这个新定义的函数中调用这个相同的函数。我的最近的一个重要发现是,除了标识区域边界和要运行的命令的参数之外,shell-command-on-region还接受一个可选参数,允许你告诉它用输出区域替换输入区域。当你用Emacs编辑文档时,这允许你将标记的区域传递到Emacs之外的一个Perl脚本中,让Perl脚本对文本执行任何操作,然后Emacs将用处理过的版本替换原始文本。(如果你的Perl脚本破坏了文本,Emacs的优秀的undo命令可以拯救你。)

考虑一个例子。当我在工作中记项目笔记时,我可能会写上“Joe R. 发来一封电子邮件告诉我,某个系统处理新数据不需要任何修改。”我想记录他告诉我这件事的时间,所以我从他发送的电子邮件中复制粘贴日期。我们在工作中使用 Microsoft Outlook,日期的格式遵循“Tue 2/22/2005 6:05 PM”的模式。我已经为alt+d绑定了一个 Emacs 宏来插入当前日期和时间(在记笔记时也很方便),我希望电子邮件中提到的日期格式与使用alt+d宏插入的格式相同:ISO 8609 格式,形式为“2005-02-22T18:05”。

.emacs 启动文件包含您在 Emacs 会话期间希望可用的自定义函数。以下是我放入其中的部分代码,以便我可以转换这些日期

(defun OLDate2ISO ()
  (interactive)
  (shell-command-on-region (point)
         (mark) "perl c:/util/OLDate2ISO.pl" nil t))

(interactive)声明告诉 Emacs 正在被定义的函数可以作为命令交互地调用。例如,我可以在 Emacs 的最小缓冲区命令提示符中输入“OLDate2ISO”,或者我可以按下一个按键或选择绑定到这个函数的菜单选项。pointmark函数是内置在 Emacs 中的,用于标识当前标记区域的边界,因此它们对于shell-command-on-region的第一个和第二个参数来说很方便,这些参数告诉它要对哪些文本进行操作。第三个参数是在区域上实际要执行的命令;输入您操作系统上可用的任何可以接受标准输入的命令。要定义调用 Perl 函数的自己的 Emacs 函数,只需将此参数中的脚本名从OLDate2ISO更改为任何您喜欢的名称,然后更改此第三个参数为shell-command-on-region以调用您自己的 Perl 脚本。

将最后两个参数保留为nilt。不要担心第四个参数,它控制 shell 输出出现的缓冲区。(将其设置为nil意味着“不必麻烦。”)第五个参数是整个技巧的关键:当非空时,它告诉 Emacs 用第三个参数中描述的命令的输出替换编辑缓冲区中标记的文本,而不是将输出发送到缓冲区。

如果您熟悉 Perl,那么OLDate2ISO.pl 脚本没有什么特别有趣的地方。它执行一些正则表达式匹配来拆分字符串,将时间转换为 24 小时制,并重新排列这些部分

# Convert Outlook format date to ISO 8309 date 
#(e.g. Wed 2/16/2005 5:27 PM to 2005-02-16T17:27)
while (<>) {
  if (/\w+ (\d+)\/(\d+)\/(\d{4}) (\d+):(\d+) ([AP])M/) {
     $AorP = $6;
     $minutes = $5;
     $hour = $4;
     $year = $3;
     $month = $1;
     $day = $2;
     $day = '0' . $day if ($day < 10);
     $month = '0' . $month if ($month < 10);
     $hour = $hour + 12 if ($6 eq 'P');
     $hour = '0' . $hour if ($hour < 10);
     $_ = "$year-$month-$day" . "T$hour:$minutes";
  }
  print;
}

当您在.emacs文件中使用类似上面所示的defun OLDate2ISO这样的函数定义启动 Emacs 时,该函数就像任何其他函数一样可供您使用。按Escape+x键以显示 Emacs 最小缓冲区命令行,并在其中输入“OLDate2ISO”来在当前标记的缓冲区上执行它。像任何其他交互式命令一样,您也可以将其分配给一个按键或菜单选项。

可能还有更有效的方法来做上面所示的 Perl 编码,但我并没有花太多时间在这上面。这就是它的美妙之处:用五分钟的 Perl 编码和一分钟 elisp 编码,我就有了一个新的菜单选项,可以快速完成我一直想要的转换。

以下是我一直想要的另一个例子,是以下txt2htmlp.pl 脚本,在将几个段落的纯文本插入到 HTML 文档中后很有用

# Turn lines of plain text into HTML p elements.
while (<>) {
  chop($_);
  # Turn ampersands and < into entity references.
  s/\&/\&amp\;/g;
  s/</\&lt\;/g;
  # Wrap each non-blank line in a "p" element.
  print "<p>$_</p>\n\n" if (!(/^\s*$/));
}

同样,这并不是一个特别创新的 Perl 脚本,但通过在.emacs文件中的以下 elisp 代码,我得到了一个可以大大加快将匆忙写下的笔记添加到网页上的工具,尤其是当我创建一个调用此函数的 Emacs 菜单选项时。

(defun txt2htmlp ()
  (interactive)
  (shell-command-on-region (point) 
         (mark) "perl c:/util/txt2htmlp.pl" nil t))

有时候,当我听到关于热门新编辑器的时候,我会想它们是否能够取代我日常工作中使用的Emacs。现在,我可以如此容易地将Perl的强大功能添加到Emacs的使用中,因此,任何其他编辑器都很难在计算机上与Emacs竞争。

使用Devel::LineTrace调试您的程序

作者:Shlomi Fish

通常,程序员会发现需要使用print语句向屏幕输出信息,以帮助他们分析脚本运行时发生的问题。然而,直接将这些语句包含在脚本中并不是一个好的主意。如果不及时删除,这些语句可能会产生各种副作用:减慢脚本运行速度、破坏其输出格式(可能破坏测试用例)、污染代码,并让用户困惑。最好的办法是根本不要在代码中放置它们。那么,如何在不进行调试的情况下进行调试呢?

这就是Devel::LineTrace出现的地方,这是一个Perl模块,可以将代码的一部分分配到代码中的任意行执行。这样,程序员可以在代码的相关位置添加print语句,而不会损害程序的整体性。

验证use lib是否生效

我最近遇到的一个例子是,我想使用我放在特定目录下的模块,而该模块已经安装在了Perl的全局包含路径中。我使用use lib "./MyPath"指令确保了这一点,但现在遇到了问题。如果use lib指令的路径有误,Perl会从全局路径加载模块怎么办?我需要一种验证它的方法。

为了展示Devel::LineTrace如何做到这一点,考虑一个类似的脚本,它试图从路径./MyModules中加载名为CGI的模块,而不是从全局Perl路径加载。(给模块命名时避免使用CPAN或Perl发行版中模块的名称是一个好主意,但这里只是为了演示。)

#!/usr/bin/perl -w

use strict;
use lib "./MyModules";

use CGI;

my $q = CGI->new();

print $q->header();

将这个脚本命名为good.pl。为了测试Perl是否从./MyModules目录加载了CGI模块,将Devel::LineTrace指向打印%INC内部变量中的相关条目,在use CGI语句后的第一行。

为此,准备以下文件,并将其命名为test-good.txt

good.pl:8
    print STDERR "\$INC{CGI.pm} == ", $INC{"CGI.pm"}, "\n";

在第一行放置文件和应该插入跟踪的行号。然后是评估的代码,从行的开头缩进。在第一个跟踪之后,可以通过开始行以文件名和行号开始,并在以下(缩进的)行中放置代码来添加其他跟踪。不过,这个例子足够简单,不需要这样做。

准备好了test-good.txt之后,通过以下命令运行脚本通过Devel::LineTrace

$ PERL5DB_LT="test-good.txt" perl -d:LineTrace good.pl

(这假设使用的是Bourne shell的衍生版本)。环境变量PERL5DB_LT包含用于调试的文件路径,而Perl中的-d:LineTrace指令指示它通过Devel::LineTrace包进行调试。

结果,您应该在标准错误中看到以下输出

$INC{CGI.pm} == MyModules/CGI.pm

这意味着Perl确实从当前目录的MyModules子目录中加载了模块。否则,您会看到类似的内容

$INC{CGI.pm} == /usr/lib/perl5/vendor_perl/5.8.4/CGI.pm

…这意味着它来自全局路径,并且出了点问题。

Devel::LineTrace的限制

Devel::LineTrace有两个限制

  1. 因为它使用Perl调试器接口并在每一行停止(以检查是否包含跟踪),所以当程序在它的控制下运行时,程序执行速度会显著减慢。
  2. 它将跟踪分配给行号,因此如果文件的行号发生变化,您必须更新它。

尽管如此,它是一种很好的解决方案,可以将那些讨厌的 print 语句从您的程序中排除出去。祝您 LineTracing 快乐!

使用 Test::MockDBI

作者:Mark Leighton Fisher

如果可以仅通过创建一组指导 DBI 行为的规则来测试程序对 DBI 的使用,而不需要接触数据库(除非您想这么做),那会怎样?这就是 Test::MockDBI 的承诺,通过模拟整个 DBI API,它让您对 DBI 与程序交互的各个方面有了前所未有的控制。

Test::MockDBI 使用 Test::MockObject::Extends 来透明地模拟所有的 DBI。程序的其他部分并不知道如何使用 Test::MockDBI,这使得 Test::MockDBI 成为测试接管程序的理想选择,因为您只需添加 Test::MockDBI 调用代码——您不需要修改程序中的任何其他代码。(我发现这作为顾问非常有用,因为我经常处理其他人的代码。)

当当前的 SQL 与规则的 SQL 模式匹配时,将调用规则。为了进行更精细的控制,每个规则都有一个可选的数字 DBI 测试类型,因此规则只有在 SQL 匹配 并且 当前 DBI 测试类型是指定的 DBI 测试类型时才会触发。您可以从命令行或通过 Test::MockDBI::set_dbi_test_type() 指定此数字 DBI 测试类型(一个简单的整数,匹配 /^\d+$/)。您还可以设置规则,如果特定的 DBI::bind_param() 参数具有特定值,则失败事务。这意味着 Test::MockDBI 规则有三种类型的条件

  • 当前 SQL
  • 当前 DBI 测试类型
  • 当前 bind_param() 参数值

Test::MockDBI 下,fetch*()select*() 方法默认返回空(空数组、空哈希或标量中的未定义)。Test::MockDBIM 允许您使用 set_retval_scalar()set_retval_array() 方法控制它们返回的数据。您可以直接在 set_retval_*() 调用中指定返回数据,或者传递一个 CODEREF,为每次匹配的 fetch*()select*() 方法的调用生成一个返回值。CODEREF 允许您更精确地模拟 DBI 与数据库的交互(您可以返回几行,然后停止),并添加任何类型的状态机或其他所需的处理,以精确测试您的代码。

当您需要测试代码是否可以处理数据库或 DBI 失败时,bad_method() 是您的朋友。它可以失败任何 DBI 方法,失败取决于当前的 SQL 和(可选)当前的 DBI 测试类型。这种能力对于测试处理坏数据库 UPDATEINSERTDELETE 的代码是必要的,并且对于测试失败的 SELECT 也很有用。

Test::MockDBI 将您的测试能力扩展到测试难以或无法在实时、运行中的数据库上测试的代码。Test::MockDBI 对整个 DBI API 的模拟允许您在不修改当前 DBI 代码的情况下将 Test::MockDBI 添加到您的程序中。尽管它尚未完成(DBI 的不是所有部分都已被模拟),Test::MockDBI 已经是测试 DBI 程序的有力工具。

不必要的取消缓冲

作者:chromatic

程序员生活中的一个巨大乐趣是删除无用代码,特别是当其缺失改善了程序时。这通常发生在旧的代码库或匆忙拼凑的代码库中。有时,这发生在新手程序员写的代码中,他们尝试了多种不同的想法,但未能撤销他们的更改。

这样一个顽固的惯用语是全局、程序范围的取消缓冲,它可以采取以下任何一种形式:

local $| = 1;
$|++;
$| = 1;

有时候这很有价值。有时候这是至关重要的。但这并不是默认设置,有很好的理由,而且最糟糕的是,在程序中包含这些中的一条通常是无效的代码。

什么是无缓冲?

默认情况下,现代操作系统不会直接将信息逐字节发送到输出设备,也不会逐字节直接从输入设备读取信息。与处理器和内存相比,I/O速度非常慢,因此添加缓冲区并在发送和接收信息之前尝试填充它们可以提高性能。

想象一下试图从手动泵往浴缸里灌水。你可以往桶里泵一点水然后往返于浴缸和桶之间,或者你可以在泵那里装一个水槽,然后从水槽里往桶里灌水。如果水槽是空的,往桶里泵一点水可以让你更快地开始,但桶装满之间的时间会比一开始就装满水槽,然后在水槽和浴缸之间来回运水的时间更长。

信息并不完全像水。有时候,即使没有填满桶,立即发送消息也很重要。“救命,着火了!”是一个非常简短的消息,但当你有一大堆消息要发送时等待发送可能是不恰当的。

这就是为什么现代操作系统还允许您取消特定文件句柄的缓冲。当您向无缓冲的文件句柄打印时,操作系统将立即处理该消息。但这并不保证另一端的用户会立即响应;那里可能有泵和水槽。

损害是什么?

根据Mark-Jason Dominus的《受缓冲之苦?》,一个样本显示,缓冲读取比无缓冲读取快40%,缓冲写入快60%。后一个数字可能仅在网络通信中有所改善,因为发送和接收单个信息包的开销可能会压倒短消息。

然而,在简单的交互式应用程序中,可能没有好处。当连接到终端,如命令行时,Perl以行缓冲模式运行。运行以下程序并仔细观察输出

#!/usr/bin/perl

use strict;
use warnings;

# buffer flushed at newline
loop_print( 5, "Line-buffered\n" );

# buffer not flushed until newline
loop_print( 5, "Buffered  " );
print "\n";

# buffer flushed with every print
{
    local $| = 1;
    loop_print( 5, "Unbuffered  " );
}

sub loop_print
{
    my ($times, $message) = @_;

    for (1 .. $times)
    {
        print $message;
        sleep 1;
    }
}

前五个问候语逐个立即出现。Perl在看到新行时刷新STDOUT的缓冲区。第二组在五秒后一次出现,因为它看到了循环后的新行。第三组逐个立即出现,因为Perl在每个print语句后刷新缓冲区。

但是,终端与其他一切不同。考虑向文件写入的情况。在一个终端窗口中,创建一个名为buffer.log的文件,并运行tail -f buffer.log或其等效命令以实时查看文件的增长。然后向之前的程序添加以下行并再次运行

open( my $output, '>', 'buffer.log' ) or die "Can't open buffer.log: $!";
select( $output );
loop_print( 5, "Buffered\n" );
{
      local $| = 1;
      loop_print( 5, "Unbuffered\n" );
}

前五条消息以批量的方式一次性出现在日志中,尽管它们都有换行符。五条消息不足以填满缓冲区。Perl仅在将文件句柄分配给$|时刷新缓冲区。第二组消息逐条出现,一秒一条。

最后,STDERR文件句柄默认是热的。向之前的程序添加以下行并再次运行

select( STDERR );
loop_print( 5, "Unbuffered STDERR " );

尽管没有代码禁用STDERR的缓冲区,但五条消息应该会立即打印出来,就像在其他无缓冲的情况下一样。(如果它们没有,那么你的操作系统很奇怪。)

解决方案是什么?

缓冲存在是有原因的;几乎总是正确的做法。当它不是正确做法时,您可以禁用它。以下是一些经验法则

  • 永远不要默认禁用缓冲。
  • 当您有多个来源向同一输出写入且它们的顺序很重要时,禁用缓冲。
  • 永远不要默认禁用网络输出的缓冲。
  • 仅当预计的满缓冲区时间间隔超过预计客户端超时长度时,才禁用网络输出的缓冲。
  • 不要禁用终端输出的缓冲。对于STDERR,这是无用的、死代码。对于STDOUT,你可能不需要它。
  • 如果定期打印消息比高效更重要,则禁用缓冲。
  • 在你知道缓冲区是问题之前,不要禁用缓冲。
  • 尽可能在小范围内禁用缓冲。

标签

反馈

这篇文章有什么问题吗?请在GitHub上打开一个问题或拉取请求来帮助我们。