Xóa các mục nhập $ PATH trùng lặp bằng lệnh awk


48

Tôi đang cố gắng viết một hàm bash shell cho phép tôi loại bỏ các bản sao trùng lặp của các thư mục khỏi biến môi trường PATH của tôi.

Tôi được cho biết rằng có thể đạt được điều này bằng lệnh một dòng bằng cách sử dụng awklệnh, nhưng tôi không thể tìm ra cách thực hiện. Có ai biết làm thế nào không?



Câu trả lời:


37

Nếu bạn chưa có bản sao trong PATHvà bạn chỉ muốn thêm thư mục nếu chúng chưa có ở đó, bạn có thể làm điều đó một cách dễ dàng chỉ với trình bao.

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

Và đây là một đoạn vỏ để loại bỏ các bản sao $PATH. Nó đi qua từng mục một và sao chép những mục chưa được nhìn thấy.

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

Sẽ tốt hơn, nếu lặp lại các mục trong $ PATH, bởi vì các mục sau thường được thêm mới và chúng có thể có giá trị cập nhật.
Eric Wang

2
@EricWang Tôi không hiểu lý do của bạn. Các phần tử PATH được dịch chuyển từ trước ra sau, vì vậy khi có các bản sao, bản sao thứ hai được bỏ qua một cách hiệu quả. Lặp lại từ sau ra trước sẽ thay đổi thứ tự.
Gilles 'SO- ngừng trở nên xấu xa'

@Gilles Khi bạn đã nhân đôi biến trong PATH, có thể nó được thêm vào theo cách này : PATH=$PATH:x=b, x trong PATH gốc có thể có giá trị a, do đó khi lặp theo thứ tự, thì giá trị mới sẽ bị bỏ qua, nhưng khi theo thứ tự đảo ngược, giá trị mới giá trị sẽ có hiệu lực.
Eric Wang

4
@EricWang Trong trường hợp đó, giá trị gia tăng không có hiệu lực nên được bỏ qua. Bằng cách đi ngược lại, bạn đang làm cho giá trị gia tăng đến trước. Nếu giá trị gia tăng được cho là đi trước, thì nó sẽ được thêm vào dưới dạng PATH=x:$PATH.
Gilles 'SO- ngừng trở nên xấu xa'

@Gilles Khi bạn nối thêm một cái gì đó, điều đó có nghĩa là nó chưa có hoặc bạn muốn ghi đè giá trị cũ, vì vậy bạn cần hiển thị biến mới được thêm vào. Và, theo quy ước, thông thường nó sẽ nối theo cách này: PATH=$PATH:...không PATH=...:$PATH. Vì vậy, nó đúng hơn để lặp lại trật tự đảo ngược. Mặc dù cách của bạn cũng sẽ hoạt động, sau đó mọi người nối lại theo cách ngược lại.
Eric Wang

23

Dưới đây là một dễ hiểu giải pháp một lót mà làm tất cả những điều đúng đắn: loại bỏ bản sao, giữ gìn trật tự của con đường, và không thêm dấu hai chấm ở cuối. Vì vậy, nó sẽ cung cấp cho bạn một PATH trùng lặp cung cấp chính xác hành vi giống như bản gốc:

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

Nó chỉ đơn giản là phân tách trên dấu hai chấm ( split(/:/, $ENV{PATH})), sử dụng grep { not $seen{$_}++ }để sử dụng để lọc bất kỳ trường hợp lặp lại nào của các đường dẫn ngoại trừ lần xuất hiện đầu tiên và sau đó nối các phần còn lại lại với nhau bằng dấu hai chấm và in kết quả ( print join(":", ...)).

Nếu bạn muốn có thêm một số cấu trúc xung quanh nó, cũng như khả năng sao chép các biến khác, hãy thử đoạn mã này, hiện tôi đang sử dụng trong cấu hình của riêng tôi:

# 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

