Hapus entri $ PATH duplikat dengan perintah awk

48

Saya mencoba untuk menulis fungsi bash shell yang akan memungkinkan saya untuk menghapus duplikat direktori dari variabel lingkungan PATH saya.

Saya diberitahu bahwa adalah mungkin untuk mencapai ini dengan satu perintah baris menggunakan awkperintah, tetapi saya tidak tahu bagaimana melakukannya. Adakah yang tahu bagaimana caranya?

Johnny Williem
sumber

Jawaban:

37

Jika Anda belum memiliki duplikat di PATHdan Anda hanya ingin menambahkan direktori jika belum ada di sana, Anda dapat melakukannya dengan mudah dengan shell saja.

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

Dan inilah cuplikan shell yang menghilangkan duplikat dari $PATH. Itu berjalan melalui entri satu per satu, dan menyalin yang belum terlihat.

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 'SANGAT berhenti menjadi jahat'
sumber
Akan lebih baik, jika iterasi item dalam $ PATH secara terbalik, karena yang kemudian biasanya baru ditambahkan, dan mereka mungkin memiliki nilai yang up to date.
Eric Wang
2
@ EricWang Saya tidak mengerti alasan Anda. Elemen PATH dilalui dari depan ke belakang, jadi ketika ada duplikat, duplikat kedua diabaikan secara efektif. Iterasi dari belakang ke depan akan mengubah urutan.
Gilles 'SO- stop being evil'
@Gilles Ketika Anda memiliki duplikat variabel dalam PATH, mungkin itu ditambahkan dengan cara ini:, PATH=$PATH:x=bx dalam PATH asli mungkin memiliki nilai a, jadi ketika iterate dalam urutan, maka nilai baru akan diabaikan, tetapi ketika dalam urutan terbalik, yang baru nilai akan berlaku.
Eric Wang
4
@ EricWang Dalam hal ini, nilai tambah tidak berpengaruh sehingga harus diabaikan. Dengan mundur, Anda membuat nilai tambah datang sebelumnya. Jika nilai tambah seharusnya terjadi sebelumnya, itu akan ditambahkan sebagai PATH=x:$PATH.
Gilles 'SANGAT berhenti menjadi jahat'
@Gilles Saat Anda menambahkan sesuatu, itu berarti belum ada di sana, atau Anda ingin mengganti nilai lama, jadi Anda perlu membuat variabel tambahan yang baru terlihat. Dan, berdasarkan konvensi, biasanya ditambahkan dengan cara ini: PATH=$PATH:...tidak PATH=...:$PATH. Oleh karena itu lebih tepat untuk mengulangi urutan terbalik. Meskipun cara Anda juga akan berhasil, maka orang menambahkan dengan cara sebaliknya.
Eric Wang
23

Berikut ini adalah solusi satu-liner yang dapat dipahami yang melakukan semua hal yang benar: menghapus duplikat, mempertahankan urutan jalur, dan tidak menambahkan titik dua pada akhirnya. Jadi itu akan memberi Anda PATH deduplicated yang memberikan perilaku yang sama persis seperti aslinya:

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

Itu hanya terbagi pada titik dua ( split(/:/, $ENV{PATH})), menggunakan kegunaan grep { not $seen{$_}++ }untuk menyaring setiap contoh jalur kecuali untuk kejadian pertama, dan kemudian bergabung dengan yang tersisa kembali bersama-sama dipisahkan oleh titik dua dan mencetak hasilnya ( print join(":", ...)).

Jika Anda ingin beberapa struktur di sekitarnya, serta kemampuan untuk mendupuplikasi variabel lain juga, coba cuplikan ini, yang saat ini saya gunakan dalam konfigurasi saya sendiri:

# 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

Kode itu akan mendupuplikasi PATH dan MANPATH, dan Anda dapat dengan mudah memanggil dedup_pathvarvariabel lain yang menyimpan daftar jalur yang dipisahkan oleh titik dua (misalnya PYTHONPATH).

Ryan Thompson
sumber
Untuk beberapa alasan saya harus menambahkan chompuntuk menghapus baris baru. Ini bekerja untuk saya:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
Håkon Hægland
12

Inilah yang ramping:

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

Lebih lama (untuk melihat cara kerjanya):

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

Oke, karena Anda baru mengenal linux, berikut adalah cara untuk benar-benar mengatur PATH tanpa trailing ":"

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

btw pastikan untuk TIDAK memiliki direktori yang berisi ":" di PATH Anda, jika tidak maka akan kacau.

beberapa kredit untuk:

akostadinov
sumber
-1 ini tidak berhasil. Saya masih melihat duplikat di jalur saya.
dogbane
4
@dogbane: Ini menghapus duplikat untuk saya. Namun memiliki masalah halus. Outputnya memiliki: pada akhirnya yang jika diset sebagai $ PATH Anda, berarti direktori saat ini ditambahkan path. Ini memiliki implikasi keamanan pada mesin multi-pengguna.
camh
@dogbane, ia berfungsi dan saya mengedit pos untuk memiliki satu perintah baris tanpa trailing:
akostadinov
@dogbane solusi Anda memiliki trailing: di output
akostadinov
hmm, perintah ketiga Anda berfungsi, tetapi dua yang pertama tidak berfungsi kecuali saya gunakan echo -n. Perintah Anda tampaknya tidak berfungsi dengan "di sini string" misalnya coba:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
dogbane
6

