Entfernen Sie doppelte $ PATH-Einträge mit dem Befehl awk

48

Ich versuche, eine Bash-Shell-Funktion zu schreiben, mit der ich doppelte Kopien von Verzeichnissen aus meiner Umgebungsvariablen PATH entfernen kann.

Mir wurde gesagt, dass es möglich ist, dies mit einem einzeiligen Befehl mit dem awkBefehl zu erreichen, aber ich kann nicht herausfinden, wie es geht. Weiß jemand wie?

Johnny Williem
quelle

Antworten:

37

Wenn Sie noch keine Duplikate im haben PATHund nur Verzeichnisse hinzufügen möchten, wenn diese noch nicht vorhanden sind, können Sie dies problemlos mit der Shell alleine tun.

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

Und hier ist ein Shell-Snippet, aus dem Duplikate entfernt werden $PATH. Es geht die Einträge nacheinander durch und kopiert diejenigen, die noch nicht gesehen wurden.

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
Gilles 'SO - hör auf böse zu sein'
quelle
Es wäre besser, wenn Sie die Elemente in $ PATH umgekehrt iterieren, da die späteren normalerweise neu hinzugefügt werden und möglicherweise den aktuellen Wert haben.
Eric Wang
2
@EricWang Ich verstehe deine Argumentation nicht. PATH-Elemente werden von vorne nach hinten durchlaufen. Wenn also Duplikate vorhanden sind, wird das zweite Duplikat effektiv ignoriert. Das Durchlaufen von hinten nach vorne würde die Reihenfolge ändern.
Gilles 'SO- hör auf böse zu sein'
@Gilles Wenn Sie eine Variable in PATH dupliziert haben, wird sie wahrscheinlich auf folgende Weise hinzugefügt: PATH=$PATH:x=bDas x im ursprünglichen PATH hat möglicherweise den Wert a. Wenn Sie also in der angegebenen Reihenfolge iterieren, wird der neue Wert ignoriert, aber in umgekehrter Reihenfolge der neue Wert wird wirksam.
Eric Wang
4
@EricWang In diesem Fall hat der Mehrwert keine Auswirkung und sollte ignoriert werden. Indem Sie rückwärts gehen, schaffen Sie den Mehrwert, der vor Ihnen liegt. Wenn die Wertschöpfung vorher hätte gehen sollen, wäre sie als hinzugefügt worden PATH=x:$PATH.
Gilles 'SO- hör auf böse zu sein'
@Gilles Wenn Sie etwas anhängen, heißt das, dass es noch nicht vorhanden ist oder Sie den alten Wert überschreiben möchten, müssen Sie die neue hinzugefügte Variable sichtbar machen. Und normalerweise wird es wie folgt angehängt: PATH=$PATH:...nicht PATH=...:$PATH. Daher ist es sinnvoller, die umgekehrte Reihenfolge zu wiederholen. Auch wenn Ihr Weg auch funktionieren würde, hängen die Leute auf umgekehrte Weise an.
Eric Wang
23

Hier ist eine verständliche einzeilige Lösung, die alles richtig macht: Entfernt Duplikate, behält die Reihenfolge der Pfade bei und fügt am Ende keinen Doppelpunkt hinzu. Sie sollten also einen deduplizierten Pfad erhalten, der genau dasselbe Verhalten wie das Original aufweist:

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

Es wird einfach auf Doppelpunkt ( split(/:/, $ENV{PATH})) aufgeteilt, verwendet grep { not $seen{$_}++ }, um alle wiederholten Instanzen von Pfaden mit Ausnahme des ersten Vorkommens herauszufiltern, und fügt dann die verbleibenden durch Doppelpunkte getrennt wieder zusammen und gibt das Ergebnis aus ( print join(":", ...)).

Wenn Sie mehr Struktur benötigen und auch die Möglichkeit haben möchten, andere Variablen zu deduplizieren, probieren Sie dieses Snippet aus, das ich derzeit in meiner eigenen Konfiguration verwende:

# 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

Dieser Code dedupliziert sowohl PATH als auch MANPATH, und Sie können problemlos dedup_pathvarandere Variablen aufrufen , die durch Doppelpunkte getrennte Pfadlisten enthalten (z. B. PYTHONPATH).

