使用awk命令删除重复的$ PATH条目


48

我试图编写一个bash shell函数,该函数将允许我从PATH环境变量中删除目录的重复副本。

有人告诉awk我,可以使用命令使用一行命令来实现此目的,但是我不知道该怎么做。有人知道吗?



Answers:


37

如果您在中没有重复项,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中的项目会更好,因为后面的项目通常是新添加的,并且它们的值可能是最新的。
埃里克·王

2
@EricWang我不明白您的理由。PATH元素是从前到后遍历的,因此当存在重复项时,第二个重复项将被有效忽略。从后到前迭代将更改顺序。
吉尔斯(Gillles)“所以-别再邪恶了”

@Gilles当您在PATH中有重复的变量时,可能是以这种方式添加的:PATH=$PATH:x=b,原始PATH中的x可能具有值a,因此当按顺序进行迭代时,新值将被忽略,而按相反的顺序,新值将被忽略。价值将生效。
Eric Wang

4
@EricWang在这种情况下,增加的值无效,因此应忽略。通过倒退,您可以使附加值先于他人。如果该附加值应该在之前使用,则将其添加为PATH=x:$PATH
吉尔斯(Gillles)“所以-别再邪恶了”

@Gilles追加内容时,这表示该内容尚不存在,或者您想覆盖旧值,因此需要使新添加的变量可见。而且,按照惯例,通常以这种方式添加:PATH=$PATH:...不是PATH=...:$PATH。因此,迭代反向顺序更合适。即使您的方式也行得通,但人们会以相反的方式进行附加。
Eric Wang

23

这是一种可理解的单行解决方案,它可以执行所有正确的操作:删除重复项,保留路径的顺序,并且最后不添加冒号。因此,它应该为您提供重复数据删除的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"
哈康Hægland

12

这是一个时尚的:

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中没有包含“:”的目录,否则它将被弄乱。

一些功劳:


-1这不起作用。我仍然在路径中看到重复项。
dogbane 2012年

4
@dogbane:它为我删除了重复项。但是,它有一个微妙的问题。输出的末尾有一个:,如果将其设置为$ PATH,则表示当前目录已添加路径。这对多用户计算机有安全隐患。
camh 2012年

@dogbane,它起作用了,我编辑了帖子以使一行命令没有尾随:
akostadinov 2012年

@dogbane您的解决方案有尾随:在输出中
akostadinov 2012年

嗯,您的第三个命令有效,但是除非我使用,前两个命令不起作用echo -n。您的命令似乎不适用于“这里的字符串”,例如,尝试:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
dogbane 2012年

6

这是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

AWK +粘贴

可以通过粘贴简化以上内容:

$ 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)))' )

好的,但是这会从现有的以冒号分隔的字符串中删除重复字符,还是防止重复字符添加到字符串中?
亚历山大·米尔斯

1
看起来像是前者
亚历山大·米尔斯

2
@AlexanderMills,好吧,OP刚问过要删除重复项,所以这就是awk调用的作用。
maxschlepzig

1
paste命令对我不起作用,除非我添加尾随-以使用STDIN。
wisbucky

2
另外,我需要在后面添加空格,-v否则会出现错误。-v RS=: -v ORS=。只是awk语法的风格不同。
wisbucky

4

已经有关于这个类似的讨论在这里

我采取了另一种方法。我不只是接受从安装的所有不同初始化文件中设置的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

3
因为你添加一个尾随这是非常危险:PATH(即空字符串项),因为那时的当前工作目录是你的一部分PATH
maxschlepzig 2014年

3

只要我们添加非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)。

真好!最短的工作答案,并且最后没有冒号。
jaap 2013年

2
PATH=`perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}'`
export PATH

这使用perl并具有以下优点:

  1. 它删除重复项
  2. 它保持排序
  3. 保持最早的外观(/usr/bin:/sbin:/usr/bin将导致/usr/bin:/sbin

2

另外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

2

正如其他人所证明的,使用awk,sed,perl,zsh或bash在一行中是可能的,取决于您对长行和可读性的容忍度。这是一个bash函数

  • 删除重复项
  • 保留订单
  • 在目录名称中允许空格
  • 允许您指定定界符(默认为':')
  • 可以与其他变量一起使用,而不仅仅是PATH
  • 可以在bash版本<4中使用,如果您使用的OS X不能解决bash版本4的许可问题,这一点很重要

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")

1

这是我的版本:

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$

1

最近的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。

+1:不错的脚本风格,但是您可以解释一下特定的语法:${a[$i]+_}编辑答案并添加一个项目符号。其余的完全可以理解,但是您在那里迷失了我。谢谢。
Cbhihe

1
@Cbhihe,我添加了一个解决此扩展的项目符号。
maxschlepzig

非常感谢你。很有意思。我认为使用数组(非字符串)是不可能的...
Cbhihe

1
PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`

awk代码说明:

  1. 用冒号分隔输入。
  2. 将新的路径条目追加到关联数组以进行快速重复查找。
  3. 打印关联数组。

除了简洁之外,这种单行代码还很快速:awk使用链式哈希表来实现摊销O(1)性能。

基于删除重复的$ PATH条目


旧帖子,但您能解释一下:if ( !x[$i]++ )。谢谢。
Cbhihe

0

用于awk在上分割路径:,然后遍历每个字段并将其存储在数组中。如果遇到数组中已经存在的字段,则意味着您之前已经看过它,因此请不要打印它。

这是一个例子:

$ MYPATH=.:/foo/bar/bin:/usr/bin:/foo/bar/bin
$ awk -F: '{for(i=1;i<=NF;i++) if(!($i in arr)){arr[$i];printf s$i;s=":"}}' <<< "$MYPATH"
.:/foo/bar/bin:/usr/bin

(已更新以删除结尾:。)


0

一种解决方案-不如更改* 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`

整个程序在BEGINEND块中运行。它从环境中提取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

0
export PATH=$(echo -n "$PATH" | awk -v RS=':' '(!a[$0]++){if(b++)printf(RS);printf($0)}')

仅保留第一次出现,并且相对顺序得到很好的维护。


-1

我只会使用基本工具(例如tr,sort和uniq)来做到这一点:

NEW_PATH=`echo $PATH | tr ':' '\n' | sort | uniq | tr '\n' ':'`

如果您的路径没有什么特别或奇怪的地方,那应该可以


顺便说一句,您可以使用sort -u代替sort | uniq

11
由于PATH元素的顺序很重要,因此不是很有用。
maxschlepzig 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.