youngfromnowhere

[Linux Shell] pyenv 설정하면서 마주친 shell script 분석 본문

Linux Shell

[Linux Shell] pyenv 설정하면서 마주친 shell script 분석

곽일땡 2023. 9. 22. 14:28

https://youngnowhere.tistory.com/54

 

[Python] pyenv로 가상환경 구축하기.

1. Python의 여러 가상환경 관리 툴 지금 듣고 있는 강의에서 가상환경 구축을 위해 pyenv라는 툴을 이용하라고 한다. 근데 이게 좀 복잡해 보인다. 가상환경만 만드는게 아니라 python 버전 관리 기능

youngnowhere.tistory.com

해당 포스팅에서 pyenv를 설치할 때, 메세지에서 안내하는 바에 따라 .profile, .bashrc 등의 설정파일을 수정했다. 이제 각 파일의 역할이 무엇이었고 거기에 추가한 스크립트는 무슨 의미였는지 알아보자.

 

로그인 shell 프로세스 이해하기

(출처 <리눅스 커맨드라인 쉘 스크립트 바이블> 리처드 블룸, 트랜지스터팩토리 옮김, 3rd, 176p.)

리눅스 시스템에 로그인했을때 실행되는 shell을 login shell이라 한다. login shell은 다음 다섯 파일을 찾아서 실행시킨다.

  • /etc/profile
  • $HOME/.bash_profile
  • $HOME/.bashrc
  • $HOME/.bash_login
  • $HOME/.profile

($HOME은 전역 환경변수로, user의 홈 디렉터리 /home/(user_name)을 가르킨다. 화면 상에서는 ~로 표시되는 directory다)

/etc/profile은 로그인하는 모든 사용자들이 실행하는 파일이며 나머지 파일들은 각 사용자에 따라 사용여부가 다를 수 있다.

이 나머지 네 $HOME 시동 파일들은 다음 목록의 순서에 따라 처음 발견되는 파일만이 실행되며 나머지는 무시된다.

  • $HOME/.bash_profile
  • $HOME/.bash_login
  • $HOME/.profile

이 중 $HOME/.bash_profile은 먼저 $HOME/.bashrc 파일이 있는지 확인하고 있으면 그 안에 있는 명령을 먼저 실행한다.

 

현재 $HOME 디렉토리를 보면, 현재 로그인 쉘은 ~/.profile과 ~/.bashrc를 사용하는 것을 알 수 있다.

 

pyenv 설치 당시 떴던 메세지

현재 홈 디렉토리에 어떤 시동 파일이 있는지 알았으니, 이제 이 안내문을 어느 파일에 적용해야 하는지도 알 수 있다. 첫번째 스크립트는 ~/.profile에, 두번째 스크립트는 ~./bashrc에 추가하면 될 것 같다. (두번째 스크립트는 실제로 추가하지 않았다.)

 

Shell Script 이해하기

그럼 이제 pyenv가 ~./profile에 추가하라고 한 스크립트의 내용을 해석해보자.

export PYENV_ROOT="$HOME/.pyenv"
command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

첫번째 line

PYENV_ROOT라는 환경변수를 설정하라는 내용이다.

 

두번째 line의 command 명령

요약하여 설명하면, 'shell normal function look up'을 생략하라는 명령이다. pyenv라는 명령을 호출하면 bash는 먼저 .bashrc에 pyenv라는 function이 정의되어 있는지 찾는다. 그리고 없으면 pyenv라는 이름의 program(binary 파일)을 찾아서 실행한다. 그런데 shell에서 "command pyenv"를 호출하면, pyenv라는 이름의 function이 있는지 찾는 과정을 생략하고 바로 program을 찾아 실행하라는 뜻이다.

command에 -v 옵션이 붙으면, 인자로 입력된 명령에 대한 description(설명문)을 출력한다. -V는 더 자세한 설명문을 출력한다. command -[v 혹은 V] 명령은 argument가 function이냐 program이냐 여부에 관계없이, shell이 해당 명령을 찾아낼 수 있는지 여부에 따라 exit code를 반환한다.

/dev/null은 명령에 의한 출력을 억제하기 위해 리눅스에서 쓰이는 파일이다. 이곳으로 redirect되는 모든 데이터는 없어지고 출력되지 않는다.

