Awk komutuyla yinelenen $ PATH girişlerini kaldırın


48

Dizinlerin kopyalarını PATH ortam değişkenimden çıkarmamı sağlayacak bir bash shell işlevi yazmaya çalışıyorum.

Bunu, komutu kullanarak tek satır komutuyla gerçekleştirmenin mümkün olduğu söylendi awk, ancak nasıl yapılacağını çözemiyorum. Nasıl bilen var mı?



Yanıtlar:


37

Zaten yinelenen kopyalarınız yoksa PATHve yalnızca zaten orada değilse, dizin eklemek istiyorsanız, bunu yalnızca kabukla kolayca yapabilirsiniz.

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

Ve işte yinelenenleri kaldıran bir kabuk pasajı $PATH. Girişleri birer birer geçirir ve henüz görülmemiş olanları kopyalar.

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

$ PATH içindeki öğeleri yinelemeliyse daha iyi olur, çünkü sonrakiler genellikle yeni eklenir ve güncel olan değere sahip olabilirler.
Eric Wang

2
@EricWang Nedenini anlamıyorum. PATH öğeleri önden arkaya hareket eder, bu nedenle kopyalar olduğunda, ikinci kopya etkin bir şekilde göz ardı edilir. Arkadan öne doğru yineleme sıralamayı değiştirir.
Gilles 'SO- kötü davranmayı

@Gilles PATH'de yinelenen değişkeniniz olduğunda, muhtemelen şu şekilde eklenir: PATH=$PATH:x=borijinal PATH'deki x, a değerine sahip olabilir, bu nedenle sırayla yinelendiğinde, yeni değer yoksayılır, ancak ters sırada iken, yeni değer etkili olacaktır.
Eric Wang

4
@EricWang Bu durumda, katma değerin etkisi yoktur, bu nedenle dikkate alınmamalıdır. Geriye doğru giderek, katma değeri daha önce gelmesini sağlıyorsunuz. Katma değerin daha önce olması gerekiyorsa, olarak eklenmiş olurdu PATH=x:$PATH.
Gilles 'SO- kötülük' dur

@Gilles Bir şey eklediğinizde, bu henüz orada olmadığı veya eski değeri geçersiz kılmak istediğiniz anlamına gelir, bu nedenle yeni eklenen değişkeni görünür hale getirmeniz gerekir. Ve, konvansiyonel olarak, genellikle bu şekilde eklenir: PATH=$PATH:...hayır PATH=...:$PATH. Bu nedenle, tersine çevrilmiş düzeni yinelemek daha uygun olur. Yolunuz da işe yarayacak olsa da, insanlar ters şekilde eklerler.
Eric Wang

23

İşte tüm doğru şeyleri yapan anlaşılır bir tek taraflı çözüm: yinelemeleri kaldırır, yolların sırasını korur ve sonunda bir sütun eklemez. Bu yüzden, size orijinaliyle tamamen aynı davranışı veren, veri tekilleştirilmiş bir PATH vermelidir:

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

Basitçe iki nokta üstüste ( split(/:/, $ENV{PATH})) bölünür grep { not $seen{$_}++ }, ilk oluşum dışında herhangi bir tekrarlanan yol örneğini filtrelemek için kullanır ve ardından kalanları iki nokta üst üste virgüllerle ayırarak birleştirir ve sonucu yazdırır ( print join(":", ...)).

Çevresinde daha fazla yapı ve diğer değişkenleri de tekilleştirme yeteneği için, şu anda kendi yapılandırmamda kullandığım bu pasajı deneyin:

# 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

Bu kod hem PATH hem de MANPATH'yi tekilleştirecek ve dedup_pathvariki nokta üstüste ayrılmış yol listelerini (örn. PYTHONPATH) tutan diğer değişkenleri kolayca arayabilirsiniz .


Nedense chomptakip eden bir yeni satırı kaldırmak için bir eklemek zorunda kaldım . Bu benim için çalıştı:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
Håkon Hægland

12

İşte şık bir tane:

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

Daha uzun (nasıl çalıştığını görmek için):

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

Tamam, Linux için yenisin, işte PATH’i izlemeden gerçekte “:”

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

btw PATH'nizde ":" içeren dizinler bulunmadığından emin olun, aksi takdirde karışacaktır.

Bazı kredi için:


-1 bu işe yaramıyor. Hala yolumdaki kopyaları görüyorum.
dogbane

4
@dogbane: Benim için çiftleri kaldırıyor. Ancak, ince bir sorunu var. Çıktıda bir: var: $ PATH'nız olarak ayarlanmışsa, geçerli dizinin yolun eklendiği anlamına gelir. Bunun çok kullanıcılı bir makinede güvenlik etkileri var.
camh

@dogbane, işe yarıyor ve yazıyı izlemeden tek bir satır komutu verecek şekilde düzenledim:
akostadinov

