$ PATH에 어떻게 깔끔하게 추가 할 수 있습니까?


31

동일한 경로를 여러 번 추가하지 않고도 시스템 전체 또는 개별 사용자를 위해 $ PATH에 항목을 추가하는 방법을 원합니다.

이 작업을 수행 .bashrc해야하는 한 가지 이유 는 로그인이 필요없는 추가 기능을 제공 할 수 있기 때문에 lightdm호출하지 않는 (예)를 사용하는 시스템에서 더 유용 합니다 .profile.

$ PATH에서 복제본을 정리하는 방법 에 대한 질문을 알고 있지만 중복을 제거하고 싶지 않습니다 . 경로가 아직없는 경우에만 경로추가 하는 방법을 원합니다 .



골디, 왜 그런지 모르겠지만 공허함에서도 첫 번째 의견을 보았습니다. 그러나 그렇습니다. 이름 접두사도 작동합니다. 걱정하지 마십시오! 다른 방법으로 닫는 것도 좋습니다.
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件

그래, 내 메시지를받는 한 때로는 이와 같은 반전을 수행하면 약간의 혼란이 생길 ​​수 있습니다.
goldilocks

답변:


35

추가하려는 새 경로가 다음과 같다고 가정하십시오.

new=/opt/bin

그런 다음 POSIX 셸을 사용 new하여 경로에 이미 있는지 확인하고 그렇지 않은 경우 추가 할 수 있습니다.

case ":${PATH:=$new}:" in
    *:"$new":*)  ;;
    *) PATH="$new:$PATH"  ;;
esac

콜론 사용에 유의하십시오. 콜론이 없으면 new=/bin패턴이에 일치하여 이미 경로에 있다고 생각할 수 있습니다 /usr/bin. PATH에는 일반적으로 많은 요소가 있지만 PATH에서 0과 1 개의 특수한 경우도 처리됩니다. 경로가 초기에 요소 (빈 인)도없는 경우를 이용하여 처리의 ${PATH:=$new}어느 양수인 PATH에게 $new비어 있으면. 이러한 방식으로 매개 변수의 기본값을 설정하는 것은 모든 POSIX 쉘의 기능입니다 ( POSIX docs의 2.6.2 절 참조 ).

호출 가능한 함수

편의상 위의 코드를 함수에 넣을 수 있습니다. 이 함수는 명령 행에서 정의하거나 영구적으로 사용 가능하도록 쉘의 초기화 스크립트에 넣을 수 있습니다 (bash 사용자의 경우 ~/.bashrc).

pupdate() { case ":${PATH:=$1}:" in *:"$1":*) ;; *) PATH="$1:$PATH" ;; esac; }

이 경로 업데이트 기능을 사용하여 현재 PATH에 디렉토리를 추가하려면 다음을 수행하십시오.

pupdate /new/path

@hammar OK. 나는 그 경우를 추가했다.
John1024

1
두 가지 경우를 구별 할 수 있습니다 (cf. unix.stackexchange.com/a/40973/1131 .
maxschlepzig

3
PATH비어 있으면 빈 항목 (즉, 현재 디렉토리)이에 추가됩니다 PATH. 다른 사건이 필요하다고 생각합니다.
CB Bailey

2
@CharlesBailey 다른 사람이 아닙니다 case. 그냥하세요 case "${PATH:=$new}". 비슷한 폴백에 대한 내 대답을 참조하십시오.
mikeserv

1
@ mc0e 쉘 함수를 사용하여 "라인 노이즈"를 숨기는 방법의 예를 추가했습니다.
John1024

9

/etc/profile.d예를 들어, mypath.sh또는 원하는대로 파일을 만듭니다 . lightdm을 사용하는 경우 실행 가능한지 또는 다른 곳 /etc/bashrc에서 가져온 파일인지 또는 파일을 사용해야 합니다. 다음 기능을 추가하십시오.

checkPath () {
        case ":$PATH:" in
                *":$1:"*) return 1
                        ;;
        esac
        return 0;
}