Ryan Thompson
quelle
Aus irgendeinem Grund musste ich eine hinzufügen chomp, um eine nachgestellte Zeile zu entfernen. Das hat bei mir funktioniert:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
Håkon Hægland
12

Hier ist eine schlanke:

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

Länger (um zu sehen, wie es funktioniert):

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

Ok, da du neu in Linux bist, ist hier, wie man PATH tatsächlich ohne ein abschließendes ":" setzt.

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

Übrigens, stellen Sie sicher, dass Sie KEINE Verzeichnisse mit ":" in Ihrem PFAD haben, sonst wird es vermasselt.

Ein Verdienst für:

akostadinov
quelle
-1 Das funktioniert nicht. Ich sehe immer noch Duplikate auf meinem Weg.
Dogbane
4
@dogbane: Es entfernt Duplikate für mich. Es hat jedoch ein subtiles Problem. Die Ausgabe hat ein: am Ende, das, wenn es als $ PATH festgelegt ist, bedeutet, dass dem aktuellen Verzeichnis der Pfad hinzugefügt wird. Dies hat Auswirkungen auf die Sicherheit eines Computers mit mehreren Benutzern.
14.
@dogbane, es funktioniert und ich bearbeitete Post, um einen einzeiligen Befehl ohne das nachgestellte zu haben:
akostadinov
@dogbane Ihre Lösung hat einen Trailing: in der Ausgabe
akostadinov
hmm, Ihr dritter Befehl funktioniert, aber die ersten beiden funktionieren nur, wenn ich sie benutze echo -n. Ihre Befehle scheinen nicht mit "here strings" zu funktionieren, zB try:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
dogbane
6

Hier ist ein AWK One Liner.

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

wo:

  • printf %s "$PATH"druckt den Inhalt $PATHohne nachfolgende Newline aus
  • RS=: Ändert das Begrenzungszeichen des Eingabesatzes (Standard ist Newline)
  • ORS= Ändert den Begrenzer des Ausgabesatzes in die leere Zeichenfolge
  • a der Name eines implizit erstellten Arrays
  • $0 verweist auf den aktuellen Datensatz
  • a[$0] ist eine assoziative Array-Dereferenzierung
  • ++ ist der Post-Inkrement-Operator
  • !a[$0]++ schützt die rechte Seite, dh es wird sichergestellt, dass der aktuelle Datensatz nur gedruckt wird, wenn er zuvor nicht gedruckt wurde
  • NR Die aktuelle Datensatznummer, beginnend mit 1

Dies bedeutet, dass AWK verwendet wird, um den PATHInhalt entlang der :Trennzeichen aufzuteilen und doppelte Einträge herauszufiltern, ohne die Reihenfolge zu ändern.

Da assoziative AWK-Arrays als Hash-Tabellen implementiert sind, ist die Laufzeit linear (dh in O (n)).

Beachten Sie, dass wir nicht nach Anführungszeichen suchen müssen, :da Shells keine Anführungszeichen enthalten , um Verzeichnisse zu unterstützen, :deren Name in der PATHVariablen enthalten ist.

Awk + Paste

Das obige kann mit Paste vereinfacht werden:

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

Der pasteBefehl wird verwendet, um die awk-Ausgabe mit Doppelpunkten zu durchsetzten. Dies vereinfacht das Drucken der awk-Aktion (dies ist die Standardaktion).

Python

Das gleiche wie Python Two-Liner:

$ PATH=$(python3 -c 'import os; from collections import OrderedDict; \
    l=os.environ["PATH"].split(":"); print(":".join(OrderedDict.fromkeys(l)))' )
maxschlepzig
quelle
OK, aber entfernt dies Dupes aus einer vorhandenen Zeichenfolge mit Doppelpunkt als Trennzeichen oder verhindert es, dass Dupes zu einer Zeichenfolge hinzugefügt werden?
Alexander Mills
1
sieht aus wie die frühere
Alexander Mills
2
@AlexanderMills, nun, das OP hat gerade nach dem Entfernen von Duplikaten gefragt, also ist dies das, was der awk-Aufruf macht.
Maxschlepzig
1
Der pasteBefehl funktioniert bei mir nur, wenn ich ein Trailing hinzufüge -, um STDIN zu verwenden.
wisbucky
2
Außerdem muss ich nach dem Leerzeichen einfügen, -vsonst erhalte ich eine Fehlermeldung. -v RS=: -v ORS=. Nur verschiedene Arten von awkSyntax.
wisbucky
4

