Usuń zduplikowane wpisy $ PATH za pomocą polecenia awk

48

Próbuję napisać funkcję powłoki bash, która pozwoli mi usunąć zduplikowane kopie katalogów ze zmiennej środowiskowej PATH.

Powiedziano mi, że można to osiągnąć za pomocą polecenia jednoliniowego za pomocą awkpolecenia, ale nie mogę wymyślić, jak to zrobić. Czy ktoś wie jak?

Johnny Williem
źródło

Odpowiedzi:

37

Jeśli nie masz jeszcze duplikatów PATHi chcesz dodać katalogi tylko wtedy, gdy jeszcze ich nie ma, możesz to zrobić z łatwością za pomocą samej powłoki.

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

A oto fragment powłoki, który usuwa duplikaty z $PATH. Przegląda wpisy jeden po drugim i kopiuje te, których jeszcze nie widziałem.

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- przestań być zły”
źródło
Byłoby lepiej, gdyby iterować elementy w $ PATH w odwrotnej kolejności, ponieważ te późniejsze są zwykle nowo dodawane i mogą mieć aktualną wartość.
Eric Wang,
2
@EricWang Nie rozumiem twojego rozumowania. Elementy PATH są przemieszczane od przodu do tyłu, więc gdy są duplikaty, drugi duplikat jest skutecznie ignorowany. Iteracja od tyłu do przodu zmieniłaby kolejność.
Gilles „SO- przestań być zły”
@Gilles Po zduplikowaniu zmiennej w PATH, prawdopodobnie jest ona dodawana w ten sposób: PATH=$PATH:x=bx w oryginalnej PATH może mieć wartość a, a więc po iteracji w kolejności, nowa wartość zostanie zignorowana, ale w odwrotnej kolejności nowa wartość zacznie obowiązywać.
Eric Wang,
4
@EricWang W takim przypadku wartość dodana nie ma wpływu, dlatego należy ją zignorować. Przechodząc wstecz, tworzysz wartość dodaną wcześniej. Gdyby wartość dodana miała być wcześniej, byłaby dodana jako PATH=x:$PATH.
Gilles „SO- przestań być zły”
@Gilles Gdy dodasz coś, co oznacza, że ​​jeszcze go nie ma, lub chcesz zastąpić starą wartość, musisz pokazać nową dodaną zmienną. I, zgodnie z konwencją, zwykle dodaje się go w ten sposób: PATH=$PATH:...nie PATH=...:$PATH. Dlatego właściwsze jest iterowanie w odwrotnej kolejności. Nawet jeśli twój sposób też by działał, ludzie dołączają w odwrotny sposób.
Eric Wang,
23

Oto zrozumiałe jedno-liniowe rozwiązanie, które robi wszystkie właściwe rzeczy: usuwa duplikaty, zachowuje porządek ścieżek i nie dodaje dwukropka na końcu. Powinien więc dać deduplikowaną ścieżkę, która daje dokładnie takie samo zachowanie jak oryginał:

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

Po prostu dzieli się na dwukropek ( split(/:/, $ENV{PATH})), używa zastosowań grep { not $seen{$_}++ }do odfiltrowania powtarzających się wystąpień ścieżek oprócz pierwszego wystąpienia, a następnie łączy pozostałe z powrotem oddzielone dwukropkami i wypisuje wynik ( print join(":", ...)).

Jeśli chcesz mieć więcej struktury wokół niego, a także możliwość deduplikacji innych zmiennych, wypróbuj ten fragment kodu, którego obecnie używam w mojej własnej konfiguracji:

# 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

Ten kod deduplikuje zarówno PATH, jak i MANPATH, i możesz łatwo wywoływać dedup_pathvarinne zmienne, które przechowują listy ścieżek oddzielone dwukropkami (np. PYTHONPATH).

Ryan Thompson
źródło
Z jakiegoś powodu musiałem dodać a, chompaby usunąć końcowy znak nowej linii. To zadziałało dla mnie:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
Håkon Hægland
12

Oto elegancki:

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

Dłuższy (aby zobaczyć, jak to działa):

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

Ok, ponieważ dopiero zaczynasz przygodę z Linuksem, oto jak ustawić PATH bez końcowego „:”

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

btw upewnij się, że NIE masz katalogów zawierających „:” w ŚCIEŻCE, w przeciwnym razie zostanie to pomieszane.

trochę uznania dla:

akostadinov
źródło
-1 to nie działa. Nadal widzę duplikaty na mojej ścieżce.
dogbane
4
@dogbane: Usuwa dla mnie duplikaty. Ma jednak subtelny problem. Dane wyjściowe mają: na końcu, który jeśli jest ustawiony jako $ PATH, oznacza, że ​​do bieżącego katalogu dodawana jest ścieżka. Ma to wpływ na bezpieczeństwo komputera z wieloma użytkownikami.
camh
@dogbane, to działa i edytowałem post, aby mieć polecenie w jednym wierszu bez końcowego:
akostadinov
@dogbane twoje rozwiązanie ma końcowe: w wyniku
akostadinov
hmm, twoje trzecie polecenie działa, ale pierwsze dwa nie działają, chyba że użyję echo -n. Twoje polecenia nie działają z „ciągami tutaj”, np. Spróbuj:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
dogbane
6

