移植 Vigilance,将Raku与标准工具集成在一起
大家好,今天我们将采用基础设施脚本并将其从Perl 5移植到Raku.本文基于James Clark的一对帖子,你可以在这里找到:
此脚本用于创建和验证MD5总和。 这些是128位值,可用于验证数据完整性。 虽然MD5已经被证明在防范恶意行为者方面不安全,但它对于检测磁盘损坏仍然很有用。
Raku生态系统正在发展,其中包含多种工具,这些工具可以从Perl 5 CPAN移植,也可以替代。 我将介绍原始脚本和移植的几个方面,并说明我为什么要进行一些特定的更改。 希望这会鼓励你出去移植你自己的小脚本。
Shebang 和导入
Perl 5版本使用一些基础设施和一些实用程序来处理Unicode并使命令行输出更好:
#!/usr/bin/perl -CSDA
use strict;
use warnings;
use utf8;
use Encode qw/encode_utf8 decode_utf8/;
use Getopt::Long;
use Digest::MD5;
use Term::ANSIColor;
use Term::ProgressBar;
use File::Find;
use File::Basename;
use Data::Dumper;
Raku默认启用了警告和限制,并且内置了Unicode支持,因此我们可以将其保留。 Data::Dumper也已经实现,它具有非常有用的IO功能。 将所有这些加在一起我们可以得到一个非常精益的标头:
#!/usr/bin/env raku
use v6;
use Digest::MD5;
use Terminal::ANSIColor;
use Terminal::Spinners;
命令行选项
Perl 5有许多用于处理命令行参数的很棒的模块,在我们使用 Getopt::Long 的原始脚本中:
# Define our command-line arguments.
my %opts = ( 'blocksize' => 16384 );
GetOptions(\%opts, "verify=s", "create=s", "update=s", "files", "blocksize=s", "help!");
在Raku中,我们可以直接在MAIN方法中定义命令行选项。 我们使用多个调度来根据传递的参数来控制脚本的执行:
multi MAIN (Str :$create, *@files where { so @files }) { ... }
multi MAIN (Str :$update, *@files) { ... }
multi MAIN (Str :$verify, *@files) { ... }
multi MAIN (*@files where { so @files }) { ... }
这也意味着我们不必定义帮助选项/sub,因为我们可以文档化我们的MAIN子例程,因此:
#| Verify the MD5 sums in a file that conforms to md5sum output:
#|
multi MAIN (Str :$verify, *@files) { ... }
您可能已经注意到Raku版本没有定义blocksize选项,我将回过头来看看。
IO: 读写文件
我们将校验和存储在一个文件中,其中每一行的格式都与GNU coreutils中的md5sum程序的输出相同:32个十六进制数字,两个空格和文件名。
一些基本的IO,我们使用正则表达式来解析每一行。 使用有意义的空格有助于保持每个正则表达式相当简洁:
sub load_md5sum_file
{
my ($filename) = @_;
my @plan;
open(my $fh, '<:utf8', $filename) or die "Couldn't open '$filename' : $!\n";
my $linenum = 0;
while (my $line = <$fh>) {
chomp $line;
$linenum++;
if ($line =~ /^(?\p{ASCII_Hex_Digit}{32}) (?.*)$/) {
# Checksum and filename compatible with md5sum output.
push @plan, create_plan_for_filename($+{filename}, $+{md5});
} elsif ($line =~ /^(?\p{ASCII_Hex_Digit}{32}) (?.*)$/) {
# Checksum and filename compatible with md5sum's manpage but not valid for the actual program.
# We'll use it, but complain.
print STDERR colored("Warning: ", 'bold red'), colored("md5sum entry '", 'red'), $line, colored("' on line $linenum of file $filename is using only one space, not two - this doesn't match the output of the actual md5sum program!.", 'red'), "\n";
push @plan, create_plan_for_filename($+{filename}, $+{md5});
} elsif ($line =~ /^\s*$/) {
# Blank line, ignore.
} else {
# No idea. Best not to keep quiet, it could be a malformed checksum line and we don't want to just quietly skip the file if so.
print STDERR colored("Warning: ", 'bold red'), colored("Unrecognised md5sum entry '", 'red'), $line, colored("' on line $linenum of file $filename.", 'red'), "\n";
push @plan, { error => "Unrecognised md5sum entry" };
}
}
close($fh) or die "Couldn't close '$filename' : $!\n";
return @plan;
}
Raku允许我们验证我们是否通过签名传递了实际存在的文件。 此外,我们用 grammar 替换正则表达式,如果需要,我们可以在脚本的不同位置使用该 grammar:
grammar MD5SUM {
token TOP { <md5> <spacer> <filehandle> }
token md5 { <xdigit> ** 32 }
token spacer { \s+ }
token filehandle { .* }
}
sub load-md5sum-file (Str $filehandle where { $filehandle.IO.f }) {
my MD5Plan @plans;
PARSE: for $filehandle.IO.lines(:close) -> $line {
next PARSE if !$line; # We don't get worked up over blank lines.
my $match = MD5SUM.parse($line);
if (!$match) {
say $*ERR: colored("Couldn't parse $line", $ERROR_COLOUR);
next PARSE;
}
if (!$match<filehandle>.IO.f) {
say $*ERR: colored("{ $match<filehandle> } isn't an existing file.", $ERROR_COLOUR);
next PARSE;
}
if ($match<spacer>.chars == 2) {
@plans.push(MD5Plan.new($match<filehandle>.Str, $match<md5>.Str));
}
else {
say $*ERR: colored("'$line' does not match the output of md5sum: wrong number of spaces.", $WARNING_COLOUR);
@plans.push(MD5Plan.new($match<filehandle>.Str, $match<md5>.Str));
}
}
return @plans;
}
写出数据非常相似:
sub save_md5sum_file
{
my ($filename, @plan) = @_;
my $fh;
unless (open($fh, '>:utf8', $filename)) {
...
}
foreach my $plan_entry (@plan) {
next unless $plan_entry->{correct_md5} && $plan_entry->{filename};
print $fh "$plan_entry->{correct_md5} $plan_entry->{filename}\n";
}
close($fh) or die "Couldn't close '$filename' : $!\n";
}
值得注意的是,Raku默认以Unicode格式写入文件:
sub save-md5sum-file (Str $filehandle, @plans) {
my $io = $filehandle.IO.open: :w;
WRITE: for @plans -> $plan {
next WRITE unless $plan.computed-md5 && $plan.filehandle;
$io.say("{ $plan.computed-md5 } { $plan.filehandle }");
}
$io.close;
}
获得MD5校验和
Perl 5版本的Digest::MD5使用了相当多的XS来提高性能。 XS中包含了以块的形式添加数据以进行整体解析的方法。 这允许我们使用ProgressBar向用户展示用户等待时的进度:
sub run_md5_file
{
my ($plan_entry, $progress_fn) = @_;
# We use the OO interface to Digest::MD5 so we can feed it data a chunk at a time.
my $md5 = Digest::MD5->new();
my $current_bytes_read = 0;
my $buffer;
$plan_entry->{start_time} = time();
$plan_entry->{elapsed_time} = 0;
$plan_entry->{elapsed_bytes} = 0;
# 3 argument form of open() allows us to specify 'raw' directly instead of using binmode and is a bit more modern.
open(my $fh, '<:raw', $plan_entry->{filename}) or die "Couldn't open file $plan_entry->{filename}, $!\n";
# Read the file in chunks and feed into md5.
while ($current_bytes_read = read($fh, $buffer, $opts{blocksize})) {
$md5->add($buffer);
$plan_entry->{elapsed_bytes} += $current_bytes_read;
$plan_entry->{elapsed_time} = time() - $plan_entry->{start_time};
&$progress_fn($plan_entry->{elapsed_bytes});
}
# The loop will exit as soon as read() returns 0 or undef. 0 is normal EOF, undef indicates an error.
die "Error while reading $plan_entry->{filename}, $!\n" if ( ! defined $current_bytes_read);
close($fh) or die "Couldn't close file $plan_entry->{filename}, $!\n";
# We made it out of the file alive. Store the md5 we computed. Note that this resets the Digest::MD5 object.
$plan_entry->{computed_md5} = $md5->hexdigest();
}
Raku版本使用纯Perl并且缺少添加功能,因此我使用微调器而不是进度条。 我们还需要专门设置我们的编码,以避免在将二进制数据读取为Unicode时出现的错误:
sub calc-md5-sum (MD5Plan $plan) {
my $md5 = Digest::MD5.new;
print "Calculating MD5 sum for { $plan.filehandle } "; # We need some space for the spinner to take up.
# I like 'bounce', so I need 6 spaces for the spinner
# + an extra one to separate it from the filehandle.
my Buf $buffer = $plan.filehandle.IO.slurp(:close, :bin);
my $decoded = $buffer.decode('iso-8859-1');
my $spinner = Spinner.new(type => 'bounce');
my $promise = Promise.start({
$md5.md5_hex($decoded)
});
until $promise.status {
$spinner.next;
}
say ''; # Add a new line after the spinner.
$plan.computed-md5 = $promise.result;
}
结束之前的思考
我没有在我的系统上使用Raku版本因为Digest::MD5的低性能,在我的系统上我用md5sum调用替换它。 其他可能性是使用Inline::Perl5和Perl 5版本的Digest::MD5,或使用惊人的Raku原生调用接口来运行C实现。 我希望这篇文章能激发您将一些自己的Perl 5脚本移植到Raku,或者至少为您提供一些命令行交互的技巧。