Es hat eine ähnliche Diskussion darüber gewesen hier .

Ich gehe ein bisschen anders vor. Anstatt nur den getconfPfad zu akzeptieren, der aus den verschiedenen zu installierenden Initialisierungsdateien festgelegt wurde , identifiziere ich lieber den Systempfad und platziere ihn zuerst, füge dann meine bevorzugte Pfadreihenfolge hinzu awkund entferne dann alle Duplikate. Dies kann die Befehlsausführung wirklich beschleunigen oder nicht (und ist theoretisch sicherer), führt aber zu warmen Unschärfen.

# 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
George M
quelle
3
Dies ist sehr gefährlich, da Sie einen abschließenden Pfad :zum PATH(dh einen leeren Zeichenfolgeneintrag) hinzufügen , da dann das aktuelle Arbeitsverzeichnis Teil Ihres Arbeitsverzeichnisses ist PATH.
Maxschlepzig
3

Solange wir nicht-awk Oneliners hinzufügen:

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

(Könnte so einfach sein, PATH=$(zsh -fc 'typeset -U path; echo $PATH')aber zsh liest immer mindestens eine zshenvKonfigurationsdatei, die geändert werden kann PATH.)

Es verwendet zwei nette zsh-Funktionen:

  • An Arrays gebundene Skalare ( typeset -T)
  • und Arrays, die doppelte Werte automatisch entfernen ( typeset -U).
Michał Politowski
quelle
nett! kürzeste funktionierende Antwort und nativ ohne Doppelpunkt am Ende.
Jaap
2
PATH=`perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}'`
export PATH

Dies verwendet Perl und hat mehrere Vorteile:

  1. Duplikate werden entfernt
  2. Es hält die Sortierreihenfolge
  3. Es behält das früheste Aussehen ( /usr/bin:/sbin:/usr/binwird dazu führen /usr/bin:/sbin)
vol7ron
quelle
2

Auch sed(hier mit GNU- sedSyntax) kann die Arbeit erledigen:

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

Dieser funktioniert nur dann gut, wenn der erste Pfad .dem Beispiel von dogbane entspricht.

Im Allgemeinen müssen Sie noch einen weiteren sBefehl hinzufügen :

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

Es funktioniert auch bei solchen Konstruktionen:

$ 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
eilen
quelle
2

Wie andere gezeigt haben, ist es in einer Zeile möglich, awk, sed, perl, zsh oder bash zu verwenden. Dies hängt von Ihrer Toleranz für lange Zeilen und Ihrer Lesbarkeit ab. Hier ist eine Bash-Funktion, die

  • entfernt Duplikate
  • bewahrt die Ordnung
  • Erlaubt Leerzeichen in Verzeichnisnamen
  • Ermöglicht die Angabe des Trennzeichens (standardmäßig ':')
  • kann mit anderen Variablen verwendet werden, nicht nur mit PATH
  • Funktioniert in Bash-Versionen <4, wichtig, wenn Sie OS X verwenden, das für Lizenzierungsprobleme nicht Bash-Version 4 enthält

Bash-Funktion

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

Verwendungszweck

So entfernen Sie Dups aus PATH

PATH=$(remove_dups "$PATH")
amdn
quelle
1

Das ist meine Version:

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

Verwendungszweck: path_no_dup "$PATH"

Beispielausgabe:

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$
Rany Albeg Wein
quelle
1

Aktuelle Bash-Versionen (> = 4) auch von assoziativen Arrays, dh Sie können auch einen Bash-Einzeiler dafür verwenden:

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)

wo:

  • IFS Ändert das Eingabefeld-Trennzeichen in :
  • declare -A deklariert ein assoziatives Array
  • ${a[$i]+_}ist eine Parametererweiterung Bedeutung: _wird nur dann ersetzt, wenn a[$i]gesetzt. Dies ähnelt dem, ${parameter:+word}der auch auf not-null testet. In der folgenden Auswertung der Bedingung wird der Ausdruck _(dh eine einzelne Zeichenfolge) mit true ausgewertet (dies entspricht -n _), während ein leerer Ausdruck mit false ausgewertet wird.
