我需要一个函数来从我的 POSIX PATH(和 MANPATH 等)中删除重复项,所以我写了这个。我是 bash 新手,在 $PATH 中犯错误并不好。我知道它会在 Windows PATH 上呕吐。我的报价可以吗?如果它是你的 root shell,你会改变什么?
fixpath() {
local IFS=:
local -A alreadyDone
local -a newpath
local -i i
local frag
i=0
for frag in $@ ; do
[ ${alreadyDone[$frag]+abc} ] || {
alreadyDone[$frag]=$frag;
newpath[$i]=$frag;
((i++))
}
done
printf '%s\n' "${newpath[*]}"
return 0
}
这是一些示例输出:
-bash-4.4$ fixpath /var/tmp:/home/bennett/bin:/home/bennett/bin:/usr/oneworld/bin:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/X11R6/bin:/usr/games:/snap/bin:/home/bennett/bin:/home/bennett/bin:/usr/oneworld/bin:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/X11R6/bin:/usr/games:/snap/bin:/tmp:/home/bennett/bin:/home/bennett/bin:/usr/oneworld/bin:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/X11R6/bin:/usr/games:/snap/bin
/var/tmp:/home/bennett/bin:/usr/oneworld/bin:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/X11R6/bin:/usr/games:/snap/bin:/tmp
我关心的是空格和 shell 元字符,我不知道我是否保护了自己。
此外,这依赖于
declare -A
。我该如何在 bash-3 中执行此操作?
附注该示例是为了测试而伪造的。我的道路上并没有真正的
/tmp
。而且,我几乎无法控制它是如何被搞砸的;我只是想把它修好。
在 bash 4 中,我将按如下方式实现:
fixpath() {
local out_str
local -a pieces=( ) out=( )
local -A seen=( )
IFS=: read -a pieces <<<"$1"
for piece in "${pieces[@]}"; do
[[ ${seen[$piece]} ]] && continue
out+=( "$piece" )
seen[$piece]=1
done
printf -v out_str '%s:' "${out[@]}"
printf '%s\n' "${out_str%:}"
}
注:
IFS=:
可以解决一些最明显的缺少引号的陷阱,但绝不能解决所有问题。IFS=: read -r -a
用于读入数组,并明确使用 :
作为分隔符。这仅修改单个命令的 IFS
,对任何其他范围没有影响(并且不依赖于 local IFS
按预期工作;我清楚地记得看到了 shell,但它没有,尽管我需要挖掘找出它到底在哪里)。return 0
,这样做会适得其反。默认返回值为最后一个命令的退出状态。如果 printf
失败(可能是因为您的标准输出是已关闭或无效的 FD),您 不应该 返回成功状态。在 bash 3 中,如果没有关联数组,效率就会显着降低。幸运的是,PATH 通常足够短,O(n^2) 不会令人望而却步(仍然比启动外部解释器在 awk 或 perl 中完成工作更快):
fixpath() {
local out_str seen
local -a pieces=( ) out=( )
IFS=: read -a pieces <<<"$1"
for piece in "${pieces[@]}"; do
seen=0
for out_piece in "${out[@]}"; do
[[ "$out_piece" = "$piece" ]] && { seen=1; break; }
done
(( seen )) && continue
out+=( "$piece" )
done
printf -v out_str '%s:' "${out[@]}"
printf '%s\n' "${out_str%:}"
}
使用 Perl 可能更容易做到:
#!/usr/bin/perl -w
use strict;
my $input = shift;
my %seen;
foreach my $dir ( split /:/, $input ) {
$seen{$dir} = 1;
}
my $output = join( ':', keys(%seen));
print $output . "\n";
您不必担心处理目录名称中的空格。
$ ./fixpath.pl "/var/tmp:/home/bennett/bin:/home/bennett/bin:/usr/oneworld/bin:/usr/local/bin:/path with space/dir:/usr/local/bin"
/var/tmp:/path with space/dir:/home/bennett/bin:/usr/local/bin:/usr/oneworld/bin
如果您想确保不更改路径的顺序,请执行以下操作:
#!/usr/bin/perl -w
use strict;
my $input = shift;
my %seen;
my $order=1;
foreach my $dir ( split /:/, $input ) {
$seen{$dir} = $order++ unless ($seen{$dir}) ;
}
my $output = join( ':', sort { $seen{$a} <=> $seen{$b} } keys(%seen));
print $output . "\n";
我正在查看
here中的
pathappend()
函数,并将其修改为使用 NEWPATH
而不是 PATH
:
unset NEWPATH
for i in $(echo $PATH | tr ':' ' '); do
[ -d "$i" ] && [[ ":$NEWPATH:" != *":$i:"* ]] && NEWPATH="${NEWPATH:+"$NEWPATH:"}$i"
done
export PATH=$NEWPATH
基本上,循环路径段,然后将每个路径段添加到新变量
NEWPATH
(如果尚未添加)。最后,将 PATH
设置为 NEWPATH
。它还会检查每个路径段是否是真实目录,然后再将其添加回来。