PATH에 적은 물결표(~)는 HOME이 아닐 수도 있습니다

The tilde in your PATH may not be your HOME

disconnect3d.pl ▲ 208 댓글 141 arusekk

요약

셸 설정에서 따옴표 안에 쓴 물결표(~)는 홈 경로로 바뀌지 않고 현재 폴더 기준 경로로 PATH에 들어갑니다.

블로그 disconnect3d.pl의 작성자가 AI 에이전트용 샌드박스(격리된 실행 환경) 도구 nono를 써 보다가 겪은 일입니다. nono가 샌드박스가 쓸 수 있는 PATH 항목으로 ~/.local/bin/을 경고했고, 원인은 셸 설정 파일의 한 줄이었습니다.

왜 중요한가

  • 흔히 쓰는 export PATH="$PATH:~/.local/bin/" 한 줄이 의도와 달리 상대 경로를 PATH에 넣습니다.
  • 그러면 현재 폴더에 ~라는 디렉터리가 있을 때 그 안의 프로그램이 명령어로 실행될 수 있습니다.
  • 샌드박스 도구의 경고가 실제 설정 실수를 잡아낸 사례이기도 합니다.

핵심 내용

  • 물결표 확장(~를 홈 경로로 바꾸는 셸 기능)은 따옴표 밖에 있는 ~에만 일어납니다.
  • 그래서 따옴표 안의 ~/.local/bin/은 홈 경로가 아니라 ./~/.local/bin/으로 PATH에 들어갑니다.
  • 작성자는 현재 폴더에 ~ 디렉터리를 만들고 프로그램을 넣어 실제로 실행되는 것을 보여 줬습니다.
  • 따옴표를 빼면 Bash와 Zsh에서는 확장되지만, 공백 하나로도 대입이 깨질 수 있어 불안하다고 했습니다.
  • 해결책은 ~ 대신 $HOME을 쓰는 것이고, echo "$PATH" | grep -- '~'로 점검할 수 있습니다.

HN 반응

  • 따옴표를 안 붙이면 될 일이라는 지적과, 대입문에서만 확장되는 규칙이 헷갈려 스크립트에는 $HOME만 쓴다는 의견이 나왔습니다.
  • 사용자 폴더를 PATH 앞에 두면 위험하다는 우려에는, 시스템 명령을 덮어쓰려는 흔한 방식이라는 반론이 나왔습니다.

댓글

