Rimuovi le voci duplicate $ PATH con il comando awk


48

Sto cercando di scrivere una funzione shell bash che mi permetterà di rimuovere copie duplicate di directory dalla mia variabile d'ambiente PATH.

Mi è stato detto che è possibile ottenere questo risultato con un comando a una riga usando il awkcomando, ma non riesco a capire come farlo. Qualcuno sa come?



Risposte:


37

Se non hai già duplicati nel file PATHe vuoi aggiungere directory solo se non sono già presenti, puoi farlo facilmente con la shell da sola.

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

Ed ecco uno snippet di shell che rimuove i duplicati da $PATH. Esamina le voci una per una e copia quelle che non sono state ancora viste.

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

Sarebbe meglio, se ripetete gli articoli in $ PATH in modo inverso, perché quelli successivi vengono aggiunti di recente di solito e potrebbero avere il valore aggiornato.
Eric Wang,

2
@EricWang Non capisco il tuo ragionamento. Gli elementi PATH vengono attraversati da davanti a dietro, quindi quando ci sono duplicati, il secondo duplicato viene effettivamente ignorato. L'iterazione da dietro a davanti cambierebbe l'ordine.
Gilles 'SO- smetti di essere malvagio' il

@Gilles Quando hai duplicato la variabile in PATH, probabilmente è stato aggiunto in questo modo:, PATH=$PATH:x=bla x nel PATH originale potrebbe avere valore a, quindi quando si itera in ordine, il nuovo valore verrà ignorato, ma quando in ordine inverso, il nuovo il valore avrà effetto.
Eric Wang,

4
@EricWang In tal caso, il valore aggiunto non ha alcun effetto, quindi dovrebbe essere ignorato. Andando indietro, stai facendo arrivare prima il valore aggiunto. Se il valore aggiunto fosse dovuto in precedenza, sarebbe stato aggiunto come PATH=x:$PATH.
Gilles 'SO- smetti di essere malvagio' il

@Gilles Quando aggiungi qualcosa, significa che non c'è ancora, o vuoi sovrascrivere il vecchio valore, quindi devi rendere visibile la nuova variabile aggiunta. E, per convenzione, di solito viene aggiunto in questo modo: PATH=$PATH:...no PATH=...:$PATH. Quindi è più corretto iterare l'ordine inverso. Anche se a modo tuo funzionerebbe anche, allora le persone aggiungono nel modo inverso.
Eric Wang,

23

Ecco una soluzione comprensibile con una sola riga che fa tutte le cose giuste: rimuove i duplicati, conserva l'ordinamento dei percorsi e non aggiunge due punti alla fine. Quindi dovrebbe darti un PERCORSO deduplicato che dia esattamente lo stesso comportamento dell'originale:

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

Si divide semplicemente su due punti ( split(/:/, $ENV{PATH})), utilizza gli usi grep { not $seen{$_}++ }per filtrare eventuali istanze ripetute di percorsi ad eccezione della prima occorrenza, quindi unisce di nuovo i restanti separati da due punti e stampa il risultato ( print join(":", ...)).

Se vuoi un po 'più di struttura attorno ad esso, oltre alla possibilità di deduplicare anche altre variabili, prova questo snippet, che attualmente sto usando nella mia configurazione:

# 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

Tale codice deduplica sia PATH che MANPATH e puoi facilmente chiamare dedup_pathvaraltre variabili che contengono elenchi di percorsi separati da due punti (ad esempio PYTHONPATH).


Per qualche motivo ho dovuto aggiungere un chompper rimuovere una nuova riga finale. Questo ha funzionato per me:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
Håkon Hægland,

12

Eccone uno elegante:

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

Più a lungo (per vedere come funziona):

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

Ok, dato che sei nuovo su Linux, ecco come impostare effettivamente PATH senza un ":" finale

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

btw assicurati di NON avere directory contenenti ":" nel tuo PERCORSO, altrimenti sarà incasinato.

un po 'di credito a:


-1 questo non funziona. Vedo ancora duplicati sul mio percorso.
dogbane,

4
@dogbane: rimuove i duplicati per me. Tuttavia ha un problema sottile. L'output ha un: alla fine che, se impostato come $ PATH, significa che alla directory corrente viene aggiunto il percorso. Ciò ha implicazioni di sicurezza su una macchina multiutente.
Camh,

