Standard Output, Standard Input, Redirection, Pipelines… 복잡한 것들 한 곳에 정리.

2016년 4월 17일에 이글루스에 썼던 글을 옮겨왔다.
살짝, 한 군데(&>)만 고쳤다.

부제 : stdout, stderr 를 모두 Redirection.


이 글 내내, 그리고 리눅스(유닉스)를 쓰는 동안 반드시 기억해야 할 것은,

모든 것은 파일로 이뤄져있다.(Everything is a file)

라는 개념이다.

파일은 당연히 파일이고, 디렉토리도 파일이며, 심지어 하드디스크도, 마우스도 모두 파일이다. 잊지 말자.

** 이 글에 나오는 ‘전문적’인 내용은 모두 The Linux Command Line (by William E. Shotts, Jr.) 에서 가져왔다.


자..

ls -al > aa.txt

이걸 보자.
이렇게 출력내용을 파일로 저장하는 방법은, DOS 를 처음 접했던 시절부터 자연스레 공식처럼 써왔었다. 정확한 의미도, 용어도 모른채, 그냥 결과만을 기억했을 뿐이다. 이런 내용이 나와있던 DOS 책, 페이지 디자인도 어렴풋이 기억이 난다. (아직까지 내 책장에 “Inside the IBM PC and PS/2” 가 꽂혀있긴 한데..)

DOS 는 모르겠고, (이제와서 알 필요도 없고)리눅스에서, 이걸 비전문적인 용어로 저걸 풀어보자면 이런 식이다.

ls -al –> 명령을 수행하고 결과값을 화면으로 출력한다.
> –> 화면 출력을 파일로 돌린다.
aa.txt –> 화면 출력을 받아서 aa.txt 를 생성하고 저장한다.

그냥 직관적으로 이해하면 된다. 이제 이걸 전문적인 용어로 바꿔보자면..

ls -al –> 명령을 수행하고 결과값을 ‘Standard Output’ 이란 파일로 보낸다.
> –> stdout 을 다른 곳으로 전환한다. (Redirection)
aa.txt –> Redirected 된 stdout 을 aa.txt 에 저장한다.

여기서 중요한 점. ls -al 결과값이 그냥 ‘문자열’이 아니고 Standard Out 이라는 특수한 ‘파일’로 보내진 상태라는 것이다.

또 하나 생각해야 할 점은, 프로그램이 의도한 결과값은 Standard Output 으로 보내지고, 오류문구들은 ‘Standard Error(stderr)’ 라는 파일로 보내진다는 사실이다.

자~ 여기서 이런 거 하나 살펴보자.
find 명령 사용할 때.. 이런 식으로 했으리라.

find / -iname 'gedit'

이렇게 하면, 제대로된 결과도 나오지만, ‘허가 거부’라는 다소 거슬리는(?) 문구도 계속해서 나온다. 이유는, 일반 사용자가 접근할 수 없는 곳까지 들어가며 찾으려 하기 때문이고, 그렇게 나온 오류를 표시해야 하기 때문에 지저분한 결과가 나올 수 밖에 없는 것이다.

이걸 피하기 위해선, 이렇게 하라고 한다.

find / -iname 'gedit' 2> /dev/null

다른 사람들은 모르겠지만, 난 도무지 이게 외워지질 않았다. 그것도 /dev/null 이 아니라, 2> 부분이.

2 를 기억하지 못했던 건, 저걸 그냥 외우려고 했기 때문이다. 그나마 자주 쓰면 모를까, 띄엄띄엄, 1년에 서너번이나 쓸까말까 했으니 기억이 안나는 것도… (내가 바보라 그런 건 아닐까? ^^)

복잡한 것을 다 집어넣고, 간단히 정리하자면, 이런 입/출력에는 번호가 매겨져있다는 것을 알아야 한다.

0 : Standard Input
1 : Standard Output
2 : Standard Error

따라서,

ls -al > aa.txt 는
ls -al 1> aa.txt

와 같다.

미루어 짐작하면, 당연히 2> 는 Standard Error 를 다른 파일로 전환하라..는 뜻이 된다.

이제 살짝 응용을 해보자. 만약, stdout 과 stderr 을 한 파일로 보내고 싶다면 어떻게 해야 하려나?
과거에는 이렇게 했다고 한다.

ls -al > aa.txt 2>&1

요즘엔 이렇게 하라고 한다. (이게 훨씬 직관적이다.)

** 다만, 이 방식은 모든 Shell 에서 지원하진 않는다. Bash, zsh 에선 되고, sh, ksh 에선 안된다.

ls -al &> aa.txt

여기까지면 stdout, stderr 에 대해 충분히 이해할 수 있었으리라~ 생각한다.
다음은 보다 헷갈리는 Standard Input 이다.


내가 이해할 수 없었기에, 그래서 공부를 하기 시작했고, 그 결과 이 글을 쓸 수 있게끔 만들어준 명령.

ls -al /etc | gzip > foo.txt.gz

바로 이거다.
여기엔 두가지 개념이 새로 나온다. Pipelines 와 Standard Input.
Pipe 는 대충 이해하곤 있었지만, gzip > foo.txt.gz 은 도무지 이해할 수 없었다. 이게 뭔 뜻일까..

자~ 먼저 Pipe.
이것은 앞 명령에서 나온 stdout 을 다음 명령에 stdin 으로 보내주는 것이다.

ls -l /usr/bin | less

이렇게 되면 앞 명령 결과(stdout)을 받아서, less 에 인수(stdin)로 넘겨주게 된다.
주로 이런 식으로 많이 쓴다.

ls -l /usr/bin | sort | grep zip