Mã đó sẽ lặp lại cả PATH và MANPATH và bạn có thể dễ dàng gọi dedup_pathvarcác biến khác chứa danh sách các đường dẫn được phân tách bằng dấu hai chấm (ví dụ PYTHONPATH).


Vì một số lý do, tôi đã phải thêm một chompđể xóa một dòng mới. Điều này làm việc cho tôi:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
Håkon Hægland

12

Đây là một kiểu dáng đẹp:

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

Dài hơn (để xem cách nó hoạt động):

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

Ok, vì bạn chưa quen với linux, đây là cách thực sự thiết lập PATH mà không có dấu ":"

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

btw đảm bảo KHÔNG có các thư mục chứa ":" trong PATH của bạn, nếu không nó sẽ bị rối tung.

một số tín dụng cho:


-1 cái này không hoạt động. Tôi vẫn thấy các bản sao trong con đường của tôi.
dogbane

4
@dogbane: Nó loại bỏ các bản sao cho tôi. Tuy nhiên nó có một vấn đề tinh tế. Đầu ra có một: ở cuối mà nếu được đặt là $ PATH của bạn, có nghĩa là thư mục hiện tại được thêm đường dẫn. Điều này có ý nghĩa bảo mật trên một máy nhiều người dùng.
camh

@dogbane, nó hoạt động và tôi đã chỉnh sửa bài đăng để có lệnh một dòng mà không cần theo dõi:
akostadinov

@dogbane giải pháp của bạn có một dấu vết: ở đầu ra
akostadinov

hmm, lệnh thứ ba của bạn hoạt động, nhưng hai lệnh đầu tiên không hoạt động trừ khi tôi sử dụng echo -n. Các lệnh của bạn dường như không hoạt động với "chuỗi ở đây", ví dụ: thử:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
dogbane

6

Đây là một lót AWK.

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

Ở đâu:

  • printf %s "$PATH"in nội dung $PATHmà không có một dòng mới
  • RS=: thay đổi ký tự phân cách bản ghi đầu vào (mặc định là dòng mới)
  • ORS= thay đổi dấu phân cách bản ghi đầu ra thành chuỗi trống
  • a tên của một mảng được tạo ngầm
  • $0 tham chiếu hồ sơ hiện tại
  • a[$0] là một sự kết hợp mảng
  • ++ là toán tử tăng sau
  • !a[$0]++ bảo vệ phía bên tay phải, tức là đảm bảo rằng bản ghi hiện tại chỉ được in, nếu nó không được in trước đó
  • NR số hồ sơ hiện tại, bắt đầu bằng 1

Điều đó có nghĩa là AWK được sử dụng để phân chia PATHnội dung dọc theo các :ký tự phân cách và để lọc ra các mục trùng lặp mà không sửa đổi thứ tự.

Vì các mảng kết hợp AWK được triển khai dưới dạng bảng băm, thời gian chạy là tuyến tính (tức là trong O (n)).

Lưu ý rằng chúng ta không cần tìm các :ký tự được trích dẫn bởi vì shell không cung cấp trích dẫn cho các thư mục hỗ trợ có :tên của nó trong PATHbiến.

Awk + dán

Ở trên có thể được đơn giản hóa bằng dán:

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

Các pastelệnh được sử dụng để nén ra đầu ra awk với dấu hai chấm. Điều này đơn giản hóa hành động awk để in (đó là hành động mặc định).

Con trăn

Giống như Python hai lớp:

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

ok, nhưng điều này có loại bỏ các bản sao ra khỏi một chuỗi được phân cách bằng dấu hai chấm hiện có không, hoặc nó có ngăn các bản sao được thêm vào một chuỗi không?
Alexander Mills

1
trông giống như trước đây
Alexander Mills

2
@AlexanderMills, tốt, OP chỉ hỏi về việc loại bỏ các bản sao, vì vậy đây là những gì cuộc gọi awk làm.
maxschlepzig

1
Các pastelệnh không làm việc cho tôi, trừ khi tôi thêm một dấu -để sử dụng STDIN.
wvducky