|| operator는 OR 논리연산자로, shell script에서 condition expression 사이에서 쓰이면 각 expression의 boolean 값에 대해 OR 연산을 하지만, command 사이에서 쓰이면 "첫번째 command가 실패하면 (즉, exit code가 0이 아니면) 두번째 command를 실행하라"라는 의미가 된다.

/dev/null에 의해 화면 출력은 억제되지만, shell이 command에 argument로 입력되는 command를 찾아내느냐 여부에 따라 exit code는 달라진다. 마지막 명령의 exit code는 $? 변수에 보관된다. 이를 이용하여 test 해보면

/dev/null로 출력은 억제했지만, exit code는 정상적으로 반환된다. python2는 shell이 인지하지 못하는 명령이고 python3는 shell이 인지하는 명령이다.

이를 바탕으로 두번째 line을 해석하자면, "pyenv라는 명령을 shell이 인지하면(즉, pyenv가 이미 설치되어 있으면) 그냥 넘어가고, 인지하지 못하면 PATH 환경변수의 맨 앞에 $HOME/.pyenv/bin을 추가하라"라는 뜻이다.

 

세번째 line의 eval

--help를 통해 제공되는 설명에 따르면, eval은 argument로 들어온 문자열을 하나의 문자열로 통합한 후, "그것을 shell에 대한 input으로 쓴 뒤 그 결과를" 명령으로서 실행한다.

설명을 그냥 읽어서는 '어차피 script라는게 shell에 내리는 명령을 순서대로 써놓은 것인데 뭐가 달라지는 것이지?'라 할 수 있다. 위에서 강조한 부분에 유의해야 한다.

결론부터 말하자면, eval 명령을 붙이면 해당 line은 '두 번 parsing'된다.

스크립트나 프롬프트를 통해 shell에 명령을 전달하면 shell은 먼저 전달된 문자열을 parsing한 뒤, 그 결과를 명령으로 받아 수행한다.

"use the result as input to the shell, and execute the resulting commands"에서 첫번째 'the result'는 shell에 의해 처음으로 parsing된 결과를 말하는 것이고, 두번째 'the resulting commands'은 첫번째 'the result'가 'input to the shell'로써 쓰였으므로, 그에 대한 result 즉 두번째 parsing의 결과물을 말한다.

 

test.sh의 내용과 실행결과

위 예시를 보면, 먼저 foo라는 변수에 10을 할당했고, x에는 "foo"라는 문자열을 할당했다.

y='$'$x

shell은 이 line을 파싱하여, $x에는 x 변수의 값인 "foo"문자열을 가져다쓰고 '$'는 따옴표로 둘러쌓였으므로 '문자 그대로의 $'으로 escape된다. 따라서 파싱 결과는 y=$foo이고 이 때 우변은 모두 '문자열'이다. 따라서 echo $y를 실행하면 "$foo"라는 문자열이 출력된다.

eval y='$'$x

앞에 eval이 붙으면, y='$'$x를 shell에 input한 뒤 그 result를 command로 쓴다. 즉, y='$'$x에서 파싱을 한번 거친 y=$foo 를 command로 쓴다는 것이다. 그럼 이것은 다시한번 shell에 의해 파싱되어 y 변수에 foo 변수의 값인 10이 할당된다. 따라서 echo $y를 실행하면 10이 출력된다.

세번째 line의 $(command)

https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html

 

Command Substitution (Bash Reference Manual)

3.5.4 Command Substitution Command substitution allows the output of a command to replace the command itself. Command substitution occurs when a command is enclosed as follows: or Bash performs the expansion by executing command in a subshell environment a

www.gnu.org

Command substitution allows the output of a command to replace the command itself.

$(command)라 입력했을 때 command의 자리에 해당 command의 output을 대입한다는 것이다.

즉, script의 마지막 line인 

eval "$(pyenv init -)"

을 해석하면, pyenv init - 라는 명령의 output을 따옴표를 이용해 하나의 string으로 묶은 뒤, eval을 통해 두번 parsing하여 실행하라는 내용인 것이다.

pyenv가 install된 상태에서 pyenv init - 명령은 다음과 같은 output을 반환한다.

이 글에서 이 스크립트를 전부 해석하지는 않을 것이다. 각종 환경변수를 세팅하고 pyenv의 subcommand들을 parsing하는 구문임을 알 수 있다.