Devin.KR
로그인

리눅스 tee 명령어 사용법 - sudo 로 파일에 쓰기와 로그를 보면서 저장하기

개발자 조회 1

이 명령어를 언제 쓰는가

파이프를 흐르는 데이터를 화면에 보여 주면서 동시에 파일에도 저장할 때 쓴다. 배포 스크립트를 돌리면서 진행 상황은 눈으로 보고 전체 로그는 파일로 남기는 상황이 대표적이다. 리다이렉션(>)만 쓰면 파일에는 남지만 화면에는 아무것도 안 보인다. 그 문제를 해결하는 것이 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 로 남긴 로그를 나중에 훑어볼 때 쓴다.