Bash 스크립트가 있는 디렉터리 경로 구하기 — dirname, BASH_SOURCE, readlink -f 실측 비교

설정 파일이나 같은 저장소의 다른 스크립트를 읽으려면 스크립트 자신이 있는 디렉터리를 알아야 한다. pwd는 호출한 쪽의 작업 디렉터리일 뿐이라 쓸 수 없고, dirname $0은 실행 방식에 따라 결과가 달라진다. 흔히 쓰이는 한 줄짜리 해법도 여러 가지인데, 심볼릭 링크나 source 같은 상황에서 각각 다르게 동작한다.

이 글에서는 많이 쓰이는 다섯 가지 방법을 실행 방식 여덟 가지(상대·절대 경로, 심볼릭 링크, PATH, source, stdin)로 Ubuntu 24.04(bash 5.2.21, GNU coreutils 9.4)에서 직접 돌려 비교하고, 상황별로 무엇을 쓸지 정리한다.

비교한 다섯 가지 방법

#!/usr/bin/env bash
echo "\$0                 : $0"
echo "BASH_SOURCE[0]     : ${BASH_SOURCE[0]}"
echo "A dirname \$0       : $(dirname -- "$0")"
echo "B dirname SOURCE   : $(dirname -- "${BASH_SOURCE[0]}")"
echo "C cd && pwd        : $(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
echo "D cd && pwd -P     : $(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)"
echo "E readlink -f      : $(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")"

A·B는 경로 문자열만 자르고, C·D는 실제로 cd해서 절대 경로를 얻고, E는 심볼릭 링크까지 풀어낸다.

실험 디렉터리 구성

mkdir -p "/tmp/so/my proj/bin" /tmp/so/links /tmp/so/other
cp whereami.sh "/tmp/so/my proj/bin/" && chmod +x "/tmp/so/my proj/bin/whereami.sh"
ln -s "../my proj/bin/whereami.sh" /tmp/so/links/wa   # 파일 심볼릭 링크
ln -s "my proj/bin" /tmp/so/bindir                    # 디렉터리 심볼릭 링크

경로에 공백을 일부러 넣었고, 모든 실행은 /tmp/so/other에서 한다. 스크립트의 실제 위치는 /tmp/so/my proj/bin이다.

실행 방식별 결과

결과가 갈리는 네 가지 케이스의 실제 출력이다.

$ ../links/wa                          # 파일 심볼릭 링크
$0                 : ../links/wa
BASH_SOURCE[0]     : ../links/wa
A dirname $0       : ../links
B dirname SOURCE   : ../links
C cd && pwd        : /tmp/so/links
D cd && pwd -P     : /tmp/so/links
E readlink -f      : /tmp/so/my proj/bin

$ ../bindir/whereami.sh                # 디렉터리 심볼릭 링크
$0                 : ../bindir/whereami.sh
BASH_SOURCE[0]     : ../bindir/whereami.sh
A dirname $0       : ../bindir
B dirname SOURCE   : ../bindir
C cd && pwd        : /tmp/so/bindir
D cd && pwd -P     : /tmp/so/my proj/bin
E readlink -f      : /tmp/so/my proj/bin

$ source "../my proj/bin/whereami.sh"  # source
$0                 : bash
BASH_SOURCE[0]     : ../my proj/bin/whereami.sh
A dirname $0       : .
B dirname SOURCE   : ../my proj/bin
C cd && pwd        : /tmp/so/my proj/bin
D cd && pwd -P     : /tmp/so/my proj/bin
E readlink -f      : /tmp/so/my proj/bin

$ bash < "../my proj/bin/whereami.sh"  # stdin
$0                 : bash
BASH_SOURCE[0]     :
A dirname $0       : .
B dirname SOURCE   : .
C cd && pwd        : /tmp/so/other
D cd && pwd -P     : /tmp/so/other
E readlink -f      : .

source에서는 $0이 호출한 셸 이름(bash)이 되어 A가 틀리고, stdin 실행에서는 파일 경로 자체가 없어 모든 방법이 실패한다.

여덟 케이스 전체 결과다(/tmp/so 접두어는 생략, 실제 위치와 같으면 ✔).

실행 방식A dirname $0B dirname SOURCEC cd && pwdD pwd -PE readlink -f
상대 경로 실행../my proj/bin../my proj/bin✔✔✔
절대 경로 실행✔✔✔✔✔
bash script.sh../my proj/bin../my proj/bin✔✔✔
파일 심볼릭 링크../links../links/links/links✔
디렉터리 심볼릭 링크../bindir../bindir/bindir✔✔
PATH 검색/links/links/links/links✔
source.../my proj/bin✔✔✔
bash < script.sh../other/other.

A·B는 상대 경로를 그대로 돌려주므로 스크립트 안에서 cd한 뒤에는 쓸모가 없다. 모든 링크를 따라가는 것은 E뿐이다.