Oto jedna wkładka AWK.

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

gdzie:

  • printf %s "$PATH"drukuje zawartość $PATHbez końca nowej linii
  • RS=: zmienia znak ogranicznika rekordu wejściowego (domyślnie jest to nowy wiersz)
  • ORS= zmienia separator rekordu wyjściowego na pusty ciąg
  • a nazwa niejawnie utworzonej tablicy
  • $0 odwołuje się do bieżącego rekordu
  • a[$0] jest dereferencją tablicy asocjacyjnej
  • ++ jest operatorem post-increment
  • !a[$0]++ chroni prawą stronę, tzn. upewnia się, że bieżący rekord jest drukowany tylko, jeśli nie był wcześniej drukowany
  • NR aktualny numer rekordu, zaczynając od 1

Oznacza to, że AWK służy do dzielenia PATHtreści wzdłuż :znaków ogranicznika i do filtrowania zduplikowanych wpisów bez zmiany kolejności.

Ponieważ tablice asocjacyjne AWK są implementowane jako tabele skrótów, środowisko wykonawcze jest liniowe (tj. W O (n)).

Zauważ, że nie musimy szukać :znaków cytowanych, ponieważ powłoki nie zawierają cudzysłowów w celu obsługi katalogów z :nazwą w PATHzmiennej.

Awk + wklej

Powyższe można uprościć za pomocą wklejania:

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

pasteKomenda służy do przeplatać wyjście awk z dwukropkiem. Upraszcza to akcję awk do drukowania (która jest operacją domyślną).

Pyton

Taki sam jak dwu-liniowy Python:

$ PATH=$(python3 -c 'import os; from collections import OrderedDict; \
    l=os.environ["PATH"].split(":"); print(":".join(OrderedDict.fromkeys(l)))' )
maxschlepzig
źródło
ok, ale czy to usuwa duplikaty z istniejącego łańcucha rozdzielanego dwukropkami, czy też zapobiega dodawaniu duplikatów do łańcucha?
Alexander Mills,
1
wygląda jak poprzedni
Alexander Mills,
2
@AlexanderMills, cóż, OP właśnie zapytał o usunięcie duplikatów, więc właśnie to robi wywołanie awk.
maxschlepzig
1
pasteKomenda nie działa dla mnie chyba dodać końcowego znaku -użyć standardowego wejścia.
wisbucky 24.04.17
2
Ponadto muszę dodać spacje po tym, -vbo w przeciwnym razie pojawia się błąd. -v RS=: -v ORS=. Po prostu różne smaki awkskładni.
wisbucky 24.04.17
4

Podobna dyskusja na ten temat tutaj .

Podchodzę trochę inaczej. Zamiast akceptować getconfścieżkę ustawioną dla wszystkich różnych plików inicjujących, które są instalowane, wolę użyć do zidentyfikowania ścieżki systemowej i umieszczenia jej najpierw, następnie dodaj moją preferowaną kolejność ścieżek, a następnie użyj, awkaby usunąć duplikaty. To może, ale nie musi, naprawdę przyspieszyć wykonywanie poleceń (i teoretycznie jest bardziej bezpieczne), ale daje mi ciepłe fuzje.

# 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.
źródło
3
Jest to bardzo niebezpieczne, ponieważ dodajesz znak końcowy :do PATH(tzn. Pusty ciąg znaków), ponieważ bieżący katalog roboczy jest częścią twojego PATH.
maxschlepzig
3

Tak długo, jak dodajemy nie-awk oneliner:

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

(Może być tak proste, PATH=$(zsh -fc 'typeset -U path; echo $PATH')ale zsh zawsze czyta co najmniej jeden zshenvplik konfiguracyjny, który można modyfikować PATH.)

Wykorzystuje dwie ładne funkcje Zsh:

  • skalary powiązane z tablicami ( typeset -T)
  • oraz tablice, które autorove duplikują wartości ( typeset -U).
Michał Politowski
źródło
miły! najkrótsza robocza odpowiedź i natywnie bez dwukropka na końcu.
jaap
2
PATH=`perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}'`
export PATH

Używa perla i ma kilka zalet:

  1. Usuwa duplikaty
  2. Utrzymuje porządek sortowania
  3. Zachowuje najwcześniejszy wygląd ( /usr/bin:/sbin:/usr/binspowoduje /usr/bin:/sbin)
vol7ron
źródło
2

Również sed(tutaj przy użyciu sedskładni GNU ) może wykonać zadanie:

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

ten działa dobrze tylko w przypadku, gdy pierwsza ścieżka jest .jak w przykładzie Dogbane.

W ogólnym przypadku musisz dodać jeszcze jedno spolecenie:

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

Działa nawet na takiej konstrukcji:

$ 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
wysypka
źródło
2