# Prepend to $PATH
prependToPath () {
        for a; do
                checkPath $a
                if [ $? -eq 0 ]; then
                        PATH=$a:$PATH
                fi
        done
        export PATH
}

# Append to $PATH
appendToPath () {
        for a; do
                checkPath $a
                if [ $? -eq 0 ]; then
                        PATH=$PATH:$a
                fi
        done
        export PATH
}

$ PATH의 시작 부분 (앞에 붙는 것)은 다음에 오는 것보다 우선하며, 반대로 끝 부분에있는 것 (앞에있는 것)은 앞에 오는 것보다 우선합니다. 이는 $ PATH가 /usr/local/bin:/usr/bin있고 gotcha두 디렉토리 모두에 실행 파일 /usr/local/bin이있는 경우 기본적으로 하나의 디렉토리 가 사용됨을 의미합니다.

이제 동일한 파일, 다른 쉘 구성 파일 또는 명령 줄에서 다음을 사용할 수 있습니다.

appendToPath /some/path /another/path
prependToPath /some/path /yet/another/path

이것이 .bashrc 새 셸을 시작할 때 값이 두 번 이상 나타나지 않습니다. 앞에 추가 된 (예 : $ PATH 내에서 경로 이동) 또는 그 반대로 추가하려는 경우 직접 수행해야한다는 제한이 있습니다.


분할 $PATH로하면 IFS=:보다 궁극적으로보다 유연하다 case.
mikeserv

@mikeserv 의심의 여지가 없습니다. 이것은 caseIMO에 대한 일종의 핵 사용입니다 . 나는 awk여기에서도 잘 사용될 수 있다고 생각 합니다.
goldilocks

그건 좋은 지적이야. 그리고 내가 생각하는 것처럼 gawk직접 할당 할 수 있습니다 $PATH.
mikeserv

5

이 방법으로 할 수 있습니다 :

echo $PATH | grep /my/bin >/dev/null || PATH=$PATH:/my/bin

참고 : 다른 변수에서 PATH를 빌드하는 경우 많은 쉘이 ""를 "와 같이 해석하기 때문에 비어 있지 않은지 확인하십시오." .


+1 매뉴얼 페이지에 따르면 -qgrep에는 POSIX가 필요하지만 POSIX가 아닌 greps가 아직 없는지 모르겠습니다.
goldilocks

1
grep 패턴이 지나치게 넓습니다. grep / my / bin> / dev / null 대신 egrep -q "(^ | :) / my / bin (: | \ $)"를 사용하십시오. 수정하면 솔루션이 정확하며 이것이 @ john1024의 현재 선호하는 답변보다 더 읽기 쉬운 솔루션이라고 생각합니다. 큰 따옴표를 사용 /my/bin
했으므로

5

코드의 중요한 부분은 PATH특정 경로가 포함되어 있는지 확인하는 것입니다 .

printf '%s' ":${PATH}:" | grep -Fq ":${my_path}:"

즉, 각 경로 확인 PATH에 구분되는 에 의해 측 PATH세퍼레이터 ( :) 다음 확인 ( -q여부) 리터럴 스트링 ( -F(A)의 구성됨) PATH세퍼레이터 경로, 다른 PATH구분이 존재한다. 그렇지 않은 경우 경로를 안전하게 추가 할 수 있습니다.

if ! printf '%s' ":${PATH-}:" | grep -Fq ":${my_path-}:"
then
    PATH="${PATH-}:${my_path-}"
fi

이것은해야 POSIX 호환 및 모든 경로가 개행 문자를 포함하지 않는로 작동합니다. POSIX와 호환되는 동안 줄 바꿈이 포함 된 경로로 작업하려면 더 복잡하지만 grep지원하는 -z것이 있으면 사용할 수 있습니다.


4

나는 ~/.profile몇 년 동안 이 작은 기능을 다양한 파일로 가지고 다녔습니다 . 나는 그것이 일했던 실험실에서 sysadmin에 의해 쓰여졌다 고 생각 하지만 확실하지 않습니다. 어쨌든 Goldilock의 접근 방식과 비슷하지만 약간 다릅니다.