maxschlepzig
quelle
+1: Schöner Skriptstil, aber können Sie die spezielle Syntax erklären: ${a[$i]+_}Bearbeiten Sie Ihre Antwort und fügen Sie einen Aufzählungspunkt hinzu. Der Rest ist vollkommen verständlich, aber du hast mich dort verloren. Danke.
Cbhihe
1
@Cbhihe, ich habe einen Aufzählungspunkt hinzugefügt, der diese Erweiterung anspricht.
Maxschlepzig
Vielen Dank. Sehr interessant. Ich hätte nicht gedacht, dass dies mit Arrays (Nicht-Strings) möglich ist ...
Cbhihe
1
PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`

Erklärung des awk-Codes:

  1. Trennen Sie die Eingabe durch Doppelpunkte.
  2. Fügen Sie dem assoziativen Array neue Pfadeinträge hinzu, um eine schnelle Suche nach Duplikaten zu ermöglichen.
  3. Druckt das assoziative Array.

Dieser Einzeiler ist nicht nur kurz, sondern auch schnell: awk verwendet eine Verkettungstabelle, um eine amortisierte O (1) -Leistung zu erzielen.

basierend auf Entfernen doppelter $ PATH-Einträge

Leftium
quelle
Alte Post, aber könnten Sie erklären: if ( !x[$i]++ ). Vielen Dank.
Cbhihe
0

Verwenden Sie awkdiese Option , um den Pfad weiter zu teilen, die :Schleife über jedes Feld zu führen und es in einem Array zu speichern. Wenn Sie auf ein Feld stoßen, das sich bereits im Array befindet, bedeutet dies, dass Sie es bereits gesehen haben, drucken Sie es also nicht aus.

Hier ist ein Beispiel:

$ 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

(Aktualisiert, um das Ende zu entfernen :.)

Dogbane
quelle
0

Eine Lösung - nicht so elegant wie jene, die die * RS-Variablen ändern, aber vielleicht einigermaßen klar:

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`

Das gesamte Programm arbeitet in den Bausteinen BEGIN und END . Es zieht Ihre PATH-Variable aus der Umgebung und teilt sie in Einheiten auf. Es durchläuft dann das resultierende Array p (das in der Reihenfolge von erstellt wird split()). Das Array e ist ein assoziatives Array, mit dem bestimmt wird, ob wir das aktuelle Pfadelement (z. B. / usr / local / bin ) bereits gesehen haben oder nicht. Wenn nicht, wird es an np angehängt , wobei ein Doppelpunkt angefügt wird np wenn es bereits text in np gibt . Der END- Block gibt einfach np wieder . Dies könnte noch weiter vereinfacht werden, indem die-F:flag, eliminiert das dritte Argument von split()(da es standardmäßig FS ist ) und ändert sich np = np ":"zu np = np FS, was uns ergibt:

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

Naiv, ich habe geglaubt, das for(element in array)würde Ordnung bewahren, aber das tut es nicht, also funktioniert meine ursprüngliche Lösung nicht, da sich die Leute aufregen würden, wenn jemand plötzlich die Reihenfolge ihrer Dinge durcheinanderwirbelt $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
Andrew Beals
quelle
0
export PATH=$(echo -n "$PATH" | awk -v RS=':' '(!a[$0]++){if(b++)printf(RS);printf($0)}')

Nur das erste Vorkommen wird beibehalten und die relative Reihenfolge wird beibehalten.

Cyker
quelle
-1

Ich würde es nur mit grundlegenden Werkzeugen wie tr, sort und uniq machen:

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

Wenn sich nichts Besonderes oder Verrücktes auf Ihrem Weg befindet, sollte es funktionieren

ghm1014
quelle
Übrigens können Sie sort -uanstelle von verwenden sort | uniq.
Ansturm
11
Da die Reihenfolge der PATH-Elemente von Bedeutung ist, ist dies nicht sehr nützlich.
Maxschlepzig