Jak inni wykazali, jest to możliwe w jednej linii za pomocą awk, sed, perl, zsh lub bash, zależy od twojej tolerancji dla długich linii i czytelności. Oto funkcja bash, która

  • usuwa duplikaty
  • zachowuje porządek
  • dopuszcza spacje w nazwach katalogów
  • pozwala określić ogranicznik (domyślnie „:”)
  • może być używany z innymi zmiennymi, nie tylko PATH
  • działa w wersjach bash <4, ważne, jeśli używasz OS X, który do problemów licencyjnych nie dostarcza bash w wersji 4

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

stosowanie

Aby usunąć duplikaty ze ŚCIEŻKI

PATH=$(remove_dups "$PATH")
Amdn
źródło
1

To jest moja wersja:

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

Stosowanie: path_no_dup "$PATH"

Przykładowe dane wyjściowe:

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
źródło
1

Najnowsze wersje bash (> = 4) również tablic asocjacyjnych, tzn. Możesz do tego użyć bash „one liner”:

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)

gdzie:

  • IFS zmienia separator pola wejściowego na :
  • declare -A deklaruje tablicę asocjacyjną
  • ${a[$i]+_}oznacza rozszerzenie parametru: _jest podstawiane, jeśli i tylko jeśli a[$i]jest ustawione. Jest to podobne do tego, ${parameter:+word}które testuje również na wartość inną niż null. Tak więc w poniższej ocenie warunkowej wyrażenie _(tj. Ciąg znaków składający się z jednego znaku) ma wartość true (jest to równoważne z -n _) - podczas gdy puste wyrażenie ma wartość false.
maxschlepzig
źródło
+1: fajny styl skryptu, ale czy możesz wyjaśnić konkretną składnię: ${a[$i]+_}edytując odpowiedź i dodając jeden punkt. Reszta jest całkowicie zrozumiała, ale mnie tam zgubiłeś. Dziękuję Ci.
Cbhihe,
1
@Chihi, dodałem punktor dotyczący tego rozszerzenia.
maxschlepzig
Dziękuję Ci bardzo. Bardzo interesujące. Nie sądziłem, że jest to możliwe z tablicami (bez ciągów) ...
Cbhihe
1
PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`

Objaśnienie kodu awk:

  1. Oddziel dane wejściowe dwukropkami.
  2. Dodaj nowe wpisy ścieżki do tablicy asocjacyjnej, aby szybko wyszukać duplikaty.
  3. Wyświetla tablicę asocjacyjną.

Oprócz tego, że jest zwięzły, ten jednowarstwowy jest szybki: awk używa łańcuchowej tabeli skrótów, aby osiągnąć amortyzowaną wydajność O (1).

na podstawie usuwania zduplikowanych wpisów $ PATH

Leftium
źródło
Stary post, ale można wyjaśnić: if ( !x[$i]++ ). Dzięki.
Cbhihe,
0

Użyj, awkaby podzielić ścieżkę :, a następnie zapętlić każde pole i zapisać je w tablicy. Jeśli natrafisz na pole, które już znajduje się w tablicy, oznacza to, że już je widziałeś, więc nie drukuj.

Oto przykład:

$ 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

(Zaktualizowano, aby usunąć końcowe :).

dogbane
źródło
0

Rozwiązanie - nie takie eleganckie jak te, które zmieniają zmienne * RS, ale być może dość jasne:

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`

Cały program działa w blokach BEGIN i END . Wyciąga zmienną PATH ze środowiska, dzieląc ją na jednostki. Następnie iteruje się nad wynikową tablicą p (utworzoną w kolejności według split()). Tablica e jest tablicą asocjacyjną, która jest używana do ustalenia, czy widzieliśmy bieżący element ścieżki (np. / Usr / local / bin ) przed, a jeśli nie, jest dołączany do np. Z logiką, aby dołączyć dwukropek do np, jeśli jest już tekst w np . END blok prostu Echos NP . Można to dodatkowo uprościć, dodając-F:flagę, eliminując trzeci argument split()(domyślnie FS ), i zmieniając np = np ":"na np = np FS, dając nam:

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

Naiwnie wierzyłem, for(element in array)że zachowałoby to porządek, ale nie działa, więc moje oryginalne rozwiązanie nie działa, ponieważ ludzie byliby zdenerwowani, gdyby ktoś nagle zakodował swoją kolejność $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
źródło
0
export PATH=$(echo -n "$PATH" | awk -v RS=':' '(!a[$0]++){if(b++)printf(RS);printf($0)}')

Zachowywane jest tylko pierwsze wystąpienie, a porządek względny jest dobrze utrzymany.

Cyker
źródło
-1

Zrobiłbym to tylko za pomocą podstawowych narzędzi, takich jak tr, sort i uniq:

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

Jeśli na twojej ścieżce nie ma nic specjalnego ani dziwnego, powinno działać

ghm1014
źródło
btw, możesz użyć sort -uzamiast sort | uniq.
pędzi
11
Ponieważ kolejność elementów PATH jest znacząca, nie jest to bardzo przydatne.
maxschlepzig