@dogbane, funziona e ho modificato il post per avere un comando a una riga senza il trascinamento:
akostadinov

@dogbane la tua soluzione ha un problema: nell'output
akostadinov

hmm, il tuo terzo comando funziona, ma i primi due non funzionano se non lo uso echo -n. I tuoi comandi non sembrano funzionare con "qui stringhe", ad esempio provare:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
dogbane

6

Ecco una fodera AWK one.

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

dove:

  • printf %s "$PATH"stampa il contenuto di $PATHsenza una nuova riga finale
  • RS=: cambia il carattere del delimitatore del record di input (il valore predefinito è newline)
  • ORS= cambia il delimitatore del record di output nella stringa vuota
  • a il nome di un array creato implicitamente
  • $0 fa riferimento al record corrente
  • a[$0] è una dereference array associativa
  • ++ è l'operatore post-incremento
  • !a[$0]++ protegge il lato destro, ovvero assicura che il record corrente sia stampato solo se non è stato stampato prima
  • NR il numero del record corrente, a partire da 1

Ciò significa che AWK viene utilizzato per dividere il PATHcontenuto lungo i :caratteri delimitatori e per filtrare le voci duplicate senza modificare l'ordine.

Poiché le matrici associative AWK sono implementate come tabelle hash, il runtime è lineare (ovvero in O (n)).

Nota che non abbiamo bisogno di cercare :caratteri tra virgolette perché le shell non forniscono quotazioni per supportare le directory con il :suo nome nella PATHvariabile.

Awk + incolla

Quanto sopra può essere semplificato con incolla:

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

Il pastecomando viene utilizzato per separare l'output di awk con due punti. Questo semplifica l'azione awk alla stampa (che è l'azione predefinita).

Pitone

Lo stesso della doppia fodera Python:

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

ok, ma questo rimuove i duplicati da una stringa delimitata da due punti esistente o impedisce l'aggiunta di duplicati a una stringa?
Alexander Mills,

1
sembra il primo
Alexander Mills il

2
@AlexanderMills, beh, l'OP ha appena chiesto di rimuovere i duplicati, quindi questo è ciò che fa la chiamata awk.
Maxschlepzig,

1
Il pastecomando non funziona per me a meno che non aggiunga un finale -per usare STDIN.
Wisbucky,

2
Inoltre, ho bisogno di aggiungere spazi dopo -vche ottengo un errore. -v RS=: -v ORS=. Solo diversi tipi di awksintassi.
Wisbucky,

4

C'è stata una discussione simile su questo qui .

Prendo un approccio un po 'diverso. Invece di accettare semplicemente il PERCORSO impostato da tutti i diversi file di inizializzazione che vengono installati, preferisco utilizzare getconfper identificare il percorso di sistema e posizionarlo prima, quindi aggiungere il mio ordine di percorso preferito, quindi utilizzare awkper rimuovere eventuali duplicati. Questo può davvero accelerare l'esecuzione dei comandi (e in teoria essere più sicuro), ma mi dà confusione.

# 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
Questo è molto pericoloso perché aggiungi un trailing :alla PATH(cioè una voce di stringa vuota), perché la directory di lavoro corrente fa parte della tua PATH.
maxschlepzig,

3

Finché stiamo aggiungendo oneliners non-awk:

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

(Potrebbe essere semplice come PATH=$(zsh -fc 'typeset -U path; echo $PATH')ma zsh legge sempre almeno un zshenvfile di configurazione, che può essere modificato PATH.)

Utilizza due belle funzioni zsh:

  • scalari legati ad array ( typeset -T)
  • e matrici che rimuovono automaticamente valori duplicati ( typeset -U).

simpatico! risposta lavorativa più breve e nativamente senza i due punti alla fine.
Jaap

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

Questo utilizza perl e ha diversi vantaggi:

  1. Rimuove i duplicati
  2. Mantiene l'ordinamento
  3. Mantiene il primo aspetto ( /usr/bin:/sbin:/usr/binsi tradurrà in /usr/bin:/sbin)

2

Inoltre sed(qui usando la sedsintassi GNU ) può fare il lavoro:

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

questo funziona bene solo nel caso in cui il primo percorso sia .come nell'esempio di dogbane.

In generale, è necessario aggiungere ancora un altro scomando:

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

Funziona anche su tale costruzione:

$ 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

Come altri hanno dimostrato che è possibile in una riga usando awk, sed, perl, zsh o bash, dipende dalla vostra tolleranza per le lunghe righe e la leggibilità. Ecco una funzione bash che

  • rimuove i duplicati
  • conserva l'ordine
  • consente spazi nei nomi di directory
  • consente di specificare il delimitatore (il valore predefinito è ':')
  • può essere utilizzato con altre variabili, non solo PERCORSO
  • funziona nelle versioni bash <4, importante se si utilizza OS X che per problemi di licenza non viene fornito con la versione 4 di bash

funzione 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

Per rimuovere i duplicati dal PERCORSO

PATH=$(remove_dups "$PATH")

1

Questa è la mia versione:

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"

Uscita campione:

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

Le versioni bash recenti (> = 4) anche di array associativi, cioè puoi anche usare un 'one liner' bash per esso:

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)