pathmunge () {
        if ! echo $PATH | /bin/grep -Eq "(^|:)$1($|:)" ; then
           if [ "$2" = "after" ] ; then
              PATH=$PATH:$1
           else
              PATH=$1:$PATH
           fi
        fi
}

따라서 시작 부분에 새 디렉토리를 추가하려면 PATH다음 을 수행하십시오 .

pathmunge /new/path

그리고 끝으로 :

pathmunge /new/path after

이것은 나를 위해 작동합니다! 그러나 기본적으로 논리를 바꾸고 "before"로 재정의했습니다. :)
Kevin Pauli

pathmunge는 Linux centos distribution / etc / profile의 일부이며, 전후 매개 변수가 있습니다. 최신 우분투 16 장에서는 볼 수 없습니다.
Kemin Zhou

macOS 10.12에서 정상적으로 작동하는 것 같습니다 /bin/grep->grep
Ben Creasy

4

최신 정보:

귀하의 답변에에 추가하거나 추가하기위한 각각의 기능이 있음을 확인했습니다 $PATH. 나는 그 아이디어를 좋아했다. 그래서 작은 인수 처리를 추가했습니다. 또한 _네임 스페이스 를 올바르게 지정 했습니다.

_path_assign() { oFS=$IFS ; IFS=: ; add=$* ; unset P A ; A=
    set -- ${PATH:=$1} ; for p in $add ; do {
        [ -z "${p%-[AP]}" ] && { unset P A
                eval ${p#-}= ; continue ; }
        for d ; do [ -z "${d%"$p"}" ] && break
        done ; } || set -- ${P+$p} $* ${A+$p}
        done ; export PATH="$*" ; IFS=$oFS
}

% PATH=/usr/bin:/usr/yes/bin
% _path_assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/bin/nope \
    -P \
    /usr/nope/bin \
    /usr/bin \
    -A \
    /nope/usr/bin \
    /usr/nope/bin

% echo $PATH

산출:

/usr/nope/bin:/usr/bin:/usr/yes/bin:/usr/bin/nope:/nope/usr/bin

기본적 -A으로은 ( 는) 보류 $PATH되지만 인수 목록의 아무 곳에 나 -P추가 하여 보류 하도록이 동작을 변경할 수 있습니다 -P. 당신이 그것을 -A건네 주면 다시 보류로 전환 할 수 있습니다-A다시 다시 .

안전 평가

대부분의 경우 사람들이의 사용을 피하는 것이 좋습니다 eval. 그러나 이것은 좋은 사용의 예라고 생각 합니다. 이 경우 유일한 문은 eval 지금까지 수 IS 참조 P=또는 A=. 인수 값은 호출되기 전에 엄격하게 테스트됩니다. 이것은 무엇 eval 을위한 것입니다.

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- ${PATH:=$1} ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH="$*" ; IFS=$oFS
}

이것은 당신이 부여한만큼 많은 인수를 받아들이고 $PATH이미 존재하지 않는 경우에 한 번만 추가합니다 $PATH. 완전히 이식 가능한 POSIX 셸 스크립트 만 사용하고 셸 내장에만 의존하며 매우 빠릅니다.

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

@ TAFKA'goldilocks '여기 업데이트를 참조하십시오-당신은 저에게 영감을주었습니다.
mikeserv

+1 궁금한 점이있을 경우 (별도의 Q & A 일 수 있음), _셸 함수 앞에 접두사가 "적절하게 네임 스페이스 화" 되어 있다는 아이디어는 어디에서 왔습니까? 다른 언어에서는 일반적으로 내부 전역 기능 (즉, 전역 적이어야하지만 API의 일부로 외부 적으로 사용되지 않는 기능)을 나타냅니다. 내 이름은 확실히 좋은 선택은 아니지만 사용하면 _충돌 문제를 전혀 해결하지 못하는 것 같습니다 . 예를 들어 실제 네임 스페이스를 사용하는 것이 좋습니다. mikeserv_path_assign().
goldilocks

@ TAFKA'goldilocks '-더 구체적으로 작성하는 것이 좋지만 이름이 길수록 사용 편의성이 떨어집니다. 그러나 접두사가있는 적절한 실행 바이너리 _가 있으면 패키지 관리자를 전환해야합니다. 어쨌든 이것은 본질적으로 단지 "내부, 내부, 함수" 일뿐입니다. 선언 된 셸에서 호출 된 모든 셸에 대해 전역 적이며 인터프리터의 메모리에 걸려있는 약간의 해석 된 언어 스크립트입니다. . unix.stackexchange.com/questions/120528/…
mikeserv

unset a프로필 끝에서 (또는 동등한) 할 수 없습니까 ?
sourcejedi

0

보다! 산업적으로 강력한 12 줄 ... 기술적으로 bash 및 zsh-portable 쉘 기능 은 선택의 시작 스크립트 ~/.bashrc또는 ~/.zshrc시작 스크립트 를 전적으로 사랑합니다 .

# void +path.append(str dirname, ...)
#
# Append each passed existing directory to the current user's ${PATH} in a
# safe manner silently ignoring:
#
# * Relative directories (i.e., *NOT* prefixed by the directory separator).
# * Duplicate directories (i.e., already listed in the current ${PATH}).
# * Nonextant directories.
+path.append() {
    # For each passed dirname...
    local dirname
    for   dirname; do
        # Strip the trailing directory separator if any from this dirname,
        # reducing this dirname to the canonical form expected by the
        # test for uniqueness performed below.
        dirname="${dirname%/}"

        # If this dirname is either relative, duplicate, or nonextant, then
        # silently ignore this dirname and continue to the next. Note that the
        # extancy test is the least performant test and hence deferred.
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname}:"* &&
           -d "${dirname}" ]] || continue

        # Else, this is an existing absolute unique dirname. In this case,
        # append this dirname to the current ${PATH}.
        PATH="${PATH}:${dirname}"
    done

    # Strip an erroneously leading delimiter from the current ${PATH} if any,
    # a common edge case when the initial ${PATH} is the empty string.
    PATH="${PATH#:}"

    # Export the current ${PATH} to subprocesses. Although system-wide scripts
    # already export the ${PATH} by default on most systems, "Bother free is
    # the way to be."
    export PATH
}