@dogbane çözümünüzün bir yolu var: çıktıda
akostadinov

hmm, üçüncü komutun çalışıyor, ama ilk ikisi ben kullanmadığım sürece işe yaramıyor echo -n. Komutların "burada dizeleri" ile çalışmıyor gibi görünüyor, örneğin, dene:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
dogbane

6

İşte bir AWK bir astar.

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

nerede:

  • printf %s "$PATH"$PATHizleyen bir yeni satır olmadan içeriğini yazdırır
  • RS=: giriş kaydı sınırlayıcı karakterini değiştirir (varsayılan satır yenidir)
  • ORS= çıktı kayıt sınırlayıcısını boş dizeye değiştirir
  • a dolaylı olarak oluşturulan bir dizinin adı
  • $0 mevcut rekoru referans alır
  • a[$0] bir ilişkisel dizi başvurusu
  • ++ artım sonrası işleci
  • !a[$0]++ sağ tarafı korur, yani geçerli kaydın yalnızca daha önce yazdırılmadıysa yazdırılmasını sağlar.
  • NR 1 ile başlayan geçerli kayıt numarası

Bu, AWK'nın PATHiçeriği :sınırlayıcı karakterler boyunca bölmek ve yinelenen girişleri sırayı değiştirmeden filtrelemek için kullanıldığı anlamına gelir .

AWK ilişkisel dizileri karma tabloları olarak uygulandığı için çalışma zamanı doğrusaldır (yani O (n)).

Alıntılanan :karakterleri aramamıza gerek olmadığını unutmayın; çünkü kabuklar , adındaki adlarında dizinleri desteklemek için tırnak işareti sunmaz: .PATH değişkende .

Awk + yapıştır

Yukarıdakiler yapıştırma ile basitleştirilebilir:

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

pasteKomut nokta üst üste ile awk çıkışı serpmek için kullanılır. Bu, yazdırılacak awk eylemini basitleştirir (varsayılan eylemdir).

piton

Python iki astar ile aynı:

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

tamam, ancak bu, kopyaları varolan iki noktadan ayrılmış dizgiden kaldırıyor mu, yoksa dizelerin dizeye eklenmesini engelliyor mu?
Alexander Mills,

1
eski gibi görünüyor
Alexander Mills

2
@AlexanderMills, peki, OP sadece kopyaları kaldırmayı istedi, bu yüzden awk çağrısının yaptığı bu.
maxschlepzig

1
STDIN kullanmak pasteiçin bir iz eklemediğim sürece bu komut benim için çalışmıyor -.
wisbucky

2
Ayrıca, sonra -vhata eklemeliyim, yoksa hata alıyorum. -v RS=: -v ORS=. Sadece farklı awksözdizimi lezzetleri .
wisbucky

4

Bu konuda benzer bir tartışma yaşandı burada .

Biraz farklı bir yaklaşım benim. Yüklenen tüm farklı başlatma dosyalarından ayarlanan PATH'ı kabul etmek yerine getconf, sistem yolunu tanımlamak ve önce yerleştirmek için kullanmayı, sonra tercih edilen yol sırasımı eklemeyi, sonra awkyinelemeleri kaldırmak için kullanmayı tercih ederim . Bu, komut çalıştırma işlemini gerçekten hızlandırabilir veya hızlandırabilir (ve teoride daha güvenlidir), ancak bana sıcak tüyler veriyor.

# 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
Eğer bir sondaki eklemek için bu çok tehlikelidir :için PATHo anki çalışma dizini bölümü, aramalarınızdan çünkü (yani boş bir dize girişi) PATH.
maxschlepzig

3

Awk olmayan oneliners eklediğimiz sürece:

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

(Bu kadar basit olabilir, PATH=$(zsh -fc 'typeset -U path; echo $PATH')fakat zsh her zaman en az bir tane zshenvyapılandırma dosyasını okur ve değiştirebilir PATH.)

İki güzel zsh özelliği kullanır:

  • skaler dizilere bağlı ( typeset -T)
  • ve otomatik olarak yinelenen değerleri ( typeset -U) dizileri .

Güzel! en kısa çalışma cevabı ve sonunda kolon olmadan yerel olarak.
16'da

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

Bu perl kullanır ve birkaç faydası vardır:

  1. Çiftleri kaldırır
  2. Bu sıralama düzenini tutar
  3. En erken görünümü korur ( /usr/bin:/sbin:/usr/binsonuçta ortaya çıkar /usr/bin:/sbin)

2

Ayrıca sed(burada GNU sedsözdizimini kullanarak ) işi yapabilir:

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

bu sadece ilk yol olması durumunda iyi çalışır . dogbane'nin örneğindeki gibi yarar.

Genel bir durumda, başka bir skomut daha eklemeniz gerekir :

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

