Remova entradas duplicadas $ PATH com o comando awk


48

Estou tentando escrever uma função de shell bash que me permita remover cópias duplicadas de diretórios da minha variável de ambiente PATH.

Disseram-me que é possível conseguir isso com um comando de uma linha usando o awkcomando, mas não consigo descobrir como fazê-lo. Alguém sabe como?



Respostas:


37

Se você ainda não possui duplicatas no PATHdiretório e deseja adicionar diretórios apenas se eles ainda não estiverem lá, é possível fazê-lo facilmente apenas com o shell.

for x in /path/to/add …; do
  case ":$PATH:" in
    *":$x:"*) :;; # already there
    *) PATH="$x:$PATH";;
  esac
done

E aqui está um trecho de shell que remove duplicatas $PATH. Ele percorre as entradas uma a uma e copia as que ainda não foram vistas.

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

Seria melhor se iterássemos os itens em $ PATH de maneira inversa, porque os últimos são adicionados normalmente normalmente e podem ter o valor atualizado.
Eric Wang

2
@ EricWang Eu não entendo o seu raciocínio. Os elementos PATH são atravessados ​​da frente para trás, portanto, quando há duplicatas, a segunda duplicada é efetivamente ignorada. Iterar de trás para frente mudaria a ordem.
Gilles 'SO- stop be evil' em

@Gilles Quando você duplicou a variável no PATH, provavelmente foi adicionada desta maneira:, PATH=$PATH:x=bo x no PATH original pode ter o valor a, portanto, quando iterar em ordem, o novo valor será ignorado, mas quando em ordem inversa, o novo valor entrará em vigor.
Eric Wang

4
@EricWang Nesse caso, o valor agregado não tem efeito, portanto deve ser ignorado. Ao retroceder, você está fazendo o valor agregado vir antes. Se o valor agregado tivesse sido aumentado antes, teria sido adicionado como PATH=x:$PATH.
Gilles 'SO- stop being evil' em

@ Gilles Quando você acrescenta algo, isso significa que ele ainda não existe, ou você deseja substituir o valor antigo, para tornar visível a nova variável adicionada. E, por convenção, geralmente é anexado desta maneira: PATH=$PATH:...não PATH=...:$PATH. Portanto, é mais apropriado repetir a ordem inversa. Mesmo que você também trabalhe, as pessoas se juntam da maneira inversa.
Eric Wang

23

Aqui está uma solução inteligente de uma linha que faz todas as coisas certas: remove duplicatas, preserva a ordem dos caminhos e não adiciona dois pontos no final. Portanto, ele deve fornecer um caminho deduplicado que oferece exatamente o mesmo comportamento que o original:

PATH="$(perl -e 'print join(":", grep { not $seen{$_}++ } split(/:/, $ENV{PATH}))')"

Simplesmente divide em dois pontos ( split(/:/, $ENV{PATH})), usa usos grep { not $seen{$_}++ }para filtrar quaisquer instâncias repetidas de caminhos, exceto a primeira ocorrência, e depois junta as demais, separadas por dois pontos e imprime o resultado ( print join(":", ...)).

Se você quiser um pouco mais de estrutura em torno dele, bem como a capacidade de deduplicar outras variáveis ​​também, tente este snippet, que estou usando atualmente em minha própria configuração:

# 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

Esse código deduplicará PATH e MANPATH, e você poderá facilmente chamar dedup_pathvaroutras variáveis ​​que contêm listas de caminhos separados por dois pontos (por exemplo, PYTHONPATH).


Por algum motivo, tive que adicionar um chomppara remover uma nova linha à direita. Isso funcionou para mim:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
Håkon Hægland

12

Aqui está um elegante:

printf %s "$PATH" | awk -v RS=: -v ORS=: '!arr[$0]++'

Mais (para ver como funciona):

printf %s "$PATH" | awk -v RS=: -v ORS=: '{ if (!arr[$0]++) { print $0 } }'

Ok, já que você é novo no linux, aqui está como realmente definir PATH sem um ":" à direita

PATH=`printf %s "$PATH" | awk -v RS=: '{ if (!arr[$0]++) {printf("%s%s",!ln++?"":":",$0)}}'`

btw certifique-se de NÃO ter diretórios contendo ":" no seu PATH, caso contrário, será confuso.

algum crédito para:


-1 isso não funciona. Ainda vejo duplicatas no meu caminho.
dogbane

4
@ Dogbane: Ele remove duplicatas para mim. No entanto, tem um problema sutil. A saída possui um: no final, que se definido como seu $ PATH, significa que o diretório atual é adicionado ao caminho. Isso tem implicações de segurança em uma máquina multiusuário.
CAMH