순간적인 영광을 위해 자신을 준비하십시오. 그런 다음이 작업을 수행하고 최선을 다하기를 바랍니다.

export PATH=$PATH:~/opt/bin:~/the/black/goat/of/the/woods/with/a/thousand/young

당신이 정말로 원하든 원하지 않든, 대신 이것을하고 최선을 다할 것을 보장하십시오 :

+path.append ~/opt/bin ~/the/black/goat/of/the/woods/with/a/thousand/young

"최고"를 정의하십시오.

전류를 안전하게 추가하고 추가 ${PATH}하는 것은 일반적으로 이루어지는 사소한 일이 아닙니다. 편리하고 겉으로는 합리적이지만 양식의 한 라이너는 다음 export PATH=$PATH:~/opt/bin과 같은 끔찍한 합병증을 유발합니다.

  • 실수로 상대 dirnames (예 export PATH=$PATH:opt/bin). 동안 bashzsh자동으로 받아들이고 주로 상대 dirnames 무시 대부분의 경우를, 상대 dirnames 중 하나를 접두어 h또는 t부끄럽게 모두에 원인 (그리고 아마도 다른 사악한 자) 마사키 고바야시의 람 자신을 토막 정액 1,962 걸작 Harakiri :

    # Don't try this at home. You will feel great pain.
    $ PATH='/usr/local/bin:/usr/bin:/bin' && export PATH=$PATH:harakiri && echo $PATH
    /usr/local/bin:/usr/bin:arakiri
    $ PATH='/usr/local/bin:/usr/bin:/bin' && export PATH=$PATH:tanuki/yokai && echo $PATH
    binanuki/yokai   # Congratulations. Your system is now face-up in the gutter.
  • 실수로 중복 된 디렉토리 이름. 중복 ${PATH}디렉토리 이름은 크게 무해 하지만 , 원치 않거나 번거롭고 약간 비효율적이며 디버깅 가능성을 방해하며 드라이브 마모를 촉진합니다. NAND 스타일 SSD는 물론 마모에 영향을받지 않지만 HDD는 그렇지 않습니다. 모든 시도 된 명령 에 대한 불필요한 파일 시스템 액세스 는 동일한 템포에서 불필요한 읽기 헤드 마모를 의미합니다. 중복은 중첩 된 서브 프로세스에서 중첩 된 쉘을 호출 할 때 특히 어색하지 않습니다.이 시점에서 무해한 1- 라이너 export PATH=$PATH:~/wat는 7 번째 ${PATH}지옥의 원으로 빠르게 폭발합니다 PATH=/usr/local/bin:/usr/bin:/bin:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat:/home/leycec/wat. 당신이 그것에 추가 디렉토리 이름을 추가하면 Beelzebubba만이 당신을 도울 수 있습니다. (소중한 아이들에게 이런 일이 일어나지 않도록하십시오. )

  • 실수로 dirname이 누락되었습니다. 다시 말해 누락 된 ${PATH}dirname은 무해하지만, 일반적으로 원치 않거나 번거롭고 약간 비효율적이며 디버깅 가능성을 방해하고 드라이브 마모를 촉진합니다.