Ini adalah AWK one liner.

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

dimana:

  • printf %s "$PATH"mencetak konten $PATHtanpa baris baru
  • RS=: mengubah karakter pembatas rekaman input (standarnya adalah baris baru)
  • ORS= mengubah pembatas catatan keluaran ke string kosong
  • a nama array yang dibuat secara implisit
  • $0 referensi catatan saat ini
  • a[$0] adalah dereference array asosiatif
  • ++ adalah operator pasca kenaikan
  • !a[$0]++ menjaga sisi kanan, yaitu memastikan bahwa catatan saat ini hanya dicetak, jika tidak dicetak sebelumnya
  • NR nomor rekaman saat ini, dimulai dengan 1

Itu berarti AWK digunakan untuk membagi PATHkonten di sepanjang :karakter pembatas dan untuk menyaring entri duplikat tanpa mengubah urutan.

Karena array asosiatif AWK diimplementasikan sebagai tabel hash runtime adalah linear (yaitu dalam O (n)).

Perhatikan bahwa kita tidak perlu mencari :karakter yang dikutip karena shell tidak memberikan penawaran untuk mendukung direktori dengan :namanya dalam PATHvariabel.

Awk + rekat

Di atas dapat disederhanakan dengan menempelkan:

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

The pastePerintah ini digunakan untuk menyelingi output awk dengan titik dua. Ini menyederhanakan tindakan awk untuk mencetak (yang merupakan tindakan standar).

Python

Sama dengan Python two-liner:

$ PATH=$(python3 -c 'import os; from collections import OrderedDict; \
    l=os.environ["PATH"].split(":"); print(":".join(OrderedDict.fromkeys(l)))' )
maxschlepzig
sumber
ok, tetapi apakah ini menghapus dupes dari string yang dibatasi usus besar yang ada, atau apakah itu mencegah dupes dari ditambahkan ke string?
Alexander Mills
1
tampak seperti mantan
Alexander Mills
2
@AlexanderMills, well, OP hanya bertanya tentang menghapus duplikat jadi ini adalah apa yang dilakukan panggilan awk.
maxschlepzig
1
The pasteperintah tidak bekerja untuk saya kecuali saya menambahkan trailing -untuk menggunakan STDIN.
wisbucky
2
Juga, saya perlu menambahkan spasi setelah -vatau saya mendapatkan kesalahan. -v RS=: -v ORS=. Hanya berbeda rasa dari awksintaks.
wisbucky
4

Telah ada diskusi serupa tentang ini di sini .

Saya mengambil sedikit pendekatan yang berbeda. Daripada hanya menerima PATH yang diatur dari semua file inisialisasi berbeda yang diinstal, saya lebih suka menggunakan getconfuntuk mengidentifikasi jalur sistem dan menempatkannya terlebih dahulu, kemudian menambahkan urutan jalur pilihan saya, kemudian gunakan awkuntuk menghapus duplikat. Ini mungkin atau mungkin tidak benar-benar mempercepat eksekusi perintah (dan secara teori lebih aman), tetapi ini memberi saya fuzzies hangat.

# 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
sumber
3
Ini sangat berbahaya karena Anda menambahkan trailing :ke PATH(yaitu entri string kosong), karena direktori kerja saat ini adalah bagian dari Anda PATH.
maxschlepzig
3

Selama kami menambahkan oneliners non-awk:

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

(Bisa sesederhana PATH=$(zsh -fc 'typeset -U path; echo $PATH')tapi zsh selalu membaca setidaknya satu zshenvfile konfigurasi, yang dapat dimodifikasi PATH.)

Ini menggunakan dua fitur zsh yang bagus:

  • skal terikat ke array ( typeset -T)
  • dan array yang autoremove nilai duplikat ( typeset -U).
Michał Politowski
sumber
bagus! jawaban kerja terpendek, dan secara alami tanpa titik dua pada akhirnya.
jaap
2
PATH=`perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}'`
export PATH

Ini menggunakan perl dan memiliki beberapa manfaat:

  1. Ini menghapus duplikat
  2. Itu terus memesan
  3. Itu membuat penampilan paling awal ( /usr/bin:/sbin:/usr/binakan menghasilkan /usr/bin:/sbin)
vol7ron
sumber
2

Juga sed(di sini menggunakan sedsintaksis GNU ) dapat melakukan pekerjaan:

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

yang ini hanya berfungsi dengan baik jika jalur pertama adalah .seperti dalam contoh dogbane.

Secara umum, Anda perlu menambahkan sperintah lain :

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

Ia bekerja bahkan pada konstruksi seperti itu:

$ 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
buru-buru
sumber
2

Seperti orang lain telah menunjukkan itu mungkin dalam satu baris menggunakan awk, sed, perl, zsh, atau bash, tergantung pada toleransi Anda untuk garis panjang dan keterbacaan. Inilah fungsi bash itu

  • menghapus duplikat
  • mempertahankan pesanan
  • memungkinkan spasi dalam nama direktori
  • memungkinkan Anda untuk menentukan pembatas (default ke ':')
  • dapat digunakan dengan variabel lain, bukan hanya PATH
  • bekerja di versi bash <4, penting jika Anda menggunakan OS X yang untuk masalah lisensi tidak mengirimkan versi bash 4

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

pemakaian

Untuk menghapus dups dari PATH

PATH=$(remove_dups "$PATH")
amn
sumber
1

Ini versi saya:

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

Pemakaian: path_no_dup "$PATH"

Output sampel:

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
sumber
1

Versi bash terbaru (> = 4) juga array asosiatif, yaitu Anda juga dapat menggunakan bash 'one liner' untuknya:

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)

dimana:

  • IFS mengubah pemisah bidang input ke :
  • declare -A mendeklarasikan array asosiatif
  • ${a[$i]+_}adalah arti perluasan parameter: _disubstitusi jika dan hanya jika a[$i]diatur. Ini mirip dengan ${parameter:+word}yang juga menguji tidak-nol. Jadi, dalam evaluasi kondisional berikut, ekspresi _(yaitu string karakter tunggal) mengevaluasi ke true (ini setara dengan -n _) - sedangkan ekspresi kosong mengevaluasi ke false.
maxschlepzig
sumber
+1: gaya skrip yang bagus, tetapi bisakah Anda menjelaskan sintaksis tertentu: ${a[$i]+_}dengan mengedit jawaban Anda dan menambahkan satu butir. Sisanya sangat bisa dimengerti tetapi Anda kehilangan saya di sana. Terima kasih.
Cbhihe
1
@ Cbhihe, saya telah menambahkan titik poin yang membahas ekspansi ini.
maxschlepzig
Terima kasih banyak. Sangat menarik. Saya tidak berpikir itu mungkin dengan array (non-string) ...
Cbhihe
1
PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`

Penjelasan kode awk:

  1. Pisahkan input dengan titik dua.
  2. Tambahkan entri jalur baru ke array asosiatif untuk pencarian duplikat cepat.
  3. Mencetak array asosiatif.

Selain singkat, one-liner ini cepat: awk menggunakan tabel hash chaining untuk mencapai kinerja O (1) yang diamortisasi.

berdasarkan pada Menghapus entri $ PATH duplikat

Leftium
sumber
Pos tua, tapi bisa Anda menjelaskan: if ( !x[$i]++ ). Terima kasih.
Cbhihe
0

Gunakan awkuntuk membelah jalur :, kemudian lewati setiap bidang dan simpan dalam larik. Jika Anda menemukan bidang yang sudah ada dalam array, itu berarti Anda telah melihatnya sebelumnya, jadi jangan cetak.

Berikut ini sebuah contoh:

$ 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

(Diperbarui untuk menghapus trailing :.)

dogbane
sumber
0

Sebuah solusi - bukan solusi yang elegan seperti yang mengubah variabel * RS, tapi mungkin cukup jelas:

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`

Seluruh program bekerja di blok BEGIN dan END . Ini menarik variabel PATH Anda dari lingkungan, membaginya menjadi beberapa unit. Ini kemudian beralih di atas p array yang dihasilkan (yang dibuat dalam urutan oleh split()). Array e adalah array asosiatif yang digunakan untuk menentukan apakah kita telah melihat elemen path saat ini (misalnya / usr / local / bin ) sebelumnya, dan jika tidak, ditambahkan ke np , dengan logika untuk menambahkan titik dua ke np jika sudah ada teks di np . The END blok hanya echos np . Ini dapat lebih disederhanakan dengan menambahkan-F:menandai, menghapus argumen ketiga ke split()(karena default ke FS ), dan mengubah np = np ":"ke np = np FS, memberi kita:

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

Naif, saya percaya itu for(element in array)akan menjaga ketertiban, tetapi tidak, jadi solusi asli saya tidak bekerja, karena orang-orang akan marah jika seseorang tiba-tiba mengacak urutan mereka $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
sumber
0
export PATH=$(echo -n "$PATH" | awk -v RS=':' '(!a[$0]++){if(b++)printf(RS);printf($0)}')

Hanya kejadian pertama yang dipertahankan dan keteraturan relatif dipertahankan.

Cyker
sumber
-1

Saya akan melakukannya hanya dengan alat dasar seperti tr, sortir dan uniq:

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

Jika tidak ada yang istimewa atau aneh di jalan Anda, itu harus bekerja

ghm1014
sumber
btw, kamu bisa menggunakan sort -ubukan sort | uniq.
buru
11
Karena urutan elemen PATH signifikan, ini tidak terlalu berguna.
maxschlepzig