2
Ngoài ra, tôi cần thêm khoảng trắng sau khi -vtôi gặp lỗi. -v RS=: -v ORS=. Chỉ cần hương vị khác nhau của awkcú pháp.
wvducky

4

Đã có một cuộc thảo luận tương tự về điều này ở đây .

Tôi có một chút của một cách tiếp cận khác nhau. Thay vì chỉ chấp nhận PATH được đặt từ tất cả các tệp khởi tạo khác nhau được cài đặt, tôi thích sử dụng getconfđể xác định đường dẫn hệ thống và đặt nó trước, sau đó thêm thứ tự đường dẫn ưa thích của tôi, sau đó sử dụng awkđể xóa bất kỳ bản sao nào. Điều này có thể hoặc không thực sự tăng tốc độ thực thi lệnh (và trên lý thuyết là an toàn hơn), nhưng nó mang lại cho tôi những vũng lầy ấm áp.

# 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
Điều này rất nguy hiểm vì bạn thêm một dấu :vào PATH(ví dụ một mục nhập chuỗi trống), vì khi đó thư mục làm việc hiện tại là một phần của bạn PATH.
maxschlepzig

3

Miễn là chúng tôi đang thêm oneliners không awk:

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

(Có thể đơn giản như PATH=$(zsh -fc 'typeset -U path; echo $PATH')nhưng zsh luôn đọc ít nhất một zshenvtệp cấu hình, có thể sửa đổi PATH.)

Nó sử dụng hai tính năng zsh đẹp:

  • vô hướng gắn với mảng ( typeset -T)
  • và các mảng tự động chuyển các giá trị trùng lặp ( typeset -U).

đẹp! câu trả lời làm việc ngắn nhất, và tự nhiên không có dấu hai chấm ở cuối.
jaap

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

Điều này sử dụng perl và có một số lợi ích:

  1. Nó loại bỏ trùng lặp
  2. Nó giữ trật tự sắp xếp
  3. Nó giữ sự xuất hiện sớm nhất ( /usr/bin:/sbin:/usr/binsẽ dẫn đến /usr/bin:/sbin)

2

Ngoài ra sed(ở đây sử dụng sedcú pháp GNU ) có thể thực hiện công việc:

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

cái này chỉ hoạt động tốt trong trường hợp đường dẫn đầu tiên .giống như trong ví dụ của dogbane.

Trong trường hợp chung, bạn cần thêm một slệnh khác :

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

Nó hoạt động ngay cả trên xây dựng như vậy:

$ 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

Như những người khác đã chứng minh rằng có thể trong một dòng sử dụng awk, sed, perl, zsh hoặc bash, tùy thuộc vào khả năng chịu đựng của bạn đối với các dòng dài và dễ đọc. Đây là một hàm bash

  • loại bỏ trùng lặp
  • giữ gìn trật tự
  • cho phép khoảng trắng trong tên thư mục
  • cho phép bạn chỉ định dấu phân cách (mặc định là ':')
  • có thể được sử dụng với các biến khác, không chỉ PATH
  • hoạt động trong các phiên bản bash <4, quan trọng nếu bạn sử dụng OS X, vấn đề cấp phép không xuất xưởng bash phiên bản 4

chức năng 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}"
}

sử dụng

Để loại bỏ dups từ PATH

PATH=$(remove_dups "$PATH")

1

Đây là phiên bản của tôi:

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

Sử dụng: path_no_dup "$PATH"

Đầu ra mẫu:

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

Các phiên bản bash gần đây (> = 4) cũng có các mảng kết hợp, tức là bạn cũng có thể sử dụng một bash 'one liner' cho nó:

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)

