리다이렉션
고친 사람 github-actions[bot]
리다이렉션은 명령이 읽고 쓰는 곳을 실행 직전에 바꿔 주는 셸 기능입니다. 셸은 사용자가 친 명령 줄을 해석해 프로그램을 실행해 주는 프로그램입니다. 리다이렉션을 쓰면 화면에 찍힐 출력을 파일에 담거나 키보드 대신 파일에서 입력을 읽게 할 수 있습니다. 웹에서는 같은 말이 다른 주소로 다시 요청하라는 응답을 가리키기도 합니다.
쉽고 빠른 이해
무슨 일을 하나 — 명령의 출력이 나가는 곳과 입력이 들어오는 곳을 바꿉니다. ls > files.txt 라고 치면 파일 목록이 화면 대신 files.txt 에 적힙니다.
왜 이렇게 하나 — 명령마다 「결과를 파일로 저장」 옵션을 따로 만들 필요가 없어집니다. 셸이 한 번에 맡아 주니 어떤 명령에든 똑같이 붙습니다.
어떻게 도나
- 셸이 명령 줄에서
>·<같은 기호를 찾습니다 - 명령을 실행하기 직전에 기호가 가리키는 파일을 엽니다
- 명령의 입출력 통로 끝에 그 파일을 이은 뒤 명령을 실행합니다
대가 — 기호 하나 차이로 결과가 크게 갈립니다. > 는 파일을 비우고 새로 쓰므로 잘못 치면 원래 내용이 사라집니다. 기호를 적는 순서가 결과를 바꾸기도 합니다.
상세
마당 수도꼭지는 늘 같은 구멍으로 물을 내보냅니다. 그 끝에 호스를 끼우면 물은 바닥 대신 화분으로 갑니다. 꼭지를 뜯어고친 사람은 없습니다. 호스만 바꿔 끼웠을 뿐입니다.
리다이렉션(redirection)은 셸이 명령을 실행하기 전에 그 명령의 입력과 출력이 이어진 곳을 바꾸는 일입니다. sort names.txt > sorted.txt 라고 치면 정렬 결과가 화면 대신 sorted.txt 에 적힙니다.
이 기능 덕분에 프로그램을 짜는 사람은 결과를 어디에 둘지 고민하지 않습니다. 화면에 찍듯이 쓰기만 하면 됩니다. 그 결과를 파일로 보낼지, 버릴지, 다른 명령으로 넘길지는 명령을 실행하는 사람이 정합니다. 명령마다 --output 같은 옵션을 따로 만들 필요가 없어집니다.
명령이 시작할 때부터 가진 통로 세 개
프로그램은 파일을 열 때마다 운영체제에게서 작은 정수 하나를 받습니다. 이 번호를 파일 디스크립터라고 부릅니다. 그 뒤로는 읽기와 쓰기를 부탁할 때 파일 이름 대신 이 번호를 건넵니다.
프로그램이 시작될 때 0번, 1번, 2번은 이미 열려 있습니다. 이 셋이 명령의 입출력 통로입니다. 셋의 쓰임이 정해져 있어서 프로그램은 따로 열지 않고 바로 씁니다.
| 번호 | 이름 | 평소에 이어진 곳 |
|---|---|---|
| 0 | 표준 입력 | 키보드 |
| 1 | 표준 출력 | 터미널 화면 |
| 2 | 표준 오류 | 터미널 화면 |
정상 결과는 1번으로, 오류 메시지는 2번으로 나갑니다. 둘 다 평소에는 화면으로 가서 한데 섞여 보입니다.
리다이렉션은 이 세 번호가 가리키는 곳을 바꾸는 일입니다. 번호는 그대로 두고 그 끝에 이어진 대상만 바꿉니다. 앞의 비유로 치면 번호가 수도꼭지이고 파일이 호스 끝의 화분입니다.
방향을 바꾸는 기호
기호 앞에 붙은 숫자가 바꿀 번호입니다. 숫자를 생략하면 > 는 1번, < 는 0번을 뜻합니다.
| 기호 | 하는 일 |
|---|---|
< 파일 |
0번이 파일을 읽게 합니다 |
> 파일 |
1번이 파일에 쓰게 합니다. 파일이 있으면 먼저 비웁니다 |
>> 파일 |
1번이 파일 끝에 덧붙이게 합니다 |
2> 파일 |
2번이 파일에 쓰게 합니다 |
2>&1 |
2번이 지금 1번이 가리키는 곳을 같이 가리키게 합니다 |
<< EOF |
다음 줄부터 EOF 만 적힌 줄 앞까지를 0번으로 넣습니다. << 뒤의 낱말은 아무것이나 됩니다. 흔히 EOF(End Of File, 파일 끝)를 씁니다. 히어 문서라고 부릅니다 |
> 와 >> 의 차이에서 가장 자주 사고가 납니다. 아래는 같은 파일에 세 번 쓴 결과입니다. 줄마다 오른쪽 주석이 그 줄을 실행한 뒤 파일에 남은 내용입니다.
echo hi > a.txt # a.txt: hi
echo yo >> a.txt # a.txt: hi 다음 줄 yo
echo ok > a.txt # a.txt: ok
셋째 줄의 > 가 앞의 두 줄을 지웠습니다. 로그 파일처럼 계속 쌓아 가야 하는 파일에는 >> 를 씁니다.
이번에는 입력을 바꿔 보겠습니다. wc -l 은 줄 수를 세는 명령입니다. 아래 두 줄은 같은 파일의 줄 수를 셉니다. 오른쪽 주석이 화면에 찍히는 값입니다.
wc -l a.txt # 1 a.txt
wc -l < a.txt # 1
첫 줄은 wc 가 파일 이름을 받아 직접 엽니다. 그래서 결과에 이름을 붙입니다. 둘째 줄은 셸이 파일을 열어 0번에 꽂아 줍니다. wc 는 이름을 모른 채 0번에서 읽기만 해서 숫자만 찍습니다.
표준 오류의 방향
정상 결과와 오류를 가르고 싶으면 2번을 따로 돌립니다. 아래 명령은 결과를 out.log 에, 오류를 err.log 에 담습니다.
./build.sh > out.log 2> err.log
둘을 한 파일에 모으려면 2>&1 을 씁니다. &1 은 「1번 파일 디스크립터」라는 뜻입니다. & 를 빼고 2>1 이라고 치면 셸은 1 이라는 이름의 파일을 만듭니다.
./server > app.log 2>&1
아예 버리고 싶은 출력은 /dev/null 로 보냅니다. 널 장치라고 부르는 특수 파일입니다. 여기에 쓴 것은 어디에도 남지 않습니다.
cron 은 정해진 시각에 명령을 실행해 주는 예약 도구입니다. 사람이 화면을 보지 않는 작업이라 명령 끝에 > /dev/null 2>&1 을 붙여 출력을 모두 버리는 일이 흔합니다.
기호를 적는 순서
셸은 리다이렉션 기호를 왼쪽부터 하나씩 처리합니다. 그리고 2>&1 은 「그 순간 1번이 가리키는 곳」을 베낍니다. 1번이 나중에 바뀌어도 2번은 따라가지 않습니다. 그래서 같은 기호 둘도 순서가 바뀌면 결과가 달라집니다.
아래 표는 > out.log 2>&1 을 한 단계씩 따라갑니다. 칸마다 그 단계가 끝난 뒤 1번과 2번이 가리키는 곳입니다.
| 단계 | 1번 | 2번 |
|---|---|---|
| 시작 | 화면 | 화면 |
> out.log |
out.log | 화면 |
2>&1 |
out.log | out.log |
1번을 먼저 파일로 돌린 뒤 2번이 그것을 베꼈습니다. 결과와 오류가 모두 파일로 갑니다.
이번에는 두 기호를 뒤집은 2>&1 > out.log 입니다.
| 단계 | 1번 | 2번 |
|---|---|---|
| 시작 | 화면 | 화면 |
2>&1 |
화면 | 화면 |
> out.log |
out.log | 화면 |
2번은 아직 화면을 가리키던 1번을 베꼈습니다. 그 뒤 1번만 파일로 떠나서 오류는 화면에 남습니다. 로그 파일에 오류가 안 보이면 이 순서부터 확인합니다.
셸이 뒤에서 하는 일
리다이렉션은 명령이 아니라 셸이 처리합니다. 이 소절은 sort names.txt > sorted.txt 한 줄을 셸이 어떤 순서로 처리하는지 따라갑니다.
프로세스는 실행 중인 프로그램 하나를 가리킵니다. 셸도 프로세스입니다. sort 도 실행되면 프로세스가 됩니다.
셸은 sort 를 실행할 때 먼저 자기를 복제해 자식 프로세스를 만듭니다. 파일을 열고 번호를 바꾸는 일은 이 자식 안에서 일어납니다.
커널은 운영체제의 핵심부입니다. 파일을 열거나 프로세스를 만드는 일은 커널이 맡습니다.
시스템 콜은 프로그램이 커널에 일을 부탁하는 정해진 호출입니다. 셸과 자식은 이 호출로 커널에 일을 맡깁니다. 이번 일에는 호출 다섯 개가 차례로 쓰입니다.
fork 는 지금 프로세스를 복제하는 호출입니다. 자식은 부모 셸의 파일 디스크립터를 복사해 받습니다. 그래서 자식의 0·1·2번도 처음에는 셸과 같은 곳을 가리킵니다.
자식은 open 으로 sorted.txt 를 비우고 엽니다. 커널은 비어 있는 번호 중 가장 작은 것을 새 파일에 줍니다. 0~2번이 이미 차 있으므로 3번을 받습니다.
dup2 는 한 번호가 다른 번호와 같은 곳을 가리키게 만드는 호출입니다. dup2(3, 1) 을 부르면 1번이 화면 대신 sorted.txt 를 가리킵니다. 할 일을 마친 3번은 close 로 닫습니다.
마지막으로 exec 가 자식 프로세스에서 도는 프로그램을 sort 로 바꿉니다. 프로그램이 바뀌어도 열린 번호는 남습니다. 그래서 sort 는 평소처럼 1번에 쓸 뿐인데 결과가 파일로 갑니다.
아래 그림은 지금까지의 순서를 한데 모은 것입니다.
sequenceDiagram
participant 셸
participant 자식 as 자식 프로세스
participant 커널
셸->>커널: fork
커널-->>자식: 셸을 복제해 만든다
자식->>커널: open("sorted.txt") · 비우고 연다
커널-->>자식: 3
자식->>커널: dup2(3, 1)
자식->>커널: close(3)
자식->>커널: exec("sort")
Note over 자식: sort 는 1번에 쓰기만 한다
이 모든 일이 자식 안에서 일어나므로 셸 자신의 1번은 계속 화면을 가리킵니다. 명령이 끝나면 다음 프롬프트가 평소처럼 화면에 뜹니다.
파이프와 함께 쓸 때
파이프는 한 명령의 1번 끝을 다른 명령의 0번에 잇는 연결입니다. | 기호로 명령을 이은 줄을 파이프라인이라고 부릅니다. 리다이렉션이 번호의 끝을 파일에 붙인다면, 파이프는 그 끝을 다른 명령에 붙입니다.
둘을 섞으면 파이프 연결이 먼저 이뤄지고 리다이렉션이 그 위에 적용됩니다. grep error 는 입력에서 error 가 든 줄만 골라 찍는 명령입니다. 아래 명령은 오류 출력까지 grep 으로 넘깁니다.
./build.sh 2>&1 | grep error
파이프가 1번을 먼저 grep 에 이어 두었습니다. 그다음 2>&1 이 그 1번을 베꼈습니다. 그래서 2번도 grep 으로 흘러갑니다.
덮어쓰기로 잃는 파일
sort a.txt > a.txt 는 파일을 정렬해 같은 이름으로 되돌려 두려는 명령처럼 보입니다. 끝나고 나면 a.txt 는 비어 있습니다. 셸이 sort 를 실행하기 전에 > 를 처리하면서 파일을 먼저 비우기 때문입니다. sort 가 읽기 시작할 때는 읽을 것이 남아 있지 않습니다.
결과를 임시 파일에 먼저 쓰고, 성공하면 원래 이름으로 옮기는 식으로 피합니다. 읽는 파일과 쓰는 파일을 한 명령 줄에서 같게 두지 않는 것이 요점입니다.
덮어쓰기 사고 자체를 막고 싶으면 셸의 noclobber 설정을 켭니다. 켜 두면 > 가 이미 있는 파일을 만났을 때 비우지 않고 오류를 냅니다. 그래도 덮어써야 할 때는 >| 로 적습니다.
sudo 로 실행한 명령의 리다이렉션
sudo 는 명령 하나를 관리자 권한으로 실행해 주는 도구입니다. 그런데 sudo echo x > /etc/app.conf 는 권한 오류로 실패합니다. 관리자 권한을 얻는 것은 echo 뿐입니다. 파일을 여는 것은 명령 줄을 해석한 사용자의 셸입니다. 셸은 일반 사용자 권한이라 그 파일을 쓰기로 열지 못합니다.
파일을 여는 프로세스까지 관리자 권한으로 실행해야 합니다. 흔히 echo x | sudo tee /etc/app.conf 처럼 tee 에 맡깁니다. tee 는 0번으로 받은 것을 파일에 쓰고 화면에도 찍는 명령입니다. 이렇게 하면 파일을 여는 주체가 관리자 권한으로 실행된 tee 가 됩니다.
파일로 보낸 출력이 늦게 보일 때
터미널에서는 한 줄씩 바로 보이던 출력이, 파일로 보내면 한참 뒤에 몰려 나오기도 합니다. 프로그램이 1번 끝에 무엇이 붙었는지 알아채고 쓰는 방식을 바꾸기 때문입니다.
많은 프로그램은 출력을 곧바로 내보내지 않고 메모리에 모아 두었다가 한꺼번에 씁니다. 이것을 버퍼링이라고 합니다. C 표준 라이브러리는 1번이 터미널이면 한 줄마다 내보냅니다. 1번이 파일이면 모아 둔 공간이 찰 때 내보냅니다. 파이썬의 print 도 같은 식으로 움직입니다.
그래서 서버를 띄우면서 로그를 파일로 돌리면 파일이 한동안 비어 보일 수 있습니다. 프로그램이 강제로 죽으면 모아 두었던 줄은 파일에 닿지 못합니다. 프로그램이 줄마다 내보내도록 설정하면 됩니다. 파이썬이면 -u 옵션으로 버퍼링을 끕니다.
웹에서 말하는 리다이렉션
웹 개발에서 리다이렉션은 전혀 다른 기능을 가리킵니다. 서버가 요청을 받고 「이 자원은 다른 주소에 있다」고 답하는 응답입니다. 브라우저는 그 응답을 받으면 알려 준 주소로 새 요청을 알아서 보냅니다. 이 응답은 흔히 리다이렉트라고도 부릅니다.
HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜)에서는 3으로 시작하는 상태 코드 묶음의 이름이 리다이렉션입니다. 301 은 주소가 영구히 옮겨졌다는 뜻입니다. 302 는 잠시 다른 주소에 있다는 뜻입니다. 새 주소는 Location 헤더에 실립니다. 셸의 리다이렉션과는 이름만 겹칩니다. 동작은 관계가 없습니다.
관련 항목
리다이렉션이 끝을 바꾸는 입출력 통로
파일 디스크립터 · 표준 입력 · 표준 출력 · 표준 오류 · 터미널 · 표준 입출력
리다이렉션을 처리하는 셸과 시스템 콜
셸 · bash · POSIX · fork · exec · dup2 · open · close · 시스템 콜 · close-on-exec
리다이렉션과 나란히 입출력을 잇는 수단
파이프 · 파이프라인 · 히어 문서 · 명령 치환 · 프로세스 치환 · 네임드 파이프 · tee
리다이렉션의 목적지가 되는 파일
널 장치 · 장치 파일 · 로그 파일 · 임시 파일
리다이렉션에서 자주 나는 실수와 장애
noclobber · 버퍼링 · sudo · isatty · 파일 권한
리다이렉션과 이름이 겹치는 웹 응답
리다이렉트 · HTTP · 상태 코드 · 상태 코드 등급 · Location 헤더 · 301 Moved Permanently · 302 Found · 리다이렉션 URI
다른 이름: redirection · 리디렉션 · 입출력 리다이렉션 · 입출력 재지정