第一天 - 移植 Vigilance, 将 Raku 与标准工具集成在一起

Porting Vigilance, integrating Raku with standard tools

移植 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,或者至少为您提供一些命令行交互的技巧。

comments powered by Disqus