@dogbane, ele funciona e eu editei o post para ter um comando de uma linha sem o seguinte:
akostadinov

@dogbane sua solução tem uma fuga: na saída
akostadinov

hmm, seu terceiro comando funciona, mas os dois primeiros não funcionam, a menos que eu use echo -n. Seus comandos não parecem trabalhar com "aqui cordas" por exemplo, tente:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
dogbane

6

Aqui está um liner AWK one.

$ PATH=$(printf %s "$PATH" \
     | awk -vRS=: -vORS= '!a[$0]++ {if (NR>1) printf(":"); printf("%s", $0) }' )

Onde:

  • printf %s "$PATH"imprime o conteúdo $PATHsem uma nova linha à direita
  • RS=: altera o caractere delimitador do registro de entrada (o padrão é nova linha)
  • ORS= altera o delimitador do registro de saída para a sequência vazia
  • a o nome de uma matriz criada implicitamente
  • $0 referencia o registro atual
  • a[$0] é uma desreferência de matriz associativa
  • ++ é o operador pós-incremento
  • !a[$0]++ protege o lado direito, ou seja, garante que o registro atual seja impresso apenas se não tiver sido impresso antes
  • NR o número do registro atual, começando com 1

Isso significa que o AWK é usado para dividir o PATHconteúdo pelos :caracteres delimitadores e para filtrar entradas duplicadas sem modificar a ordem.

Como as matrizes associativas do AWK são implementadas como tabelas de hash, o tempo de execução é linear (ou seja, em O (n)).

Observe que não precisamos procurar :caracteres entre aspas , porque os shells não fornecem aspas para suportar diretórios :em seu nome na PATHvariável.

Awk + colar

O acima pode ser simplificado com pasta:

$ PATH=$(printf %s "$PATH" | awk -vRS=: '!a[$0]++' | paste -s -d:)

O pastecomando é usado para intercalar a saída do awk com dois pontos. Isso simplifica a ação awk da impressão (que é a ação padrão).

Pitão

O mesmo que o Python de duas linhas:

$ PATH=$(python3 -c 'import os; from collections import OrderedDict; \
    l=os.environ["PATH"].split(":"); print(":".join(OrderedDict.fromkeys(l)))' )

ok, mas isso remove dupes de uma string delimitada por dois pontos existente ou impede que dupes sejam adicionados a uma string?
Alexander Mills

1
se parece com o anterior
Alexander Mills

2
@AlexanderMills, bem, o OP acabou de perguntar sobre a remoção de duplicatas, então é isso que a chamada awk faz.
maxschlepzig

1
O pastecomando não funciona para mim, a menos que eu adicione um final -para usar STDIN.
precisa saber é

2
Além disso, preciso adicionar espaços após o -verro, caso contrário. -v RS=: -v ORS=. Apenas diferentes tipos de awksintaxe.
precisa saber é

4

Houve uma discussão semelhante sobre isso aqui .

Eu adoto uma abordagem um pouco diferente. Em vez de aceitar apenas o PATH definido em todos os diferentes arquivos de inicialização instalados, prefiro usar getconfpara identificar o caminho do sistema e colocá-lo primeiro, depois adicionar minha ordem de caminho preferida e depois awkremover as duplicatas. Isso pode ou não acelerar a execução de comandos (e, em teoria, ser mais seguro), mas isso me deixa confuso.

# 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
Isso é muito perigoso porque você adiciona um final :à PATH(ou seja, uma entrada de sequência vazia), porque o diretório de trabalho atual faz parte do seu PATH.
maxschlepzig

3

Enquanto estivermos adicionando oneliners não awk:

PATH=$(zsh -fc "typeset -TU P=$PATH p; echo \$P")

(Pode ser tão simples quanto PATH=$(zsh -fc 'typeset -U path; echo $PATH')mas o zsh sempre lê pelo menos um zshenvarquivo de configuração, que pode ser modificado PATH.)

Ele usa dois recursos interessantes do zsh:

  • escalares vinculados a matrizes ( typeset -T)
  • e matrizes que removem automaticamente valores duplicados ( typeset -U).

legais! resposta de trabalho mais curta e nativamente sem dois pontos no final.
19416

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

Isso usa perl e tem vários benefícios:

  1. Remove duplicatas
  2. Mantém a ordem de classificação
  3. Mantém a aparência mais antiga ( /usr/bin:/sbin:/usr/binresultará em /usr/bin:/sbin)

