빌드노트
Bash에서 2>&1을 붙였는데 오류 로그가 파일에 안 들어간다
Bash의 >file 2>&1과 2>&1 >file을 실제로 비교한다. ERR가 부모 stdout으로 빠지는 결과와 복제 뒤 stdout을 다른 파일로 바꾼 결과로 연결 시점을 설명하고, 로그 목적지와 종료 코드를 구분한다.
Bash에서 명령 뒤에 2>&1 >run.log를 붙이면 파일에는 표준 출력만 들어가고 오류 메시지는 터미널에 남을 수 있다. 두 출력을 한 파일에 모으려면 >run.log 2>&1 순서로 쓴다. Bash는 리다이렉션을 왼쪽부터 처리하며, 2>&1은 그 시점에 표준 출력이 연결된 대상을 표준 오류에도 연결하기 때문이다.
파일 디스크립터는 프로세스가 입출력 대상을 구별하는 번호다. 표준 출력(stdout)은 1번, 표준 오류(stderr)는 2번이다. >run.log는 1번을 파일로 연결하고, 2>&1은 현재 1번의 연결을 2번에도 복제한다. 파일 내용을 복사하는 동작은 아니다. Debian이 제공하는 GNU Bash 5.2 매뉴얼 (새 탭에서 열림)의 리다이렉션 규칙에 따른 설명이다.
같은 OUT과 ERR로 순서만 바꿨다
차이를 확인하기 위해 Debian 13 Linux 컨테이너의 Bash 5.2.37에서 별도 Bash 자식을 실행했다. 자식은 1번으로 OUT, 2번으로 ERR를 한 줄씩 쓴다. 실제 운영 로그를 수집한 것이 아니라 출력 목적지를 구별하기 위한 합성 실험이다.
직접 비교하려면 Linux에서 bash --noprofile --norc로 새 Bash를 열고 다음 입력을 실행한다. Bash와 mktemp, cat, 임시 디렉터리를 만들 쓰기 권한이 필요하다. >는 기존 파일을 비우므로 실습에서는 mktemp -d로 만든 새 폴더의 파일만 사용한다.
lab=$(mktemp -d) || exit 1
bash --noprofile --norc -c 'printf "OUT\n"; printf "ERR\n" >&2' >"$lab/together.log" 2>&1
bash --noprofile --norc -c 'printf "OUT\n"; printf "ERR\n" >&2' 2>&1 >"$lab/split.log"
printf '%s\n' '--- together.log ---'
cat "$lab/together.log"
printf '%s\n' '--- split.log ---'
cat "$lab/split.log"
이 입력을 실행한 출력은 다음과 같았다.
ERR
--- together.log ---
OUT
ERR
--- split.log ---
OUT
맨 위의 ERR는 두 번째 자식이 파일 밖으로 보낸 출력이다. 첫 번째 자식의 ERR는 together.log 안에 있으므로 cat으로 파일을 읽을 때 나타난다. split.log에는 OUT만 남았다.
두 순서를 왼쪽부터 읽으면 목적지가 갈리는 위치를 알 수 있다.
>together.log 2>&1: 먼저 1번을 파일로 바꾼다. 이어 2번이 그 연결을 복제하므로 두 출력 모두 파일로 간다.2>&1 >split.log: 먼저 2번이 원래 1번의 연결을 복제한다. 그 뒤 1번만 파일로 바뀐다. 2번은 원래 표준 출력의 목적지에 남는다.
터미널에서는 stdout과 stderr가 같은 화면에 보여 어느 채널로 나왔는지 구별하기 어렵다. 그래서 별도 실험에서는 부모가 자식의 두 채널을 나누어 캡처했다. >file 2>&1은 파일에 OUT\nERR\n이 들어갔고 부모가 받은 stdout과 stderr는 모두 비었다. 2>&1 >file은 파일에 OUT\n만 들어갔고, 부모가 받은 stdout에 ERR\n, stderr에는 아무것도 없었다. 여기서 \n은 줄바꿈을 뜻한다.
즉, 두 번째 순서에서 화면에 남은 오류 메시지는 원래 stdout 쪽으로 나온 것이다. 부모가 연결한 stdout의 목적지가 터미널 대신 다른 수집기라면 그쪽으로 간다. 로그 파일에서 빠졌다는 사실만으로 출력이 사라졌다고 판단할 수는 없다.
복제 뒤에 stdout을 바꿔도 stderr는 따라가지 않는다
2>&1이 “앞으로 stdout이 가는 곳을 계속 따라가라”는 설정이라면, 나중에 1번을 바꿀 때 2번도 함께 바뀌어야 한다. 실제 동작을 구별하려고 같은 자식 명령에 >first.log 2>&1 >second.log를 붙였다.
관찰 결과는 first.log에 ERR, second.log에 OUT이었다. 2번은 복제 당시 1번이 연결돼 있던 첫 파일을 계속 사용했다. 뒤의 >second.log가 바꾼 것은 1번의 연결뿐이다. 출력 전에 이 리다이렉션들이 모두 처리됐는데도 결과가 나뉘었다.
여기에 마지막 2>&1을 한 번 더 붙인 >first.log 2>&1 >second.log 2>&1도 실행했다. 이번에는 첫 파일이 비어 있고 두 번째 파일에 OUT과 ERR가 모두 들어갔다. 마지막 복제가 새 1번의 연결을 다시 가져왔기 때문이다. 실제 명령에 리다이렉션이 여러 개 있으면 최종 파일 이름만 보지 말고 각 2>&1을 만나는 시점의 1번 목적지를 확인한다.
로그의 목적지와 명령의 성공 여부는 따로 본다
앞의 합성 자식은 ERR를 써도 종료 코드가 0이었다. 마지막에 exit 7을 넣은 대조군에서는 두 리다이렉션 순서 모두 종료 코드 7을 반환했고, 파일과 부모 채널에 나뉘는 출력은 그대로였다. 오류가 파일에 들어갔다고 작업이 성공한 것도, stderr에 한 줄을 썼다고 반드시 실패한 것도 아니다.
실제 작업에서는 명령 직후 rc=$?로 상태를 저장해 성공·실패를 판단한다. cat으로 로그를 읽은 뒤의 $?는 이미 cat의 상태다. Bash 매뉴얼의 EXIT STATUS (새 탭에서 열림)도 마지막 명령의 종료 상태를 $?로 확인한다고 설명한다.
두 출력을 기존 로그에 덧붙여야 한다면 순서는 >>"$log" 2>&1이다. $log에는 사용할 로그 경로를 먼저 넣는다. 새 임시 파일에 이 방식으로 두 번 실행했을 때는 OUT, ERR가 각각 두 번씩 남았다. 순서를 고쳤는데도 파일이 생기지 않으면 경로 오류도 확인한다. 상위 디렉터리가 없는 경로에 >missing/out.log 2>&1을 붙인 별도 실행은 자식이 시작되기 전에 실패했고, Bash의 No such file or directory 진단과 종료 코드 1이 남았다.
여기서 확인한 범위는 Bash 5.2.37의 전경 자식 하나가 고정 문자열을 차례로 쓰는 경우다. 여러 프로세스가 동시에 로그를 쓰거나 프로그램이 출력을 버퍼링할 때의 줄 순서는 검증하지 않았다. 다른 셸과 PowerShell에서도 같은 문법과 결과라고 가정하지 않는다.