dove:

  • IFS cambia il separatore del campo di input in :
  • declare -A dichiara una matrice associativa
  • ${a[$i]+_}è un parametro di espansione che significa: _viene sostituito se e solo se a[$i]è impostato. Questo è simile al ${parameter:+word}quale verifica anche se non è null. Pertanto, nella seguente valutazione del condizionale, l'espressione _(cioè una singola stringa di caratteri) viene valutata come vera (questo equivale a -n _) - mentre un'espressione vuota viene valutata come falsa.

+1: bello stile di script, ma puoi spiegare la sintassi particolare: ${a[$i]+_}modificando la risposta e aggiungendo un punto elenco. Il resto è perfettamente comprensibile ma mi hai perso lì. Grazie.
Cbhihe,

1
@Cbhihe, ho aggiunto un punto elenco che affronta questa espansione.
maxschlepzig,

Grazie mille. Molto interessante. Non pensavo fosse possibile con le matrici (senza stringhe) ...
Cbhihe,

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

Spiegazione del codice awk:

  1. Separare l'input per due punti.
  2. Aggiungi nuove voci di percorso all'array associativo per una rapida ricerca dei duplicati.
  3. Stampa l'array associativo.

Oltre ad essere conciso, questo one-liner è veloce: awk utilizza una tabella hash concatenata per ottenere prestazioni O (1) ammortizzate.

basato sulla rimozione di voci duplicate $ PATH


Vecchio post, ma potrebbe spiegare: if ( !x[$i]++ ). Grazie.
Cbhihe,

0

Utilizzare awkper dividere il percorso :, quindi scorrere su ogni campo e memorizzarlo in un array. Se ti imbatti in un campo che è già nell'array, significa che l'hai già visto prima, quindi non stamparlo.

Ecco un esempio:

$ 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

(Aggiornato per rimuovere il finale :).


0

Una soluzione - non elegante quanto quelle che cambiano le variabili * RS, ma forse ragionevolmente chiara:

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`

L'intero programma funziona nei blocchi BEGIN ed END . Estrae la variabile PATH dall'ambiente, suddividendola in unità. Quindi scorre sull'array risultante p (che viene creato in ordine di split()). L'array e è un array associativo che viene utilizzato per determinare se abbiamo già visto l'elemento del percorso corrente (ad es. / Usr / local / bin ) e, in caso contrario, è aggiunto a np , con la logica per aggiungere un colon a np se c'è già del testo in np . Il blocco END semplicemente echos np . Ciò potrebbe essere ulteriormente semplificato aggiungendo il-F:flag, eliminando il terzo argomento in split()(come predefinito in FS ) e cambiando np = np ":"in np = np FS, dandoci:

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

Incredibilmente, credevo che for(element in array)avrebbe preservato l'ordine, ma non funziona, quindi la mia soluzione originale non funziona, dato che la gente si arrabbierebbe se qualcuno improvvisamente decifrasse l'ordine del loro $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)}')

Viene mantenuta solo la prima occorrenza e l'ordine relativo è ben mantenuto.


-1

Lo farei solo con strumenti di base come tr, sort e uniq:

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

Se non c'è nulla di speciale o strano nel tuo percorso, dovrebbe funzionare


a proposito, puoi usare sort -uinvece di sort | uniq.
corsa il

11
Poiché l'ordine degli elementi PATH è significativo, questo non è molto utile.
maxschlepzig
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.