Ở đâu:

  • IFS thay đổi dấu phân cách trường đầu vào thành :
  • declare -A tuyên bố một mảng kết hợp
  • ${a[$i]+_}là một ý nghĩa mở rộng tham số: _được thay thế khi và chỉ khi a[$i]được đặt. Điều này tương tự như ${parameter:+word}cũng kiểm tra không-null. Do đó, trong đánh giá sau đây về điều kiện, biểu thức _(tức là một chuỗi ký tự đơn) ước tính là đúng (điều này tương đương với -n _) - trong khi một biểu thức trống đánh giá là sai.

+1: kiểu tập lệnh đẹp, nhưng bạn có thể giải thích cú pháp cụ thể: ${a[$i]+_}bằng cách chỉnh sửa câu trả lời của bạn và thêm một dấu đầu dòng. Phần còn lại là hoàn toàn dễ hiểu nhưng bạn đã mất tôi ở đó. Cảm ơn bạn.
Cbhihe

1
@Cbhihe, tôi đã thêm một gạch đầu dòng giải quyết việc mở rộng này.
maxschlepzig

Cảm ơn rât nhiều. Rất thú vị. Tôi không nghĩ điều đó là có thể với mảng (không phải chuỗi) ...
Cbhihe

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

Giải thích về mã awk:

  1. Phân tách đầu vào bằng dấu hai chấm.
  2. Nối các mục đường dẫn mới vào mảng kết hợp để tra cứu trùng lặp nhanh.
  3. In mảng kết hợp.

Ngoài việc ngắn gọn, lớp lót này còn nhanh: awk sử dụng bảng băm chuỗi để đạt được hiệu suất O (1) được khấu hao.

dựa trên việc xóa các mục nhập $ PATH trùng lặp


Bài cũ, nhưng bạn có thể giải thích : if ( !x[$i]++ ). Cảm ơn.
Cbhihe

0

Sử dụng awkđể phân chia đường dẫn trên :, sau đó lặp qua từng trường và lưu trữ nó trong một mảng. Nếu bạn gặp một trường đã có trong mảng, điều đó có nghĩa là bạn đã thấy nó trước đó, vì vậy đừng in nó.

Đây là một ví dụ:

$ 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

(Cập nhật để xóa dấu vết :.)


0

Một giải pháp - không phải là một giải pháp thanh lịch như những giải pháp thay đổi các biến * RS, nhưng có lẽ rõ ràng hợp lý:

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`

Toàn bộ chương trình hoạt động trong các khối BEGINEND . Nó kéo biến PATH của bạn khỏi môi trường, chia nó thành các đơn vị. Sau đó, nó lặp lại trên mảng kết quả p (được tạo theo thứ tự bởi split()). Mảng e là một mảng kết hợp được sử dụng để xác định xem chúng ta đã thấy phần tử đường dẫn hiện tại (ví dụ / usr / local / bin ) hay chưa, và nếu không, sẽ được thêm vào np , với logic để nối thêm dấu hai chấm vào np nếu đã có văn bản trong np . Khối END chỉ đơn giản là echos np . Điều này có thể được đơn giản hóa hơn nữa bằng cách thêm-F:gắn cờ, loại bỏ đối số thứ ba thành split()(vì nó mặc định là FS ) và thay đổi np = np ":"thành np = np FS, cho chúng ta:

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

Ngây thơ, tôi tin rằng for(element in array)sẽ giữ trật tự, nhưng không, vì vậy giải pháp ban đầu của tôi không hiệu quả, vì mọi người sẽ khó chịu nếu ai đó đột nhiên xáo trộn trật tự của họ $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)}')

Chỉ có sự xuất hiện đầu tiên được giữ và trật tự tương đối được duy trì tốt.


-1

Tôi sẽ làm điều đó chỉ với các công cụ cơ bản như tr, sort và uniq:

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

Nếu không có gì đặc biệt hoặc kỳ lạ trong con đường của bạn, nó sẽ hoạt động


btw, bạn có thể sử dụng sort -uthay vì sort | uniq.
vội vàng

11
Vì thứ tự của các phần tử PATH rất quan trọng, nên điều này không hữu ích lắm.
maxschlepzig
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.