awk 명령으로 중복 $ PATH 항목 제거


48

PATH 환경 변수에서 디렉토리의 복제본을 제거 할 수있는 bash 쉘 함수를 작성하려고합니다.

명령을 사용하여 한 줄 명령 으로이 작업을 수행 할 수 있다고 말 awk했지만 어떻게 수행 해야하는지 알 수 없습니다. 아무도 어떻게 알아?



답변:


37

에 중복 PATH이없고 디렉토리가없는 경우에만 디렉토리를 추가하려는 경우 쉘만으로 디렉토리를 쉽게 수행 할 수 있습니다.

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

그리고에서 중복을 제거하는 쉘 스 니펫이 있습니다 $PATH. 항목을 하나씩 통과하고 아직 보지 못한 항목을 복사합니다.

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의 항목을 반대로 반복하면 나중 항목이 일반적으로 새로 추가되고 최신 값을 가질 수 있으므로 더 좋습니다.
Eric Wang

2
@EricWang 나는 당신의 추론을 이해하지 못합니다. PATH 요소는 앞뒤로 이동하므로 중복이 있으면 두 번째 중복이 효과적으로 무시됩니다. 뒤에서 앞으로 반복하면 순서가 변경됩니다.
Gilles 'SO- 악마 그만'

@Gilles PATH에 변수를 복제했을 때 아마도 다음과 같은 방식으로 추가되었을 것입니다 : PATH=$PATH:x=b, 원래 PATH의 x는 값 a를 가질 수 있으므로 순서대로 반복하면 새로운 값은 무시되지만 역순으로 새로운 것은 무시됩니다 가치가 적용됩니다.
Eric Wang

4
@EricWang이 경우 추가 된 값은 영향을 미치지 않으므로 무시해야합니다. 거꾸로 가면 부가 가치를 앞당길 수 있습니다. 더해진 값이 이전에 가야한다면로 추가되었을 것 PATH=x:$PATH입니다.
Gilles 'SO- 악마 그만'

@Gilles 무언가를 추가 할 때 아직 추가되지 않았거나 이전 값을 재정의하려는 경우 새로 추가 된 변수를 표시해야합니다. 그리고 일반적으로 일반적으로 다음과 같이 추가됩니다 : PATH=$PATH:...not PATH=...:$PATH. 따라서 역순을 반복하는 것이 더 적절합니다. 당신이 방법도 효과가 있지만 사람들은 반대 방향으로 덧붙입니다.
Eric Wang

23

다음 은 모든 올바른 작업을 수행 하는 이해 하기 쉬운 단일 라이너 솔루션입니다. 중복을 제거하고 경로 순서를 유지하며 끝에 콜론을 추가하지 않습니다. 따라서 원본과 정확히 동일한 동작을 제공하는 중복 제거 된 PATH를 제공해야합니다.

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

단순히 콜론 ( split(/:/, $ENV{PATH}))으로 분할되고 grep { not $seen{$_}++ }, 첫 번째 발생을 제외하고 경로의 반복 된 인스턴스를 필터링하는 데 사용 하여 콜론으로 구분 된 나머지 경로를 다시 결합하여 결과를 인쇄합니다 ( print join(":", ...)).

더 많은 구조를 원하고 다른 변수를 중복 제거하는 기능을 원한다면 현재 내 구성에서 사용하고있는이 스 니펫을 사용해보십시오.

# 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

이 코드는 PATH와 MANPATH를 중복 제거하며 dedup_pathvar콜론으로 구분 된 경로 목록 (예 : PYTHONPATH)을 보유하는 다른 변수를 쉽게 호출 할 수 있습니다 .


어떤 이유로 나는 chomp후행 줄 바꿈을 제거하기 위해 를 추가해야했습니다 . 이것은 나를 위해 일했다 :perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
Håkon Hægland

12

매끄러운 것이 있습니다 :

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

더 길게 (작동 방식보기) :

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

좋아, 리눅스를 처음 사용하기 때문에, 뒤에 ":"없이 실제로 PATH를 설정하는 방법이있다

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

btw는 PATH에 ":"를 포함하는 디렉토리가 없어야합니다. 그렇지 않으면 엉망이됩니다.

약간의 크레딧 :


-1 작동하지 않습니다. 여전히 내 경로에 중복이 표시됩니다.
dogbane

4
@ dogbane : 그것은 나를 위해 중복을 제거합니다. 그러나 미묘한 문제가 있습니다. 출력에는 끝에 :가 있습니다. $ PATH로 설정하면 현재 디렉토리에 경로가 추가됩니다. 이는 다중 사용자 시스템에 보안 관련 사항이 있습니다.
camh

@ dogbane, 그것은 작동하고 후행없이 한 줄 명령을 갖도록 게시물을 편집했습니다.
akostadinov

