리눅스 tee 명령어 사용법 - sudo 로 파일에 쓰기와 로그를 보면서 저장하기
이 명령어를 언제 쓰는가
파이프를 흐르는 데이터를 화면에 보여 주면서 동시에 파일에도 저장할 때 쓴다. 배포 스크립트를 돌리면서 진행 상황은 눈으로 보고 전체 로그는 파일로 남기는 상황이 대표적이다. 리다이렉션(>)만 쓰면 파일에는 남지만 화면에는 아무것도 안 보인다. 그 문제를 해결하는 것이 tee 다.
그리고 실무에서 tee 를 가장 많이 꺼내는 이유는 따로 있다. sudo echo ... > /etc/파일 이 왜 Permission denied 로 실패하는가 — 이 문제의 표준 해법이 | sudo tee 이기 때문이다. 이 관용구 하나만 알아도 tee 는 값을 한다.
기본 형식
명령 | tee [옵션] 파일 [파일2 ...]
- 표준 입력으로 받은 것을 표준 출력과 지정한 파일 전부에 동시에 쓴다.
- 파일을 여러 개 나열하면 같은 내용이 모든 파일에 들어간다.
- 기본은 덮어쓰기다. 이어 쓰려면
-a를 붙인다.
이름은 배관의 T자 이음쇠에서 왔다. 흐름 하나를 둘로 나눈다는 뜻이 그대로 동작이다.
자주 쓰는 옵션
| 옵션 | 의미 | 예시 |
|---|---|---|
| (없음) | 파일을 덮어쓴다 | make | tee build.log |
-a | 덮어쓰지 않고 뒤에 이어 쓴다 | ./deploy.sh | tee -a deploy.log |
-i | 인터럽트(SIGINT)를 무시한다. Ctrl+C 로도 tee 가 안 죽는다 | long-job | tee -i job.log |
-p, --output-error=warn | 파이프가 끊겨도 바로 종료하지 않는다 (GNU 전용) | cmd | tee -p a.log | head |
>(...) | 파일 대신 프로세스로 보낸다. bash·zsh 의 프로세스 치환 | cmd | tee >(gzip > a.gz) > /dev/null |
실전 예제
1) sudo 권한으로 설정 파일에 쓴다. tee 의 존재 이유라고 해도 될 관용구다.
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-forward.conf
sudo echo ... > /etc/sysctl.d/99-forward.conf 가 실패하는 이유는 리다이렉션을 셸이 처리하기 때문이다. sudo 는 echo 만 root 로 실행하고, 파일을 여는 것은 여전히 일반 사용자 권한의 셸이다. tee 를 쓰면 파일을 여는 주체가 root 권한의 tee 가 되므로 통과한다.
2) 기존 설정에 한 줄 덧붙인다. 덮어쓰지 않도록 -a 를 반드시 확인한다.
echo 'AllowUsers deploy' | sudo tee -a /etc/ssh/sshd_config
3) 배포 로그를 보면서 남긴다. 실패했을 때 스크롤을 되짚지 않고 파일을 열면 된다.
./deploy.sh 2>&1 | tee -a /var/log/deploy-$(date +%F).log
2>&1 을 tee 앞에 두는 것이 중요하다. tee 는 표준 입력만 받으므로 표준 에러를 합쳐 주지 않으면 에러 메시지는 파일에 남지 않는다.
4) 여러 줄을 한 번에 파일로 만든다. heredoc 과 조합하면 설정 파일 생성이 한 블록으로 끝난다.
sudo tee /etc/systemd/system/myapp.service > /dev/null <<'EOF'
[Unit]
Description=My App
[Service]
ExecStart=/usr/local/bin/myapp
Restart=always
[Install]
WantedBy=multi-user.target
EOF
> /dev/null 로 화면 출력을 버리는 것이 요령이다. 안 그러면 파일 내용이 터미널에 그대로 한 번 더 찍힌다. heredoc 구분자를 'EOF' 처럼 따옴표로 감싸면 $ 가 셸에 의해 치환되지 않는다.
5) 중간 결과를 저장하면서 파이프를 계속 이어 간다. 긴 파이프라인을 디버깅할 때 유용하다.
grep 'ERROR' app.log | tee /tmp/errors.raw | awk '{print $5}' | sort | uniq -c
6) 같은 출력을 파일과 다른 명령에 동시에 보낸다. 저장과 집계를 한 번의 읽기로 끝낸다.
zcat access.log.gz | tee >(gzip > /backup/access.log.gz) | awk '{print $1}' | sort -u | wc -l
함정과 주의점
tee 는 파이프 앞 명령의 종료 코드를 가린다. $? 는 파이프라인의 마지막 명령인 tee 의 결과이고, tee 는 거의 항상 0 을 반환한다. 배포 스크립트가 실패했는데 성공으로 판정되는 사고가 여기서 난다.
$ false | tee /dev/null; echo $?
0
$ set -o pipefail
$ false | tee /dev/null; echo $?
1
스크립트 맨 위에 set -o pipefail 을 넣거나, bash 라면 ${PIPESTATUS[0]} 로 앞 명령의 종료 코드를 직접 확인한다.
-a 를 빠뜨리면 파일이 통째로 날아간다. echo 'x' | sudo tee /etc/hosts 는 /etc/hosts 를 그 한 줄로 갈아엎는다. 시스템 설정 파일에 tee 를 쓸 때는 명령을 실행하기 전에 -a 가 있는지 눈으로 한 번 더 확인하고, 중요한 파일은 sudo cp /etc/hosts /etc/hosts.bak 로 백업부터 뜬다.
표준 에러는 tee 를 그냥 지나간다. cmd | tee log 에서 에러 메시지는 터미널에만 뜨고 파일에는 안 남는다. 로그를 남기는 것이 목적이라면 2>&1 을 잊지 않는다. 순서도 중요해서 cmd 2>&1 | tee log 여야 하고 cmd | tee log 2>&1 은 다른 뜻이 된다.
뒤에 head 가 붙으면 tee 가 중간에 죽을 수 있다. head 가 먼저 종료하면서 파이프가 끊기면 tee 도 SIGPIPE 로 종료해 파일이 잘린 채 남는다. GNU 환경이면 tee -p 로 완화할 수 있다.
함께 보면 좋은 명령어
sudo— tee 관용구의 절반. 리다이렉션이 왜 sudo 를 통과하지 않는지 이해하고 있어야 한다.xargs— tee 로 저장한 목록을 다음 명령의 인자로 넘길 때 이어서 쓴다.less— tee 로 남긴 로그를 나중에 훑어볼 때 쓴다.