Ergo, 위에서 정의한 쉘 기능과 같은 친숙한 자동화. 우리는 자신을 구해야합니다.

하지만 ... 왜 "+ path.append ()"? 왜 append_path ()가 아닌가?

(현재 외부 명령을 예를 들어, disambiguity를 들어 ${PATH}다른 곳에서 정의 또는 시스템 전체 쉘 기능), 사용자 정의 쉘 기능이 이상적으로 접두사 또는 고유 지원 하위 접미사 bashzsh, 말, 같은 -하지만 그렇지 않으면 표준 명령의 basename 금지 +.

안녕 작동합니다. 나를 판단하지 마십시오.

하지만 ... 왜 "+ path.append ()"? 왜 "+ path.prepend ()"가 아닙니까?

current에 추가하는 것이 current ${PATH}앞에 추가하는 것보다 안전 하기 때문에 ${PATH}모든 것이 동일하며 결코 존재하지 않습니다. 사용자 별 명령을 사용하여 시스템 전체 명령을 재정의하는 것은 최선이 아니고 최악의 경우 미친 짓을 할 수 있습니다. 예를 들어 Linux에서 다운 스트림 응용 프로그램은 일반적으로 사용자 지정 비표준 파생 제품이나 대안이 아닌 GNU coreutils 명령 변형을 기대합니다 .

즉, 그렇게하는 데 유효한 유스 케이스가 절대적으로 있습니다. 동등한 +path.prepend()기능을 정의하는 것은 쉽지 않습니다. Sans prolix nebulosity, 그와 그녀의 공유 된 정신력 :

+path.prepend() {
    local dirname
    for dirname in "${@}"; do
        dirname="${dirname%/}"
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname}:"* &&
           -d "${dirname}" ]] || continue
        PATH="${dirname}:${PATH}"
    done
    PATH="${PATH%:}"
    export PATH
}

하지만 ... 왜 질이 아닌가?

Gilles다른 곳에서 받아 들여진 대답 은 일반적인 경우에 "쉘과 무관 한 append 등식 부록" 으로서 인상적으로 최적이다 . 그러나, 바람직하지 않은 심볼릭 링크 가 없는 일반적인 경우에 bash, 그렇게하기 위해 필요한 성능 불이익은 젠투 라이저 를 슬프게합니다 . 바람직하지 않은 심볼릭 링크가 존재하더라도 인수 당 하나의 서브 쉘을 포크하는 것이 심볼릭 링크 복제본을 삽입 할 가치 가 있는지 여부는 논란의 여지가 있습니다.zshadd_to_PATH()

symlink 복제본도 제거해야하는 엄격한 사용 사례의 경우이 zsh특정 변형은 비효율적 인 포크가 아닌 효율적인 내장을 통해 수행됩니다.