@dogbane 솔루션에는 후행이 있습니다 : 출력
akostadinov

흠, 세 번째 명령은 작동하지만을 사용하지 않으면 첫 번째 명령은 작동하지 않습니다 echo -n. 귀하의 명령은 "여기에 문자열"로 작동하지 않는 예를 들어보십시오 :awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
개정 향풀

6

다음은 AWK 원 라이너입니다.

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

어디:

  • printf %s "$PATH"$PATH줄 바꿈없이 내용을 인쇄합니다.
  • RS=: 입력 레코드 구분 문자를 변경합니다 (기본값은 줄 바꿈).
  • ORS= 출력 레코드 분리 문자를 빈 문자열로 변경
  • a 내재적으로 작성된 배열의 이름
  • $0 현재 레코드를 참조
  • a[$0] 연관 배열 역 참조입니다.
  • ++ 증가 후 연산자입니다
  • !a[$0]++ 오른쪽을 보호합니다. 즉, 현재 레코드가 인쇄되지 않은 경우에만 인쇄되도록합니다.
  • NR 1로 시작하는 현재 레코드 번호

즉, AWK는 구분 문자를 PATH따라 내용 을 분할 :하고 순서를 수정하지 않고 중복 항목을 필터링하는 데 사용됩니다 .

AWK 연관 배열은 해시 테이블로 구현되므로 런타임은 선형 적입니다 (즉, O (n)).

: 변수 :에서 이름으로 디렉토리를 지원 하기 위해 인용 부호제공하지 않기 때문에 인용 문자를 찾을 필요 가 없습니다PATH .

Awk + 붙여 넣기

페이스트로 위를 단순화 할 수 있습니다.

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

paste명령은 awk 출력을 콜론으로 분산시키는 데 사용됩니다. 이것은 인쇄에 대한 awk 동작 (기본 동작)을 단순화합니다.

파이썬

파이썬 2 라이너와 동일 :

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

좋아, 그러나 이것은 기존 콜론으로 구분 된 문자열에서 듀피를 제거합니까, 아니면 듀피가 문자열에 추가되는 것을 방지합니까?
Alexander Mills

1
전처럼 보인다
Alexander Mills

2
@AlexanderMills, OP는 방금 중복 제거에 대해 물었으므로 awk 호출이 수행합니다.
maxschlepzig

1
paste난 뒤에 추가하지 않으면 명령은 나를 위해 작동하지 않습니다 -STDIN을 사용합니다.
wisbucky

2
또한 공백을 추가해야합니다 -v. 그렇지 않으면 오류가 발생합니다. -v RS=: -v ORS=. 다른 맛의 awk구문.
wisbucky

4

이것에 대해서도 비슷한 토론이 있었습니다 .

나는 조금 다른 접근법을 취합니다. 설치되는 다른 모든 초기화 파일에서 설정된 PATH를 그대로 사용하는 대신 getconf시스템 경로를 식별하고 먼저 배치 한 다음 선호하는 경로 순서를 추가 한 다음 awk중복을 제거하는 데 사용 하는 것이 좋습니다 . 이것은 실제로 명령 실행 속도를 높이거나 높이 지 않을 수도 있지만 이론적으로 더 안전합니다.

# 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
이것은 현재 작업 디렉토리가의 일부이기 때문에 (즉 빈 문자열 항목)에 후행 :을 추가하기 때문에 매우 위험 PATH합니다 PATH.
maxschlepzig

3

우리는 비 ak oneliners를 추가하는 한 :

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

(단순 PATH=$(zsh -fc 'typeset -U path; echo $PATH')하지만 zsh는 항상 하나 이상의 zshenv구성 파일을 읽습니다 PATH.이 파일은 수정할 수 있습니다 .)

두 가지 멋진 zsh 기능을 사용합니다.

  • 배열에 연결된 스칼라 ( typeset -T)
  • 중복 값을 자동 제거하는 배열 ( typeset -U).

좋은! 가장 짧은 작업 답변 및 기본적으로 끝에 콜론이 없습니다.
jaap

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

이것은 perl을 사용하며 몇 가지 이점이 있습니다.

  1. 중복을 제거합니다
  2. 정렬 순서를 유지합니다
  3. 그것은 초기 모양을 유지한다 ( /usr/bin:/sbin:/usr/bin발생합니다 /usr/bin:/sbin)

2

또한 sed(여기서는 GNU sed구문을 사용하여 ) 작업을 수행 할 수 있습니다.

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

이것은 첫 번째 경로가 .dogbane의 예와 같은 경우에만 잘 작동합니다 .

일반적으로 다른 s명령 을 추가해야합니다 .

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

그것은 그러한 구성에서도 작동합니다.

$ 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

