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.