Bu tür inşaatlarda bile çalışır:

$ 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

Diğerlerinin gösterdiği gibi, awk, sed, perl, zsh veya bash kullanarak bir satırda mümkün olabilir, uzun satırlar ve okunabilirlik için toleransınıza bağlıdır. İşte bir bash işlevi

  • kopyaları siler
  • düzeni korur
  • dizin adlarında boşluklara izin verir
  • sınırlayıcıyı belirlemenizi sağlar (varsayılan olarak ':' olarak)
  • sadece PATH ile değil diğer değişkenlerle de kullanılabilir
  • basım sürümleri <4'te çalışır, lisans sorunları için bash sürüm 4'ü göndermeyen OS X kullanıyorsanız önemlidir

bash işlevi

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

kullanım

DupH’leri PATH’tan çıkarmak için

PATH=$(remove_dups "$PATH")

1

Bu benim versiyonum:

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

Kullanımı: path_no_dup "$PATH"

Örnek çıktı:

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

Aynı zamanda ilişkisel dizilerin son bash sürümleri (> = 4), yani bunun için bir bash 'one liner' kullanabilirsiniz:

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)

nerede:

  • IFS giriş alanı ayırıcısını değiştirir :
  • declare -A ilişkisel dizi bildirir
  • ${a[$i]+_}parametre genişletme anlamıdır: _eğer ve sadece a[$i]ayarlanmışsa sübstitüe edilir . Bu, ${parameter:+word}boş olmayanları sınamak için de benzer . Bu nedenle, şartlılığın aşağıdaki değerlendirmesinde, ifade _(yani tek karakterli bir dize) true olarak değerlendirilir (buna eşittir -n _) - boş bir ifade false olarak değerlendirilir.

+1: komut dosyası tarzını güzel, ancak sözdizimini açıklayabilir misiniz: ${a[$i]+_}cevabınızı düzenleyerek ve bir madde işareti ekleyerek. Gerisi tamamen anlaşılabilir ama beni orada kaybettin. Teşekkür ederim.
Cbhihe

1
@Cbhihe, bu genişlemeyi hedefleyen bir madde işareti noktası ekledim.
maxschlepzig

Çok teşekkür ederim. Çok ilginç. Bunun dizelerle (
telsiz

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

Awk kodunun açıklaması:

  1. Girişi sütunlarla ayırın.
  2. Hızlı yinelenen aramalar için ilişkisel diziye yeni yol girişleri ekleyin.
  3. İlişkisel diziyi yazdırır.

Vücuduna ek olarak, bu tek astar hızlıdır: awk, itfa edilmiş O (1) performansını elde etmek için bir zincirleme karma tablosu kullanır.

yinelenen $ PATH girişlerini kaldırma temelli


Eski sonrası, ancak açıklayabilir: if ( !x[$i]++ ). Teşekkürler.
Cbhihe

0

awkYolu açmak için kullanın :, ardından her alanın üzerine gelin ve bir dizide saklayın. Dizide bulunan bir alana rastlarsanız, bu daha önce görmüş olduğunuz anlamına gelir, bu nedenle yazdırmayın.

İşte bir örnek:

$ 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

(İzi kaldırmak için güncellendi :.)


0

Bir çözüm - * RS değişkenlerini değiştirenler kadar zarif değil, belki de oldukça net:

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`

Tüm program BEGIN ve END bloklarında çalışır. PATH değişkeninizi ortamdan çekerek birimlere böler. Daha sonra, sonuç dizisi p'yi (sırayla yaratılır split()) tekrar eder. Dizi E şu andaki yol elementi (örneğin seen olup olmadığını belirlemek için kullanılan bir birleştirici dizidir / usr / yerel / bin öncesi), ve değilse, eklenen np bir kolon eklemek için mantığı ile, np içinde zaten bir metin varsa np . SON blok basitçe yankılar np . Bu ekleyerek daha da basitleştirilebilir-F:bayrak, üçüncü argüman ortadan kaldırarak split()(için varsayılan olarak olarak FS ve değiştirme) np = np ":"için np = np FSbize veren:

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

Doğal olarak, bunun for(element in array)düzeni koruyacağına inandım , ancak öyle olmaz, bu yüzden asıl çözümüm işe yaramaz, çünkü eğer biri aniden birisinin düzenini karıştırırsa, halk üzülecektir $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)}')

Sadece ilk oluşum korunur ve nispi düzen iyi korunur.


-1

Bunu sadece tr, sort ve uniq gibi temel araçlarla yapardım:

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

Yolunuzda özel veya garip bir şey yoksa, çalışması gerekir.


BTW sort -uyerine kullanabilirsiniz sort | uniq.
acele

11
PATH öğelerinin sırası önemli olduğundan, bu çok kullanışlı değildir.
maxschlepzig
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.