ls 결과를 정렬한 뒤, 거기에서 zip 이 들어간 것만을 찾으라는 명령이다.
그야말로 직관적으로 이해하면 된다.

** 넘어가기 전에 Standard Input 에 대해 살짝 덧붙이면, stdin 은 키보드 입력이라 생각하면 된다.

여기까지는 어려울 게 없었는데…
이해가 어려웠던 Gzip 을 살펴보자. gzip 사용방법은 이런 식이다.

gzip foo.txt

기존에 내가 알고 있던 일반적인 압축프로그램과 확연히 다른 점은, 원본 파일이 없어진 다는 점이다.
저 결과로 foo.txt.gz 파일이 생기는데, 원본인 foo.txt 는 사라진다.
뭐, 그래 여기까진 좋다.

gzip > foo.txt.gz

도대체 얘는 무슨 뜻이란 말이냐..

다음 명령들 차이를 알아보자.

gzip abcd.txtgzip abcd.txt > xyz.gzgzip -c abcd.txt > xyz.gz
  • 첫번째 명령은 abcd.txt 를 압축하여 abcd.txt.gz 이란 파일을 만든다. (abcd.txt 는 사라진다.)
  • 두번째 명령부터 헷갈리기 시작하는데.. 이것은, abcd.txt.gz 을 먼저 만든다. 그리고, 이 명령을 수행하고 난 결과(stdout)을 xyz.gz 라는 파일로 출력한다. 그러나, gzip abcd.txt 를 수행하고 나면 아무런 결과값이 출력되지 않으므로, 즉 stdout 엔 아무 것도 없게 되므로, xyz.gz 에는 아무 것도 넘어가질 않게 된다. 그냥, 파일 이름만 xyz.gz 라는 것이 하나 생기게 된다. (abcd.txt 는 사라진다.)
  • 세번째 명령은 -c 를 써서, stdout 을 ‘의미있게’ 만들어준다. gzip -c abcd.txt 는, gzip abcd.txt 를 수행한 뒤, 그 결과를 stdout 으로 출력하라는 의미가 된다. 그리고 이것을 Redirection 을 통해 xyz.gz 로 돌려줬으므로, abcd.txt 파일은 xyz.gz 안에 abcd.txt 라는 파일명을 유지한채 압축된다. 그리고 abcd.txt 는 사라지지 않는다.

여기까지는 이해가 된다.

다시 돌아가서,

gzip > foo.txt.gz

를 보자.

이것을 터미널에서 실행하면, 커서가 그냥 깜빡깜빡하는 상태가 유지된다.
여기에서 ‘What the hell’을 입력하고, Ctrl+D를 누르면…
foo.txt.gz 가 생성되고, 그 안엔 foo.txt 라는 파일이 있으며, 그 파일 내용은 ‘What the hell’ 이다.

음?그럼 이걸 터미널에서 실행하면?

gzip -c > bar.txt.gz

를 입력하고, ‘What the heck’ 을 넣어보면..
bar.txt.gz 가 생성되고, 그 안엔 bar.txt 라는 파일이 있으며, 그 파일 내용은 ‘What the heck’ 이다.

이것으로 미루어 짐작해보면,

압축할 파일이 명시되지 않았을 경우, 그리고 Redirection 이 있을 경우 -c 옵션이 주어지지 않아도 기본적으로 작동된다.

이것이 결론이다.

즉,

ls -l /etc | gzip > foo.txt.gz

이것과

ls -l /etc | gzip -c > foo.txt.gz

이것은 같다.

** 만약 gzip 뒤에 압축할 파일이 명시적으로 주어진다면, 위 명령 결과는 전혀 다르다. (위에 예가 나와있으니 헷갈리지 말 것!)

이렇게 몸으로 때우고 나서 gzip man 페이지를 보니까.. 이 내용이 써 있었다. (그것도 제일 첫머리에..)

If no files are specified, or if a file name is “-“, the standard input is compressed to the standard output. Gzip will only attempt to compress regular files.

그런데, 이 글을 먼저 봤어도 쉽게 이해하진 못했을 것 같다. (확실히 이론보단 실전이 중요하다~)
아무튼, 위에 내가 써놓은 내용이 맞는 내용이었음을 이렇게 증명할 수 있었다. (Quod Erat Demonstrandum.)


최종적으로 위 명령을 풀어서 정리를 하자면, ls -l /etc 결과(stdout)를 Pipe 를 통해 gzip 에서 stdin 으로 사용할 수 있도록 넘긴다.
gzip 은 결과값을 받아서 (먼저 그 파일(stdout–>stdin)을 foo.txt 로 이름을 바꾸고) 압축을 한 뒤 결과를 stdout 으로 보낸다.
Redirection 으로 stdout 은 foo.txt.gz 으로 저장된다.

설명에 괄호를 쓴 것은, 내 추측이기 때문이다. 허나.. 논리적으로 보자면 그게 맞는 것 같다.
ls 가 넘긴 stdout을 gzip 은 stdin 으로 받는다. 그런데, 그게 모두 어쨌든 ‘파일’이기 때문에, 그 파일을 저장/압축하기 위해서 일단 foo.txt 로 이름을 변경하여 저장한 뒤, 최종적으로 압축을 한다.
아마 이런 시나리오 아닐런지??

이것 때문에 또 반나절이 휙~ 지나갔지만.. 그래도 몇 십년간 대충 알고 있던 것을 조금이나마 정확하게 알게 되니 뭔가 뿌듯함이 밀려온다.

아무튼, 이 책.. 꽤 좋은 책임엔 틀림이 없다.

Author: 아무도안

안녕하세요. 글 남겨주셔서 고맙습니다.