Devin.KR
로그인

리눅스 env 명령어 사용법 - 환경변수 확인과 임시 변수로 명령 실행하기

개발자 조회 1

이 명령어를 언제 쓰는가

"터미널에서는 되는데 cron에서만 안 된다"는 문제의 90%는 환경변수 차이다. 특히 PATH가 다르다. env는 이 차이를 눈으로 확인하고 재현하게 해 주는 명령이다.

또 하나의 용도는 실행이다. env VAR=값 명령 형태로 쓰면 그 명령 하나에만 환경변수를 붙일 수 있다. export로 셸 전체를 오염시키지 않아도 되고, 테스트 후 되돌릴 필요도 없다. 두 용도가 이름 하나에 붙어 있어서 처음엔 헷갈리지만, 실무에서는 두 번째가 더 자주 쓰인다.

기본 형식

env [-i] [-u 이름] [이름=값]... [명령 [인자...]]

명령을 생략하면 현재 환경변수를 전부 출력한다. 명령을 주면 이름=값들을 환경에 얹은 뒤 그 명령을 실행한다. env 자신은 exec로 넘겨주고 사라지므로 프로세스가 추가로 남지 않는다.

자주 쓰는 옵션

옵션의미예시
(없음)현재 환경변수 전체 출력env | sort
-i기존 환경을 전부 비우고 시작env -i /bin/sh -c 'echo $PATH'
-u 이름해당 변수만 제거하고 실행env -u http_proxy curl -I https://devin.kr
-0줄바꿈 대신 NUL로 구분(값에 개행이 있을 때)env -0 | tr '\0' '\n'
-C 디렉터리지정 디렉터리로 이동한 뒤 실행(coreutils 8.28+)env -C /srv/app ./run.sh
-S "문자열"문자열을 인자로 쪼갬. shebang에서 옵션을 넘길 때#!/usr/bin/env -S python3 -u

-C는 coreutils 8.28(2017년) 이상에 있으므로 Ubuntu 20.04나 RHEL 8(둘 다 coreutils 8.30)에서도 쓸 수 있고 macOS의 env에도 있다. -S는 coreutils 8.30 이상과 macOS/BSD env 양쪽에 다 있다. 아주 오래된 CentOS 7(coreutils 8.22)만 둘 다 없다고 보면 된다.

실전 예제

1) cron과 같은 빈 환경을 만들어 "왜 cron에서만 안 되는지" 확인한다.

env -i PATH=/usr/bin:/bin /bin/sh -c 'echo $PATH; command -v node'

env -i만 쓰면 셸이 자기 기본 PATH를 다시 채워 넣어 cron의 값과 달라지므로, cron이 쓰는 PATH를 직접 지정해 주는 편이 정확하다. cron은 로그인 셸을 거치지 않아 PATH가 /usr/bin:/bin 정도로 짧다. nvm이나 pyenv로 설치한 실행 파일이 안 잡히는 이유가 이것이다. 해결책은 크론탭 안에서 절대 경로를 쓰거나 맨 위에 PATH=...를 명시하는 것이다.

2) 환경변수 하나만 바꿔 애플리케이션을 한 번 돌려 본다(셸은 그대로).

env NODE_ENV=production LOG_LEVEL=debug node app.js

3) 프록시 설정을 잠깐 무시하고 접속되는지 시험한다.

env -u http_proxy -u https_proxy curl -sI https://devin.kr | head -1

4) 이미 실행 중인 프로세스가 어떤 환경으로 떴는지 본다(리눅스 전용).

sudo tr '\0' '\n' < /proc/1234/environ

서비스가 잘못된 설정으로 뜬 것 같을 때 결정적인 증거가 된다. /proc/PID/environ프로세스가 시작될 때의 환경이라 실행 중 변경분은 반영되지 않는다는 점만 기억하면 된다.

5) 특정 변수만 골라 확인한다.

env | grep -E '^(PATH|LANG|TZ|JAVA_HOME)='

6) 스크립트 첫 줄에서 인터프리터를 PATH로 찾게 한다.

#!/usr/bin/env python3

파이썬이 /usr/bin/python3일 수도 /usr/local/bin/python3일 수도 있고 가상환경 안일 수도 있다. 경로를 박지 않고 env에게 찾게 하는 것이 이식성 있는 관행이다.

함정과 주의점

env FOO=1 echo $FOO 는 빈 값이 나온다. 셸이 $FOOenv를 실행하기 전에 먼저 치환하기 때문이다. 이 시점에는 아직 FOO가 없으니 빈 문자열이 들어간다. 확인하려면 치환을 늦춰야 한다.

env FOO=1 echo $FOO              # 빈 줄 (셸이 먼저 치환)
env FOO=1 sh -c 'echo $FOO'      # 1  (하위 셸이 나중에 치환)

env는 export하지 않은 셸 변수를 보지 못한다. FOO=1만 하면 그것은 셸 내부 변수라 자식 프로세스에 전달되지 않고 env 출력에도 없다. 자식이 못 읽는다는 뜻이므로 스크립트가 값을 못 받는 원인이 여기인 경우가 많다.

FOO=1
env | grep FOO      # 안 나옴
export FOO
env | grep FOO      # FOO=1

셸 변수까지 전부 보려면 bash/zsh 내장 명령인 set을, 내보낸 변수만 정확히 보려면 export -p를 쓴다.

env 출력에는 비밀이 그대로 들어 있다. DB 비밀번호나 API 키를 환경변수로 넣는 구조가 흔한데, env 결과를 그대로 로그나 이슈에 붙여 넣으면 그대로 유출된다. 공유 전에 env | grep -viE 'pass|secret|token|key' 정도로 걸러야 한다. 같은 이유로 리눅스에서는 같은 uid의 사용자가 /proc/PID/environ을 읽을 수 있으니, 다중 사용자 서버라면 비밀은 환경변수보다 권한을 좁힌 파일에 두는 편이 안전하다.

systemd 서비스는 로그인 셸의 환경을 물려받지 않는다. ~/.bashrc에 export를 아무리 써도 systemctl start로 뜬 서비스에는 적용되지 않는다. 유닛 파일의 Environment= 또는 EnvironmentFile=에 써야 하고, 실제로 무엇이 적용됐는지는 아래로 확인한다.

systemctl show -p Environment myapp.service

함께 보면 좋은 명령어

  • printenv - 변수 하나만 값으로 뽑을 때 printenv PATH가 더 간결하다.
  • crontab - env로 확인한 PATH 차이를 크론탭 상단에서 바로잡는다.
  • systemctl - 서비스로 뜬 프로세스의 환경은 유닛 파일에서 관리한다.