+path.append() {
    local dirname
    for   dirname in "${@}"; do
        dirname="${dirname%/}"
        [[ "${dirname:0:1}" == '/' &&
           ":${PATH}:" != *":${dirname:A}:"* &&
           -d "${dirname}" ]] || continue
        PATH="${PATH}:${dirname}"
    done
    PATH="${PATH#:}"
    export PATH
}

원본 이 *":${dirname:A}:"*아니라 참고하십시오 *":${dirname}:"*. :Azsh포함하여 대부분의 다른 껍질에는 슬프게도 존재하지 않는 놀라운 현상 bash입니다. 인용 man zshexpn:

A : a수정 자 처럼 파일 이름을 절대 경로로 바꾼 다음 realpath(3)라이브러리 함수를 통해 결과를 전달하여 심볼릭 링크를 확인합니다. 참고 :이없는 시스템에서 realpath(3)라이브러리 기능을 심볼릭 링크가 있으므로 이러한 시스템에서 해결되지 aA동일합니다.

추가 질문이 없습니다.

천만에요. 안전한 포격을 즐기십시오. 이제 그럴 자격이 있습니다.


0

다음은 함수형 프로그래밍 스타일 버전입니다.

  • 콜론으로 구분 된 *PATH변수뿐만 아니라 작동 PATH합니다.
  • 전역 상태에 액세스하지 않습니다
  • 주어진 불변 입력에 대해서만 작동
  • 단일 출력을 생성
  • 부작용 없음
  • 기억할 수있는 (원칙)

주목할만한 점들 :

  • exporting 에 관한 불가지론 ; 그것은 발신자에게 남았습니다 (예 참조)
  • 순수 bash; 포크 없음
path_add () {
  # $ 1 : 주어진 경로 문자열에 정확히 한 번만 존재하도록하는 요소
  # $ 2 : 기존 경로 문자열 값 ( "PATH"가 아닌 "$ PATH")
  # $ 3 (선택 사항, 무엇이든) : 주어진 경우 $ 1을 추가하십시오; 그렇지 않으면, 접두사
  #
  # 예 :
  # $ export PATH = $ (path_add '/ opt / bin' "$ PATH")
  # $ CDPATH = $ (path_add '/ Music' "$ CDPATH"at_end)

  로컬 -r already_present = "(^ | :) $ {1} ($ | :)"
  만약 [[ "$ 2"= ~ $ already_present]]; 그때
    에코 "$ 2"
  elif [[$ # == 3]]; 그때
    에코 "$ {2} : $ {1}"
  그밖에
    에코 "$ {1} : $ {2}"
  fi
}

0

이 스크립트를 사용하면 다음 끝에 추가 할 수 있습니다 $PATH.

PATH=path2; add_to_PATH after path1 path2:path3
echo $PATH
path2:path1:path3

또는 시작 부분에 추가하십시오 $PATH.

PATH=path2; add_to_PATH before path1 path2:path3
echo $PATH
path1:path3:path2

# Add directories to $PATH iff they're not already there
# Append directories to $PATH by default
# Based on https://unix.stackexchange.com/a/4973/143394
# and https://unix.stackexchange.com/a/217629/143394
add_to_PATH () {
  local prepend  # Prepend to path if set
  local prefix   # Temporary prepended path
  local IFS      # Avoid restoring for added laziness

  case $1 in
    after)  shift;; # Default is to append
    before) prepend=true; shift;;
  esac

  for arg; do
    IFS=: # Split argument by path separator
    for dir in $arg; do
      # Canonicalise symbolic links
      dir=$({ cd -- "$dir" && { pwd -P || pwd; } } 2>/dev/null)
      if [ -z "$dir" ]; then continue; fi  # Skip non-existent directory
      case ":$PATH:" in
        *":$dir:"*) :;; # skip - already present
        *) if [ "$prepend" ]; then
           # ${prefix:+$prefix:} will expand to "" if $prefix is empty to avoid
           # starting with a ":".  Expansion is "$prefix:" if non-empty.
            prefix=${prefix+$prefix:}$dir
          else
            PATH=$PATH:$dir  # Append by default
          fi;;
      esac
    done
  done
  [ "$prepend" ] && [ "$prefix" != "" ] && PATH=$prefix:$PATH
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.