2

Também sed(aqui usando a sedsintaxe GNU ) pode fazer o trabalho:

MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb')

este funciona bem apenas se o primeiro caminho for .como no exemplo do dogbane.

Em geral, você precisa adicionar outro scomando:

MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb;s/^\([^:]*\)\(:.*\):\1/:\1\2/')

Ele funciona mesmo em tal construção:

$ 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

Como outros demonstraram, é possível em uma linha usar awk, sed, perl, zsh ou bash, depende da sua tolerância para linhas longas e legibilidade. Aqui está uma função bash que

  • remove duplicatas
  • preserva a ordem
  • permite espaços nos nomes de diretório
  • permite especificar o delimitador (o padrão é ':')
  • pode ser usado com outras variáveis, não apenas PATH
  • funciona em versões do bash <4, importante se você usar o OS X que, para problemas de licenciamento, não é fornecido no bash versão 4

função 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}"
}

uso

Para remover dups do PATH

PATH=$(remove_dups "$PATH")

1

Esta é a minha versão:

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[*]}"
}

Uso: path_no_dup "$PATH"

Saída de amostra:

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

Versões recentes do bash (> = 4) também de matrizes associativas, ou seja, você também pode usar um bash 'one liner' para ele:

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)

Onde:

  • IFS altera o separador do campo de entrada para :
  • declare -A declara uma matriz associativa
  • ${a[$i]+_}é um significado de expansão de parâmetro: _é substituído se e somente se a[$i]estiver definido. Isso é semelhante ao ${parameter:+word}qual também testa não-nulo. Assim, na seguinte avaliação do condicional, a expressão _(ou seja, uma única cadeia de caracteres) é avaliada como verdadeira (isso é equivalente a -n _) - enquanto uma expressão vazia é avaliada como falsa.

+1: bom estilo de script, mas você pode explicar a sintaxe específica: ${a[$i]+_}editando sua resposta e adicionando um marcador. O resto é perfeitamente compreensível, mas você me perdeu lá. Obrigado.
Cbhihe

1
@ CBhihe, eu adicionei um ponto de bala que aborda essa expansão.
maxschlepzig

Muito obrigado. Muito interessante. Eu não acho que era possível com matrizes (não-cordas) ...
Cbhihe

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

Explicação do código awk:

  1. Separe a entrada por dois pontos.
  2. Anexe novas entradas de caminho à matriz associativa para uma rápida pesquisa duplicada.
  3. Imprime a matriz associativa.

Além de concisa, essa linha única é rápida: o awk usa uma tabela de hash de encadeamento para obter o desempenho O (1) amortizado.

com base em Removendo entradas duplicadas $ PATH


Post antigo, mas você poderia explicar: if ( !x[$i]++ ). Obrigado.
Cbhihe

0

Use awkpara dividir o caminho :, faça um loop sobre cada campo e armazene-o em uma matriz. Se você se deparar com um campo que já está na matriz, significa que já o viu antes, portanto, não imprima.

Aqui está um exemplo:

$ 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

(Atualizado para remover o final :).


0

Uma solução - não tão elegante quanto aquelas que alteram as variáveis ​​* RS, mas talvez razoavelmente clara:

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`

Todo o programa funciona nos blocos BEGIN e END . Ele puxa sua variável PATH do ambiente, dividindo-a em unidades. Ele itera sobre a matriz resultante p (que é criada em ordem por split()). A matriz e é uma matriz associativa usada para determinar se vimos ou não o elemento do caminho atual (por exemplo, / usr / local / bin ) antes e, se não, é anexado ao np , com lógica para anexar dois pontos ao np se já houver texto em np . O bloco END simplesmente ecoa np . Isso poderia ser mais simplificado adicionando o-F:flag, eliminando o terceiro argumento para split()(como padrão para FS ) e alterando np = np ":"para np = np FS, fornecendo-nos:

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

Ingenuamente, eu acreditava que for(element in array)isso preservaria a ordem, mas não funciona, então minha solução original não funciona, pois as pessoas ficariam chateadas se alguém de repente mexesse na ordem das suas $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)}')

Somente a primeira ocorrência é mantida e a ordem relativa é bem mantida.


-1

Eu faria isso apenas com ferramentas básicas como tr, sort e uniq:

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

Se não houver nada de especial ou estranho no seu caminho, ele deve funcionar


BTW, você pode usar em sort -uvez de sort | uniq.
apressar

11
Como a ordem dos elementos PATH é significativa, isso não é muito útil.
maxschlepzig
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.