Remove entradas $ PATH duplicadas com o comando awk

41

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

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

    
por Johnny Williem 14.06.2012 / 06:22

16 respostas

32

Se você ainda não tem duplicatas no PATH e só deseja adicionar diretórios se eles ainda não estiverem lá, você pode 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 snippet de shell que remove duplicatas de $PATH . Ele passa pelas entradas uma a uma e copia aquelas 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
    
por 17.06.2012 / 04:15
18

Aqui está uma solução de inteligível one-liner que faz as coisas certas: remove duplicados, preserva a ordenação de caminhos e não adiciona dois pontos no final. Portanto, ele deve fornecer um PATH desduplicado que forneça exatamente o mesmo comportamento do original:

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

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

Se você quiser mais estrutura, bem como a capacidade de desduplicar outras variáveis, tente este snippet, que estou usando na minha própria configuração:

# Deduplicate path variables
get_var () {
    eval 'printf "%s\n" "${'"$1"'}"'
}
set_var () {
    eval "$1=\"\\""
}
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 desduplicará o PATH e o MANPATH, e você poderá chamar facilmente dedup_pathvar em outras variáveis que contêm listas de caminhos separadas por dois pontos (por exemplo, PYTHONPATH).

    
por 07.08.2014 / 19:35
9

Aqui está um elegante:

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

Mais longo (para ver como funciona):

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

Ok, desde que você é novo no Linux, aqui está como definir o PATH sem um ":"

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, ele vai ser aparafusado.

algum crédito para:

por 14.06.2012 / 09:20
4

Houve uma discussão semelhante sobre este aqui .

Adoto uma abordagem diferente. Em vez de aceitar apenas o PATH definido em todos os arquivos de inicialização diferentes que são instalados, prefiro usar getconf para identificar o caminho do sistema e colocá-lo primeiro, depois adicionar minha ordem de caminho preferida e usar awk para remover qualquer duplicatas. Isso pode ou não realmente acelerar a execução de comandos (e, em teoria, ser mais seguro), mas isso me dá uma sensação de conforto.

# 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
    
por 14.06.2012 / 15:52
4

Aqui está um forro AWK.

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

onde:

  • printf %s "$PATH" imprime o conteúdo de $PATH sem uma nova linha final
  • RS=: altera o caractere delimitador de registro de entrada (o padrão é nova linha)
  • ORS= altera o delimitador de registro de saída para a cadeia vazia
  • a o nome de uma matriz criada implicitamente
  • $0 faz referência ao registro atual
  • a[$0] é uma desreferência de matriz associativa
  • ++ é o operador pós-incremento
  • !a[$0]++ protege o lado direito, isto é, certifica-se de que o registro atual é impresso somente se não foi impresso antes
  • NR do número do registro atual, começando com 1

Isso significa que o AWK é usado para dividir o conteúdo PATH ao longo dos caracteres delimitadores : e para filtrar entradas duplicadas sem modificar o pedido.

Como os arrays associativos AWK são implementados como tabelas de hash, o tempo de execução é linear (ou seja, em O (n)).

Observe que não precisamos procurar por caracteres : citados porque shells não fornece citações para suportar diretórios com : em seu nome na variável PATH .

Awk + colar

Os itens acima podem ser simplificados com a colagem:

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

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

Python

O mesmo que o Python two-liner:

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

Contanto que adicionemos 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 zsh sempre lê pelo menos um arquivo de configuração zshenv , que pode modificar PATH .)

Ele usa dois ótimos recursos zsh:

  • escalares vinculados a matrizes ( typeset -T )
  • e matrizes que autorizam a remoção de valores duplicados ( typeset -U ).
por 24.11.2016 / 12:55
2

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

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

este funciona bem apenas no caso de o primeiro caminho ser . como no exemplo do dogbane.

Em geral, você precisa adicionar outro comando s :

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

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/:\([^:]*\)\(:.*\):/:/;tb;s/^\([^:]*\)\(:.*\)://'

/bin:.:/foo/bar/bin:/usr/bin:/bar/bin
    
por 14.06.2012 / 10:17
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"

Exemplo de saída:

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$
    
por 25.04.2013 / 04:20
1
PATH='perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}''
export PATH

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

  1. Remove os duplicados
  2. Mantém a ordem de classificação
  3. Mantém a aparência mais antiga ( /usr/bin:/sbin:/usr/bin resultará em /usr/bin:/sbin )
por 08.10.2012 / 00:30
1

Versões bash recentes (> = 4) também de matrizes associativas, ou seja, você também pode usar um 'liner' 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)

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] for definido. Isso é semelhante a ${parameter:+word} , que também testa para não-nulo. Assim, na avaliação seguinte do condicional, a expressão _ (ou seja, uma única cadeia de caracteres) é avaliada como verdadeira (isto é equivalente a -n _ ) - enquanto uma expressão vazia é avaliada como falsa.
por 13.04.2014 / 11:57
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. Anexar novas entradas de caminho ao array associativo para pesquisa rápida em duplicata.
  3. Imprime o array associativo.

Além de ser conciso, este one-liner é rápido: o awk usa uma tabela hash de encadeamento para atingir o desempenho O (1) amortizado.

com base em Removendo entradas duplicadas do $ PATH

    
por 21.02.2013 / 06:50
1

Como outros demonstraram que é possível em uma linha usando 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 o pedido
  • 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 com o PATH
  • funciona em versões bash < 4, importante se você usar o OS X, que para problemas de licenciamento não envia 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")
    
por 19.01.2017 / 23:29
0

Use awk para dividir o caminho em : , depois 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, isso significa que você o viu antes, portanto, não 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 : ).

    
por 14.06.2012 / 09:32
0

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

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'

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

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

Naïvely, eu acreditei que for(element in array) preservaria a ordem, mas não, então minha solução original não funciona, pois as pessoas ficariam chateadas se alguém subitamente alterasse a ordem de seus $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
    
por 28.04.2016 / 21:21
0
export PATH=$(echo -n "$PATH" | awk -v RS=':' '(!a[$0]++){if(b++)printf(RS);printf($0)}')

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

    
por 24.11.2016 / 11:25
-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 especial ou estranho no seu caminho, deve funcionar

    
por 24.04.2013 / 17:03