删除重复 PATH 条目,第二部分:Perl 的崛起
向 阿诺德和终结者系列电影 致歉标题的冒犯,让我们再次审视一下从 PATH 变量中删除重复项的问题。这个方法的出现源于一位名叫 Shaun 的读者在之前的帖子中评论说:“如果你愿意使用非 bash 解决方案 (AWK) 来解决问题,为什么不使用 Perl 呢?” Shaun 非常好心地提供了一个 Perl 版本的代码,这很好,因为我很难自己想出一个。这是一段简短的代码,比 AWK 版本更短,所以看起来应该很容易理解。最后,我不确定我会称之为容易,但这很有趣,并且我认为其他非 Perl 程序员也可能会觉得有趣。
首先,让我重复一下第一篇文章中的内容:没有令人信服的理由从您的 PATH 变量中删除重复项;shell 将忽略重复路径的第二个和后续出现。
来自评论的代码,略作修改如下
PATH='/usr/bin:/bin::/usr/local/bin:/root/new folder:/bin:/usr/bin'
PATH=$(perl -E 'chomp($_=<>);say join":",grep{$_&&!$_{$_}++}split/:/' <<<$PATH)
为了使代码更易于阅读,我稍微重新排列了一下,添加了一些空格,并将其放入文件中
chomp($_ = <>);
say join ":", grep { $_ && !$_{$_}++ } split /:/
为了确保没有出错,我运行了它,当然,像往常一样,第一次尝试某些东西时,它失败了
$ perl test.pl <<<'/usr/bin:/bin::/usr/local/bin:/root/new folder:/bin:/usr/bin'
syntax error at test.pl line 2, near "say join"
Execution of test.pl aborted due to compilation errors.
这里的罪魁祸首是 -E 选项,或者在这种情况下缺少它,它在原始命令中使用。-E 选项除了将脚本作为字符串传递给解释器外,还启用了一些默认情况下未启用的较新的 Perl 功能。在这种情况下,它是 say 功能。要在脚本文件中启用这些较新的功能,您需要告诉 Perl 使用该功能
use feature qw(say) # use "say" feature
chomp($_ = <>);
say join ":", grep { $_ && !$_{$_}++ } split /:/
在那之后,它按预期运行
$ perl test2.pl <<<'/usr/bin:/bin::/usr/local/bin:/root/new folder:/bin:/usr/bin'
/usr/bin:/bin:/usr/local/bin:/root/new folder
那么它是如何工作的呢?从最广泛的意义上讲,它的工作方式与 AWK 版本几乎相同:它用冒号分割路径,使用关联数组(在 Perl 中也称为哈希)来确定是否之前已经见过路径元素,然后一旦知道最终的路径列表,它就用冒号将它们重新连接在一起。但是,让我们逐步了解一下细节。
第一行(忽略 use 行)在 $_=<>
上调用 chomp 函数。Chomp 非常简单;它从每个参数中删除一个“记录分隔符”。默认的记录分隔符是换行符,就像在 AWK 中一样。chomp 的参数是一个赋值表达式,它将空文件句柄 <>
赋值给 Perl 的“默认输入和模式搜索空间”,即名为 $_
的变量。在本例中,空文件句柄,即通过 bash 的 <<<
语法表示的标准输入,是 path 变量的值。默认模式空间等同于 AWK 中的 $0
:它是函数在缺少显式变量/表达式时默认操作的数据。
因此,赋值表达式 $_=<>
将路径分配给模式空间,然后 chomp
函数从模式空间的末尾删除记录分隔符。所有这些都是冗长的说法,即代码的第一行从输入中删除了那个讨厌的换行符。如果您还记得第一篇文章,换行符是通过向 echo
添加 -n 选项来消除的。
注意:空文件句柄
Perl 的 <>
的完整故事有点复杂。介于“<”和“>”之间的内容是文件句柄,因此例如,<STDIN>
指的是标准输入。但是有一些魔术文件句柄,例如 <ARGV>
,它指的是在命令行上指定的脚本的输入文件。输入文件按顺序逐行读取,就像从 <ARGV>
读取一样。如果在命令行上没有指定输入文件,Perl 会将 <ARGV>
设置为 “-”,以便读取标准输入。空文件句柄 <>
等同于 <ARGV>
。
代码的第二部分是实际工作发生的地方。从语法上讲,代码看起来像这样
say EXPR
/ \
/ \
join STR EXPR
/ \
/ \
grep EXPR LIST
/ \
/ \
split PATTERN
在该图的底部是对 split 函数的调用
split /:/
这应该相当明显:这会用冒号分割模式空间。换句话说,它将路径分成单独的路径元素。它对模式空间进行操作,因为没有提供其他特定数据。split 函数生成一个列表供 grep
使用。如果您还记得第一篇文章,AWK 版本将记录分隔符更改为冒号,以将路径拆分为单独的元素;在这个版本中,split
函数用于完成该操作。
那么为什么 chomp
函数在其参数周围使用括号,而 split
函数却没有呢?实际上,chomp
函数在其参数周围没有使用括号;Perl 函数调用的语法很简单 func args,没有括号。调用 chomp
中的括号是为了改变表达式中项的优先级。如果没有括号,则调用将被视为 chomp($_)=<>;
,这是无效的语法。
图中的下一个是 grep
。请注意,这不是命令行 grep;
这是 Perl grep
函数。Perl 的 grep
函数为其 LIST 中的每个项目评估其 EXPR,当表达式为真时,列表中的当前项目将添加到新列表中,该列表将成为 grep 函数的结果。在这种情况下,EXPR 实际上是一个 BLOCK(花括号中的代码 {}
)。
暂时忽略块的内容,代码的其余部分也应该相当明显:grep 函数将生成最终的路径列表,join 函数将用冒号将它们重新连接在一起,而 say 函数将输出最终的路径值。
因此,要理解的最后一部分代码是 grep
函数为每个路径元素评估的块中的代码
$_ && !$_{$_}++
在这里,与许多语言一样,&&
是逻辑与运算符,!
是逻辑非运算符,++
是后增量运算符。
与运算符的左侧是简单的表达式 $_
,它测试模式空间是否为空。这是消除空路径元素的地方:如果模式空间为空,则与运算符的左侧为假,因此整个表达式为假,并且当前(空)路径元素不会添加到 grep
函数正在创建的列表中。
[编辑: 正如下面的评论中指出的那样,这是不正确的:不应删除空路径元素,它们“意味着”当前目录。要修复它,只需从表达式中删除 “$_ &&”。 -- Mitch]
如果您还记得上面,我说过 split
函数创建了一个列表,该列表用作 grep 要处理的数据,因此您可能想知道模式空间 $_
是如何参与其中的。根据 Perl 文档,grep
函数在评估其块/表达式之前,将每个列表项分配给 $_
变量的本地副本。
如果您认为自己会毫发无损地摆脱困境,那么这就是事情变得有点棘手的地方。在与表达式的右侧是哈希(关联数组)。在 Perl 中,花括号也用于在引用哈希中的元素时包围键值,因此 {$_}
使用(本地)模式空间(也称为当前路径元素)作为哈希的键。不太复杂,但是然后您会看到您认为的哈希名称:$_
,但是模式空间变量怎么能也是哈希的名称呢?好吧,它不是,但是再次,让我们暂时忽略它。为了减轻您感到的认知失调,您可以更改代码并为哈希使用不同的名称(这实际上在真实代码中有效)
$_ && !$myhash{$_}++
我目前的工作假设是,使用 $_
作为哈希名称相当于当地小屋的秘密握手:俱乐部成员明白了,其他人都感到困惑。开玩笑的,这实际上是哈希名称的一个不错的选择,因为这是我花费最多时间试图掌握的部分,这引导我走上了许多有用的道路。但是在深入了解其中一些内容之前,让我们完成代码。
因此,表达式测试哈希元素以查看它是否为零 $myhash{$_}
。如果哈希元素为零,则表达式为真(由于否定)。因此,当前路径元素以前未见过,并且它被添加到 grep 正在创建的列表中。如果哈希元素为非零,则路径已被看到,表达式为假,并且路径元素未添加到列表中。在测试哈希元素之后,通过 ++
运算符对其进行递增,以便下次看到路径元素时,表达式将为假,并且将跳过重复的路径元素。概括来说,在伪代码中,该块看起来像这样
if path-element is not blank
tmp = hash[path-element]
hash[path-element] += 1
if tmp == 0
"BLOCK" is true, add path to output list
else
"BLOCK" is false, path already added to output
与 AWK 中一样,在 Perl 中引用不存在的哈希(也称为关联数组)会自动创建它。并且对未定义的值执行算术运算与使用零相同。因此,哈希及其所有元素都是根据需要创建的,而无需任何显式代码来创建它们。
现在让我们回到该哈希的名称。Perl 文档 (perldata) 指出
Perl 有三种内置数据类型:标量、标量数组和标量的关联数组,称为“哈希”。标量是单个字符串(大小任意,仅受可用内存限制)、数字或对某物的引用 [...]。普通数组是由数字索引的标量的有序列表,从 0 开始。哈希是由其关联的字符串键索引的标量值的无序集合。
在代码中,它看起来像这样
$scalar_var = 1; # simple var
@list_var = (1, 2); # a list
%hash_var = (one => 1, two => 2); # a hash
因此您可能会认为,“好吧,我明白了,每种数据类型都在其名称中使用特殊字符”。所以你尝试这个
$v = 1;
@v = (1, 2);
%v = (one => 1, two => 2);
果然,Perl 非常喜欢这样。但是,您实际上没有三个名为 $v、@v 和 %v 的变量。W 您拥有的是三个名为 v 的变量,它们的名称来自三个不同的命名空间。希望某些 Perl 语言律师会指出解释它的正确方法,但目前这可以工作。
再多一点,我就可以回到 $_{$_}
的哈希名称难题。考虑以下代码,它创建一个标量和一个同名的哈希,然后打印出标量和哈希中的一个元素
my $v = "scalar v";
my %v = ( a => "hash element with key a", b => "hash element with key b" );
print "v : $v\n";
print "v{a}: $v{a}\n";
运行它,您会得到您期望的结果
$ perl test.pl
v : scalar v
v{a}: hash element with key a
但是仔细看看最后一个打印语句。您可能希望在字符串中看到 %v{a}
而不是 $v{a}
,因为它正在打印哈希中的值。因此,这里有力地证明了前导字符 $、@ 或 % 不是变量名称的一部分。这些前导字符在 Perl 中称为“sigils”,考虑到 sigil 的定义,这似乎是一个合适的名称
在占星术或魔法中,被认为具有神秘力量的符号、文字或装置。
在谈论 Perl 时,您经常会看到对某物处于标量上下文或列表上下文的引用。例如,考虑以下代码,它创建一个列表,然后在标量上下文中引用该列表
@v = (4, 5, 6);
$u = scalar @v;
print "@v\n";
print "$u\n";
$ perl test.pl
4 5 6
3
当您在列表上下文中引用列表时,您会得到列表;当您在标量上下文中引用它时,您会得到它的长度。因此,在上面的示例中,打印语句引用了 $v{a}
,该引用是对哈希元素 v{a}
的引用,并且由于哈希元素是单个值,并且由于您要打印该单个值,因此您需要在标量上下文中引用它。因此,与其使用 %v{a}
(这将给您一个哈希而不是标量),不如使用 $v{a}
来获取标量值。实际上,如果您将脚本更改为使用 %v{a}
,您将不会得到哈希,因为它不是有效的语法,您只会得到
$ perl test.pl
v : scalar v
v{a}: %v{a}
最后回到原始代码
!$_{$_}++
因此,在这里,第一次使用 $_
指的是名为 _ 的哈希,而不是 Perl 的模式空间变量 $_
。但是,正如我已经提到的,第二次使用 $_
确实指的是模式空间;它用作哈希的键。在找到(或创建)哈希元素之后,在标量上下文中对其进行测试和否定。然后,最后,也在标量上下文中,对其进行递增。
对于一段非常短的代码来说,这是一个相当长的解释,但是希望您觉得它有趣。如果它没有让您睡着,那可能就足够了。正如我在前面提到的,我不是 Perl 程序员,所以我可能在某个地方搞砸了。请告诉我哪里。