다른 사람들이 awk, sed, perl, zsh 또는 bash를 사용하여 한 줄에서 가능하다는 것은 긴 줄과 가독성에 대한 내성에 달려 있습니다. 다음은 bash 함수입니다.

  • 중복을 제거합니다
  • 질서를 유지하다
  • 디렉토리 이름에 공백을 허용
  • 구분자를 지정할 수 있습니다 (기본값은 ':').
  • PATH뿐만 아니라 다른 변수와 함께 사용할 수 있습니다.
  • bash 버전 <4에서 작동합니다. 라이센싱 문제로 bash 버전 4를 제공하지 않는 OS X를 사용하는 경우 중요합니다.

배쉬 기능

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

용법

PATH에서 딥을 제거하려면

PATH=$(remove_dups "$PATH")

1

이것은 내 버전입니다.

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

용법: path_no_dup "$PATH"

샘플 출력 :

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

최근의 bash 버전 (> = 4)도 연관 배열입니다. 즉, 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)

어디:

  • IFS 입력 필드 구분자를 :
  • declare -A 연관 배열을 선언
  • ${a[$i]+_}매개 변수 확장 의미 입니다. 설정된 _경우에만 대체됩니다 a[$i]. 이것은 ${parameter:+word}null이 아닌지 테스트하는 것과 비슷합니다 . 따라서 다음 조건부 평가에서 표현식 _(예 : 단일 문자열)은 true로 평가되고 (이와 동일 함 -n _) 빈 표현식은 false로 평가됩니다.

+1 : 멋진 스크립트 스타일이지만 ${a[$i]+_}답변을 편집하고 글 머리 기호 하나를 추가 하여 특정 구문을 설명 할 수 있습니다 . 나머지는 완벽하게 이해할 수 있지만 당신은 저를 잃었습니다. 감사합니다.
Cbhihe

1
@Cbhihe, 나는이 확장을 다루는 글 머리 기호를 추가했습니다.
maxschlepzig

대단히 감사합니다. 매우 흥미로운. 배열 (비 문자열)에서 가능하다고 생각하지 않았습니다 ...
Cbhihe

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

awk 코드 설명 :

  1. 콜론으로 입력을 분리하십시오.
  2. 빠른 복제 조회를 위해 새 경로 항목을 연관 배열에 추가합니다.
  3. 연관 배열을 인쇄합니다.

간결한 것 외에도이 one-liner는 빠릅니다. awk는 연결 해시 테이블을 사용하여 상각 된 O (1) 성능을 얻습니다.

중복 $ PATH 항목 제거를 기반으로


이전 게시물이지만 설명 할 수 있습니다 if ( !x[$i]++ ).. 감사.
Cbhihe

0

사용 awk에 경로를 분할 :각 필드 위에 다음 루프와 배열에 저장합니다. 이미 배열에있는 필드를 발견했다면, 전에 본 적이 있으므로 인쇄하지 마십시오.

예를 들면 다음과 같습니다.

$ 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

(후행을 제거하도록 업데이트되었습니다 :.)


0

해결책-* RS 변수를 변경하는 것만 큼 우아하지는 않지만 합리적으로 명확한 솔루션 :

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`

전체 프로그램은 BEGINEND 블록 에서 작동합니다 . 환경에서 PATH 변수를 가져 와서 단위로 나눕니다. 그런 다음 결과 배열 p 를 반복합니다 (순서대로 생성됨 split()). 배열 e 는 현재 경로 요소 (예 : / usr / local / bin )를 보았는지 여부를 결정하는 데 사용되는 연관 배열 이며, 그렇지 않은 경우 콜론을 추가하는 로직과 함께 np 에 추가됩니다. np 에 이미 텍스트가 있으면 np . 최종 블록은 단순히 메아리 순이익 . 이것은 추가하여 단순화 할 수 있습니다-F:플래그, 세 번째 인수를 생략하고 split()(기본값은 FS )로 변경 np = np ":"하여 다음 np = np FS을 제공합니다.

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

순진하게, 나는 for(element in array)그것이 질서를 유지 한다고 믿었 지만, 그렇지 않다. 그래서 누군가가 갑자기 그들의 질서를 뒤섞 으면 사람들이 화를 내기 때문에 내 원래의 해결책은 효과가 없다 $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)}')

첫 번째 발생 만 유지되고 상대 순서가 잘 유지됩니다.


-1

tr, sort 및 uniq와 같은 기본 도구를 사용하여 수행합니다.

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

경로에 특별하거나 이상한 것이 없다면 작동해야합니다.


btw sort -u대신을 사용할 수 있습니다 sort | uniq.
rush

11
PATH 요소의 순서가 중요하기 때문에 이것은 유용하지 않습니다.
maxschlepzig
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.