我试图编写一个bash shell函数,该函数将允许我从PATH环境变量中删除目录的重复副本。
有人告诉awk
我,可以使用命令使用一行命令来实现此目的,但是我不知道该怎么做。有人知道吗?
我试图编写一个bash shell函数,该函数将允许我从PATH环境变量中删除目录的重复副本。
有人告诉awk
我,可以使用命令使用一行命令来实现此目的,但是我不知道该怎么做。有人知道吗?
Answers:
如果您在中没有重复项,PATH
并且只想添加目录(如果目录中不存在重复项),那么仅使用Shell即可轻松完成。
for x in /path/to/add …; do
case ":$PATH:" in
*":$x:"*) :;; # already there
*) PATH="$x:$PATH";;
esac
done
这是一个shell片段,可从中删除重复项$PATH
。它逐个浏览条目,并复制那些尚未被看到的条目。
if [ -n "$PATH" ]; then
old_PATH=$PATH:; PATH=
while [ -n "$old_PATH" ]; do
x=${old_PATH%%:*} # the first remaining entry
case $PATH: in
*:"$x":*) ;; # already there
*) PATH=$PATH:$x;; # not there yet
esac
old_PATH=${old_PATH#*:}
done
PATH=${PATH#:}
unset old_PATH x
fi
PATH=$PATH:x=b
,原始PATH中的x可能具有值a,因此当按顺序进行迭代时,新值将被忽略,而按相反的顺序,新值将被忽略。价值将生效。
PATH=x:$PATH
。
PATH=$PATH:...
不是PATH=...:$PATH
。因此,迭代反向顺序更合适。即使您的方式也行得通,但人们会以相反的方式进行附加。
这是一种可理解的单行解决方案,它可以执行所有正确的操作:删除重复项,保留路径的顺序,并且最后不添加冒号。因此,它应该为您提供重复数据删除的PATH,使其行为与原始行为完全相同:
PATH="$(perl -e 'print join(":", grep { not $seen{$_}++ } split(/:/, $ENV{PATH}))')"
它仅对冒号(split(/:/, $ENV{PATH})
)进行拆分,使用用途grep { not $seen{$_}++ }
过滤掉路径中所有重复的实例(除了第一个出现的实例),然后将其余的实例重新连接在一起,并用冒号隔开,并打印结果(print join(":", ...)
)。
如果您需要更多的结构,以及还可以对其他变量进行重复数据删除,请尝试以下代码段,我目前正在自己的配置中使用该代码段:
# Deduplicate path variables
get_var () {
eval 'printf "%s\n" "${'"$1"'}"'
}
set_var () {
eval "$1=\"\$2\""
}
dedup_pathvar () {
pathvar_name="$1"
pathvar_value="$(get_var "$pathvar_name")"
deduped_path="$(perl -e 'print join(":",grep { not $seen{$_}++ } split(/:/, $ARGV[0]))' "$pathvar_value")"
set_var "$pathvar_name" "$deduped_path"
}
dedup_pathvar PATH
dedup_pathvar MANPATH
该代码将对PATH和MANPATH进行重复数据删除,并且您可以轻松地调用dedup_pathvar
其他包含用冒号分隔的路径列表的变量(例如PYTHONPATH)。
chomp
来删除尾随的换行符。这为我工作:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
这是一个时尚的:
printf %s "$PATH" | awk -v RS=: -v ORS=: '!arr[$0]++'
更长(看它如何工作):
printf %s "$PATH" | awk -v RS=: -v ORS=: '{ if (!arr[$0]++) { print $0 } }'
好的,因为您是Linux新手,所以这里是如何实际设置PATH而不用结尾的“:”
PATH=`printf %s "$PATH" | awk -v RS=: '{ if (!arr[$0]++) {printf("%s%s",!ln++?"":":",$0)}}'`
顺便说一句,请确保您的PATH中没有包含“:”的目录,否则它将被弄乱。
一些功劳:
echo -n
。您的命令似乎不适用于“这里的字符串”,例如,尝试:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
这是AWK一种衬板。
$ PATH=$(printf %s "$PATH" \
| awk -vRS=: -vORS= '!a[$0]++ {if (NR>1) printf(":"); printf("%s", $0) }' )
哪里:
printf %s "$PATH"
打印的内容$PATH
不带尾随换行符RS=:
更改输入记录分隔符(默认为换行符)ORS=
将输出记录定界符更改为空字符串a
隐式创建的数组的名称$0
引用当前记录a[$0]
是关联数组取消引用++
是后增量运算符!a[$0]++
保护右侧,即确保只打印当前记录(如果之前未打印)NR
当前记录号,从1开始这意味着AWK用于PATH
沿:
分隔符分隔内容,并在不修改顺序的情况下过滤出重复的条目。
由于AWK关联数组实现为哈希表,因此运行时是线性的(即,在O(n)中)。
请注意,我们不需要查找带引号的:
字符,因为外壳程序不提供引号来支持变量:
名称中具有其名称的目录PATH
。
可以通过粘贴简化以上内容:
$ PATH=$(printf %s "$PATH" | awk -vRS=: '!a[$0]++' | paste -s -d:)
该paste
命令用于在awk输出中插入冒号。这简化了awk打印操作(这是默认操作)。
与Python两层相同:
$ PATH=$(python3 -c 'import os; from collections import OrderedDict; \
l=os.environ["PATH"].split(":"); print(":".join(OrderedDict.fromkeys(l)))' )
paste
命令对我不起作用,除非我添加尾随-
以使用STDIN。
-v
否则会出现错误。-v RS=: -v ORS=
。只是awk
语法的风格不同。
已经有关于这个类似的讨论在这里。
我采取了另一种方法。我不只是接受从安装的所有不同初始化文件中设置的PATH,而是更喜欢使用getconf
标识系统路径并将其放置在先,然后添加我的首选路径顺序,然后用于awk
删除所有重复项。这可能会或可能不会真正加快命令的执行速度(从理论上讲是更安全的),但这给我带来了模糊的印象。
# I am entering my preferred PATH order here because it gets set,
# appended, reset, appended again and ends up in such a jumbled order.
# The duplicates get removed, preserving my preferred order.
#
PATH=$(command -p getconf PATH):/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# Remove duplicates
PATH="$(printf "%s" "${PATH}" | /usr/bin/awk -v RS=: -v ORS=: '!($0 in a) {a[$0]; print}')"
export PATH
[~]$ echo $PATH
/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/lib64/ccache:/usr/games:/home/me/bin
:
的PATH
(即空字符串项),因为那时的当前工作目录是你的一部分PATH
。
只要我们添加非awk oneliners:
PATH=$(zsh -fc "typeset -TU P=$PATH p; echo \$P")
(可能很简单,PATH=$(zsh -fc 'typeset -U path; echo $PATH')
但是zsh总是读取至少一个zshenv
可以修改的配置文件PATH
。)
它使用了两个不错的zsh功能:
typeset -T
)typeset -U
)。另外sed
(这里使用GNU sed
语法)也可以做到:
MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb')
仅当第一个路径.
类似于dogbane的示例时,此方法才有效。
通常,您需要添加另一个s
命令:
MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb;s/^\([^:]*\)\(:.*\):\1/:\1\2/')
即使在这样的构造上也可以使用:
$ echo "/bin:.:/foo/bar/bin:/usr/bin:/foo/bar/bin:/foo/bar/bin:/bar/bin:/usr/bin:/bin" \
| sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb;s/^\([^:]*\)\(:.*\):\1/\1\2/'
/bin:.:/foo/bar/bin:/usr/bin:/bar/bin
正如其他人所证明的,使用awk,sed,perl,zsh或bash在一行中是可能的,取决于您对长行和可读性的容忍度。这是一个bash函数
bash功能
remove_dups() {
local D=${2:-:} path= dir=
while IFS= read -d$D dir; do
[[ $path$D =~ .*$D$dir$D.* ]] || path+="$D$dir"
done <<< "$1$D"
printf %s "${path#$D}"
}
用法
删除PATH中的公仔
PATH=$(remove_dups "$PATH")
这是我的版本:
path_no_dup ()
{
local IFS=: p=();
while read -r; do
p+=("$REPLY");
done < <(sort -u <(read -ra arr <<< "$1" && printf '%s\n' "${arr[@]}"));
# Do whatever you like with "${p[*]}"
echo "${p[*]}"
}
用法: path_no_dup "$PATH"
样本输出:
rany$ v='a:a:a:b:b:b:c:c:c:a:a:a:b:c:a'; path_no_dup "$v"
a:b:c
rany$
最近的bash版本(> = 4)也具有关联数组,即,您也可以为其使用bash“一个衬里”:
PATH=$(IFS=:; set -f; declare -A a; NR=0; for i in $PATH; do NR=$((NR+1)); \
if [ \! ${a[$i]+_} ]; then if [ $NR -gt 1 ]; then echo -n ':'; fi; \
echo -n $i; a[$i]=1; fi; done)
哪里:
IFS
将输入字段分隔符更改为 :
declare -A
声明一个关联数组${a[$i]+_}
是参数扩展的含义:_
当且仅当a[$i]
设置了时,才替换。这类似于${parameter:+word}
也测试非空值。因此,在对条件的以下评估中,表达式_
(即单个字符串)的评估结果为true(这等效于-n _
)-而空表达式的评估结果为false。${a[$i]+_}
编辑答案并添加一个项目符号。其余的完全可以理解,但是您在那里迷失了我。谢谢。
PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`
awk代码说明:
除了简洁之外,这种单行代码还很快速:awk使用链式哈希表来实现摊销O(1)性能。
if ( !x[$i]++ )
。谢谢。
一种解决方案-不如更改* RS变量的解决方案那么优雅,但也许很明确:
PATH=`awk 'BEGIN {np="";split(ENVIRON["PATH"],p,":"); for(x=0;x<length(p);x++) { pe=p[x]; if(e[pe] != "") continue; e[pe] = pe; if(np != "") np=np ":"; np=np pe}} END { print np }' /dev/null`
整个程序在BEGIN和END块中运行。它从环境中提取PATH变量,将其拆分为多个单元。然后,它遍历结果数组p(按顺序创建split()
)。数组e是一个关联数组,用于确定是否在将当前路径元素(例如/ usr / local / bin)附加到np之前(如果没有),以及是否将冒号附加到的逻辑,NP如果已经在文本NP。在END块只是回声NP。可以通过添加以下内容进一步简化-F:
标记,消除第三个参数split()
(默认为FS),然后更改np = np ":"
为np = np FS
,从而得到:
awk -F: 'BEGIN {np="";split(ENVIRON["PATH"],p); for(x=0;x<length(p);x++) { pe=p[x]; if(e[pe] != "") continue; e[pe] = pe; if(np != "") np=np FS; np=np pe}} END { print np }' /dev/null
天真的,我认为这样for(element in array)
可以保留顺序,但是不能保留顺序,因此我的原始解决方案不起作用,因为如果有人突然扰乱了顺序,人们会感到不安$PATH
:
awk 'BEGIN {np="";split(ENVIRON["PATH"],p,":"); for(x in p) { pe=p[x]; if(e[pe] != "") continue; e[pe] = pe; if(np != "") np=np ":"; np=np pe}} END { print np }' /dev/null
我只会使用基本工具(例如tr,sort和uniq)来做到这一点:
NEW_PATH=`echo $PATH | tr ':' '\n' | sort | uniq | tr '\n' ':'`
如果您的路径没有什么特别或奇怪的地方,那应该可以
sort -u
代替sort | uniq
。