상황별 선택

필요한 것방법비고
일반적인 스크립트 위치C cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd외부 명령 의존 최소
디렉터리 링크를 푼 물리 경로D (C에서 pwd -P)파일 링크는 못 푼다
~/bin 등에 링크로 설치한 스크립트E dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")"GNU coreutils 또는 macOS 12.3+
source되는 라이브러리 자신의 위치B·C·E 중 하나 (BASH_SOURCE 기반)$0 기반 A는 불가
#!/usr/bin/env bash
set -euo pipefail
# 링크로 설치될 일이 없으면 첫 줄, 있으면 둘째 줄
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
SCRIPT_DIR=$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")
source "$SCRIPT_DIR/../lib/common.sh"

source된 라이브러리에서 BASH_SOURCE 배열

BASH_SOURCE는 호출 스택과 같은 순서로 쌓이는 배열이라, 라이브러리 함수 안에서 자기 위치와 메인 스크립트 위치를 모두 얻을 수 있다.

show_src() {
    echo "\$0              : $0"
    echo "BASH_SOURCE     : ${BASH_SOURCE[*]}"
    echo "FUNCNAME        : ${FUNCNAME[*]}"
    LIB_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
    MAIN_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[-1]}")" &>/dev/null && pwd)
    echo "LIB_DIR         : $LIB_DIR"
    echo "MAIN_DIR        : $MAIN_DIR"
}
#!/usr/bin/env bash
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
source "$SCRIPT_DIR/../lib/common.sh"
show_src
$ cd /tmp/so/other && "../my proj/bin/main.sh"
$0              : ../my proj/bin/main.sh
BASH_SOURCE     : /tmp/so/my proj/bin/../lib/common.sh ../my proj/bin/main.sh
FUNCNAME        : show_src main
LIB_DIR         : /tmp/so/my proj/lib
MAIN_DIR        : /tmp/so/my proj/bin

BASH_SOURCE[0]은 함수가 정의된 파일, BASH_SOURCE[-1]은 최초 진입 스크립트다.

주의사항

&>/dev/null을 빼면 CDPATH 환경에서 경로가 두 번 찍힌다. cd가 CDPATH로 디렉터리를 찾으면 그 경로를 stdout에 출력하고, 이것이 $(...)에 함께 잡힌다.

d1=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
d2=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
printf "d1=[%s]\nd2=[%s]\n" "$d1" "$d2"
$ cd /tmp/so && CDPATH=/tmp/so bash "my proj/bin/cdpath.sh"
d1=[/tmp/so/my proj/bin
/tmp/so/my proj/bin]
d2=[/tmp/so/my proj/bin]

--를 빼면 -로 시작하는 경로가 옵션으로 해석된다.

$ dirname -x/whereami.sh
dirname: invalid option -- 'x'
Try 'dirname --help' for more information.
$ dirname -- -x/whereami.sh
-x

sh script.sh로 실행하면 깨진다. Ubuntu의 sh는 dash라 배열 문법이 없다. shebang을 #!/usr/bin/env bash로 두고 실행 권한으로 실행할 것.

$ sh "../my proj/bin/whereami.sh"
$0                 : ../my proj/bin/whereami.sh
../my proj/bin/whereami.sh: 3: Bad substitution
  • curl ... | bash나 bash < script.sh로 배포하는 설치 스크립트는 위 표의 stdin 케이스에 해당하므로, 자기 위치에 의존하지 말거나 파일로 내려받아 실행하게 안내한다.
  • macOS의 readlink는 12.3부터 -f를 지원한다. 기본 /bin/bash는 3.2라 음수 인덱스 ${BASH_SOURCE[-1]}(bash 4.3+)도 쓸 수 없으니 ${BASH_SOURCE[${#BASH_SOURCE[@]}-1]}로 쓴다. (macOS 환경이 없어 문서 기준으로만 정리했다.)
  • readlink -f는 마지막 구성 요소(파일)가 없어도 경로를 출력하고 성공(0)을 반환한다. 존재까지 확인하려면 realpath -e나 readlink -e를 쓴다.
  • $(cd ... && pwd)는 서브셸에서 cd하므로 호출한 셸의 작업 디렉터리는 바뀌지 않는다. 스크립트 전체를 그 디렉터리 기준으로 돌리고 싶으면 cd -- "$SCRIPT_DIR"를 별도로 실행한다.

마무리

대부분의 스크립트는 C 한 줄로 충분하고, ~/bin에 심볼릭 링크로 설치하는 스크립트라면 readlink -f 기반 E를 쓴다. $0은 source 상황에서 틀리므로 위치 계산에는 항상 BASH_SOURCE를 쓰는 것이 안전하다.

참고

답글 남기기