30개 표시 · 전체 141개
  1. amarshall HN

    그냥 틸데를 따옴표로 감싸지 않으면 되잖아요. 다들 Bash에서 “문자열”만 보면 반사적으로 따옴표를 치는데, (거의) 모든 게 문자열이고 대부분의 문자열은 따옴표로 감싸지 않으며 감싸면 오히려 이상하다는 걸 모르는 것 같아요(예를 들어 "ls" "-a" "foo"라고 쓰는 사람은 없잖아요).

  2. kragen HN

    틸데를 따옴표로 감싸지 않아도 해결되지 않습니다:

        : tmp; echo $PATH:~
        /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:~
    

    수정: mjmas 님이 지적하셨듯 제가 틀렸습니다. 변수 대입에는 다른 캘빈볼 규칙이 적용되기 때문입니다:

        : tmp; x=$PATH:~
        : tmp; echo "$x"
        /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/home/user
    

    그리고 전반적으로 당신의 조언은 아주 나쁜 조언입니다. 셸 스크립트 코드의 대부분을 차지하는 명령줄 인자에서는, 변수 확장이 들어간 문자열을 항상 따옴표로 감싸야 합니다. 변수 확장 뒤에 공백을 기준으로 암묵적으로 쪼개지기를 원하는 경우가 아니라면요. 그렇게 쪼개지는 건 변수에 저장한 것이 하나로 묶인 문자열이 아니라 공백으로 구분된 목록이라는 뜻이기 때문입니다.

    이건 실제로 원하는 동작인 경우가 거의 없고, 목록을 저장하고 싶은 드문 경우에도 공백이 들어간 파일 이름에서는 셸의 암묵적 공백 분리가 대개 깨집니다. Bash에는 진짜 배열 변수가 있어서 이를 일급 시민처럼 다룰 수는 있지만, 끔찍하게 번거롭습니다:

        : tmp; x=("foo bar" baz)
        : tmp; echo "${x[@]}"
        foo bar baz
        : tmp; touch "${x[@]}"
        : tmp; ls -l "${x[@]}"
        -rw-r--r-- 1 user user 0 Oct 11 17:52  baz
        -rw-r--r-- 1 user user 0 Oct 11 17:52 'foo bar'
    

    이 ksh 기능은 Bourne 셸과 dash에는 없지만, MirBSD ksh, Bash, zsh에는 모두 있습니다.

    일반적으로 셸 스크립트에서 큰따옴표 밖에 $variable $expansion이 보이면, 파일 이름이나 디렉터리 이름에 공백이 들어 있을 때 그 스크립트가 실패할 거라고 의심해야 합니다. 아주 많은 경우 이것이 경로 주입 보안 취약점으로 이어집니다. 따옴표 없는 $variable $expansion이 안전한 맥락도 있지만 상대적으로 드뭅니다.

    그런데 이번 건에는 중요한 디테일이 있습니다! 그 안전한 맥락 중 하나가 바로 변수 대입이고, 아래에서 mjmas 님이 도움이 되는 댓글로 짚어 주셨듯 여기서는 따옴표 없는 변수 확장이 실제로 완벽하게 안전합니다:

        : tmp; a='x  y'
        : tmp; b=α:$a:ω
        : tmp; echo "$b"
        α:x  y:ω
    
  3. mjmas HN

    Bash는 변수 대입 안에서만 그렇게 합니다:

      a=123:~
      echo $a
      echo 123:~
    

    결과는 다음과 같습니다:

      123:/home/me
      123:~
    
  4. kragen HN

    아, 바로잡아 주셔서 감사합니다. 제가 틀렸습니다. 캘빈볼이네요.

  5. theowaway213456 HN

    : tmp;는 무슨 주문인가요?

  6. kragen HN

    죄송합니다, 제 셸 프롬프트입니다. 실수로 복사해서 붙여넣어도 아무 해가 없게 만든 건데, tcsh의 기본 프롬프트인 >와는 정반대 극단이죠.

  7. Cockbrand HN

    이건 상식이라서 굳이 언급할 가치도 없다고 생각했어요. 하지만 이 글이 보여 주듯 상식이 아닌 모양이네요.

    저도 이 문제를 겪은 적이 있냐고요?

    물론 있죠.

    그 일을 계기로 따옴표를 더 잘, 꼭 필요한 곳에만 쓰도록 배웠냐고요?

    물론 배웠죠.

    Bourne 호환 셸은 익히는 데 시간이 걸리고 경험도 필요합니다. 이건 바뀌지 않겠지만, 나름의 단점을 가진 대안들이 있긴 하죠.

  8. MathMonkeyMan HN

    제가 같이 일해 본 사람 중에 셸이 어떻게 동작하는지 아는 사람은 거의 없어요. 문서를 보내 주긴 하는데, 사람들이 문서를 읽는다면 그게 무슨 세상이겠어요? 게다가 술술 넘어가는 글도 아니고요.

  9. Cockbrand HN

    저는 이런 걸 친구들, O'Reilly 책, 그리고 끝없는 밤샘 실패와 시행착오로 배웠습니다. 제가 대단한 고수는 아니지만, 필요한 일을 하는 데 부족한 셸 실력은 없다고 생각합니다.

    [여기에 요즘 애들은 집중력이 없다는 가벼운 푸념 삽입]

  10. jolux HN

    게다가 구성도 형식도 엉망이고요.

  11. isityettime HN

    이 기능, 적어도 문제가 되는 구문 단위에는 여기서 "bare words"라는 이름이 붙어 있습니다. Perl과 Ruby의 일부 맥락, YAML, 그리고 아마 다른 언어들에도 있을 거예요, 잘 모르겠지만요.

    저는 셸을 전반적으로 잘 쓰는 것처럼 보이는 사람들한테서도 이런 걸 본 적이 있어요. Idrgi

  12. SoftTalker HN

    저는 스크립트에서는 틸데를 절대 쓰지 않고, 대화형으로 명령어를 입력할 때 편의상으로만 씁니다.

    그 외에는 $HOME을 쓰는데, 이쪽에도 함정이 있긴 하지만 다른 환경 변수와 똑같은 함정입니다.

  13. ndegruchy HN

    네, 스크립트에서는 최대한 명시적으로 쓰려고 합니다. 홈 디렉터리는 $HOME, /home/foo/.local/share/는 $XDG_DATA_HOME 같은 식으로요.

    구체적으로 쓸수록 함정에 빠질 일이 줄어듭니다.

  14. nlehuen HN

    bash의 따옴표·이스케이프 규칙은 캘빈볼급으로 기괴한데, 저는 그런 지식으로 제 머리를 무겁게 만들 생각이 없어요.

  15. weinzierl HN

    좋은 결정입니다. 가능하면 셸은 피하세요. 저는 이미 늦었어요. 셸 프로그래밍을 꽤 많이 했고(진짜 고수들한테 배우기도 했고요) 그래서 머리가 벌써 무거워졌습니다. 그런데도 좀 길거나 복잡한 셸 스크립트를 오류 0개로 짤 수 있다는 자신감은 여전히 0이라, 얻은 게 하나도 없어요.

  16. FeepingCreature HN

    ~/.local/bin PATH가 안 먹히는 걸 보고 눈치챘어야 하는 거 아닌가요?

  17. thefilmore HN

    그냥 이렇게 하면 돼요:

      PATH=~/.local/bin:$PATH
    

    PATH는 이미 export되어 있고, 대입에는 따옴표도 필요 없어요.

  18. stkdump HN

    PATH에서 사용자가 쓸 수 있는 디렉터리를 시스템 디렉터리보다 앞에 두는 건 위험해 보이는데요.

  19. bityard HN

    아뇨, 아주 흔한 일이에요. 여러 가지 합당하고 편리한 이유로 시스템 명령어를 일부러 덮어쓰거나 감싸고 싶을 때가 많거든요.

    누군가 ~/.local/bin에 불법 바이너리를 심을까 봐 걱정이시라면, 보안 문제는 PATH 순서보다 훨씬 더 심각한 거예요.

  20. russellbeattie HN

    다들 까다롭고 난해한 얘기를 하는 김에, zsh에서는 이렇게 할 수 있어요:

    path+=(~/.local/bin)

    "export" 키워드도 쓸 필요가 없어요. 제가 이렇게 쓰겠다는 건 아니지만, 다들 셸 잡학을 늘어놓고 있으니까요.

    (zsh에서 소문자 "path" 변수는 $PATH 문자열에 자동으로 연결된 배열이라서, 괄호로 새 배열 요소를 표시하고 공백으로 구분해 넣으면 바로 추가할 수 있어요.)

  21. alexpotato HN

    틸데와는 직접 상관없지만, 대기업에서 bash 때문에 벌어질 수 있는 황당한 일에 관한 이야기입니다.

    예전 직장에서 제 bashrc의 어느 부분이 특정 환경 변수를 설정하는지 알아내려던 적이 있습니다. 사용자 프로필에 설치된 어떤 기본값이거나 /etc/<무언가>에서 물려받은 것이겠거니 짐작했습니다.

    제 bashrc가 다른 파일들을 불러오고 있다는 건 금방 알았습니다. 이번에도 그 불러오기가 한 단계 깊이일 거라고 생각했습니다.

    알고 보니 "ur-bashrc"(원조 bashrc)에서 시작해 10단계가 넘게 불러오기가 이어졌고, 층층이 더 많은 파일을 불러온 끝에야 사용자 수준 프로필에 도달하는 구조였습니다.

    너무 복잡해서 미쳐 버릴 지경이었는데, 이 Stack Exchange 글을 찾고서야 해결됐습니다: https://unix.stackexchange.com/questions/813/how-to-determine-where-an-environment-variable-came-from/154971#154971

    bash의 불러오기에 "추적"을 켜서, 환경 변수가 어디서 설정되는지 범위를 좁혀 갈 수 있게 해 줍니다.

  22. OroPla HN

    20년 넘게 bash를 쓰면서 틸데를 따옴표로 감쌀 생각을 해 본 적이 없어서, 이 문제는 겪어 본 적이 없어요.

    ~가 변수와 다르게 동작한다는 건 흥미롭긴 하네요. 보통은 문자열 리터럴 그대로를 얻으려면 작은따옴표를 써야 하는데, 그렇다고 따옴표 안에서 틸데가 확장되길 바라지도 않을 테니까요.

  23. dspillett HN

    “저한테는 항상 잘 되던데요”라고 말하려던 참에 “… 사실 Bash와 Zsh에서는 되는데, 왜냐하면 …”이라는 대목을 봤네요.

    이식성을 신경 쓸 때 쓰지 않도록 조심해야 할 Bash 전용 문법이 또 하나 늘었어요.

    많은 시스템에서 /bin/sh가 bash(나 zsh)가 아니라는 점도 짚어 둘 만해요. Bash 전용 문법(Bashism)에 기대려면(혹은 그런 걸 일일이 찾아보기 귀찮다면) 해시뱅을 “#!/bin/bash”처럼 명확하게 적으세요. 예를 들어 데비안과 그 계열에서는 보통 dash거든요.

  24. opello HN

    busybox를 쓰는 여러 임베디드 환경에서는 busybox 나름의 ash입니다. 저는 이런 경계 사례에 부딪혀서 뭔가를 배우게 되는 기회가 대체로 즐거워요.

  25. dspillett HN

    맞아요. 평소 일이나 취미로 쓸 때는 Bash 전용 문법에 기대는 게 전혀 거리낌 없지만, 그 한계가 어디인지는 알고 싶어요. 평소 Bash의 편의 문법으로 쓰는 것을 POSIX 방식으로는 어떻게 하는지 당장 떠오르지 않더라도, 필요할 때 그게 필요하다는 사실과 어디서 찾아야 하는지는 알거든요.

  26. the__alchemist HN

    리눅스 배포판이 스크립트 언어와 분리된 "터미널"/"CLI" 프로그램을 기본으로 제공했으면 좋겠어요. 특정 셸에 묶이지 않는 범용 Path 환경 변수가 있고, cd 명령을 실행하거나 python/git/cargo 같은 임의의 애플리케이션을 띄울 수 있고, 쓸 만한 북마크 + 자동완성 시스템도 갖춘 거죠. 스크립트 언어와 CLI 애플리케이션이 뒤섞여 있는 게 이런 복잡함과 미묘함의 근본 원인 같아요.

    셸 스크립팅을 한다면(그리고 Python보다 Bash 등을 선호한다면) 계속 Bash/Fish/Zsh 등을 쓰면 되고, GUI가 없는 애플리케이션을 실행하거나 디렉터리를 이동하거나 파일 시스템 작업을 하려고 CLI를 쓴다면 그냥 평범한 터미널을 쓰면 되죠.

  27. dicytea HN

    $PATH는 특정 셸에 묶여 있지 않아요. 그리고 말씀하신 게 바로 셸이잖아요. 기존 해법과 뭐가 다른 건지 모르겠네요.

  28. the__alchemist HN

    묶여 있어요. 그래서 리눅스에서 Path에 뭔가를 추가하기가 까다로운 거고요. 아니면, 흔히 알려진 방법(export)과 실제로 해야 하는 일(예: bash 설정 파일 편집) 사이에 불일치가 있다고 해야겠네요.

  29. hnlmorg HN

    범용 PATH 변수라는 게 없으니까요.

    환경 변수는 전역이 아니에요. fork()가 기본적으로 실행 중인 환경 변수를 자식에게 넘겨주기 때문에 전역인 것 같은 착각이 들 뿐이죠. 그렇게 값이 아래로 흘러 내려가는 겁니다.

  30. the__alchemist HN

    맞아요. 설치한 애플리케이션을 이름만 입력해서 실행하려는 것뿐인데 그게 짜증스러워요. 제 생각엔 PATH가 바로 그걸 위한 거거든요.

Hacker News에서 보기 ↗