Redirection

지금은 터미널 프로그램을 열면 별다른 설정 없이 바로 명령을 입력할 수 있고 출력을 볼 수 있지만 unix 이전의 OS 에서는 프로그래머가 직접 입,출력장치를 (punch card reader, magnetic tape drive, disk drive, line printer, card punch, interactive terminal) 설정하고 연결해야 했다고 합니다. 하지만 unix 에서는 data stream 이라는 개념을 이용하여 장치를 추상화 함으로써 프로그래머가 어떤 종류의 장치를 사용하는지 알 필요 없이 동일하게 open, read, write 할 수 있게 하였고 또한 프로그램 실행시 자동으로 입,출력 장치를 연결하여 많은 부담을 덜 수 있게 하였습니다.

스트림은 디스크상의 파일이나 컴퓨터에 연결되는 여러 장치들을 통일된 방식으로 다루기 위한 가상적인 개념입니다. 따라서, 스트림은 데이터가 어디서 나와서 어디로 가는지 신경을 쓸 필요없이 자유롭게 어떤 장치 및 프로세스, 화일들과 연결될 수 있어서 프로그래머에게 많은 편리성을 줍니다. 컴퓨터에 연결되는 디지털 장치들은 결국은 비트 값들의 입, 출력에 의해 동작 되므로 OS 단에서 이와 같은 추상화가 가능합니다.

모든 프로세스는 기본적으로 입,출력을 합니다. 그 대상이 차이가 있을 뿐이지 입,출력을 하지 않는 프로세스는 없습니다. 만약에 그런 프로세스가 있다면 존재 의미가 있을까요? 따라서 프로세스가 생성되면 기본적으로 입,출력을 위한 채널을 가지게 되는데 이것을 standard stream 이라고 하며 입력을 위한 stdin, 출력을 위한 stdout, 에러출력을 위한 stderr 가 존재합니다. 터미널을 열면 shell 프로세스의 stdin, stdout, stderr 가 모두 터미널에 연결되어 사용자로부터 입력을 받고 출력을 합니다.

unix 에서는 보통 모든 장치들을 파일로 관리한다고 합니다. standard stream 도 각각 /dev/stdin, /dev/stdout, /dev/stderr 에 파일로 존재합니다. 우리가 사용하는 파일들은 보기 좋은 이름이 있지만 실제 프로그램 내에서는 FD (file descriptor) 라는 양의 정수 번호에 의해 처리가 됩니다. 관례에 따라 stdin 은 0 번, stdout 은 1 번, stderr 는 2 번을 사용하는데 shell 에서도 redirection 을 할때 이와 같은 FD 번호를 사용합니다.

기본 사용법 과 default value

$ cat infile
hello
world

$ wc 0< infile 1> outfile
$ cat outfile 
 2  2 12

위의 wc 명령은 redirection 의 기본 사용법을 보여줍니다. 데이터 흐름의 방향을 나타내는 redirection 기호인 < (입력), > (출력), >> (출력:append) 을 가운데 두고 왼쪽에는 FD (file descriptor), 오른쪽에는 FD 나 filename 을 위치시키면 됩니다.

위의 예에서는 infile 을 FD 0 (stdin) 에 연결하여 입력으로 사용하였고 FD 1 (stdout) 을 outfile 파일에 연결하여 출력이 outfile 파일에 저장되게 하였습니다. redirection 기호를 사용할땐 이렇게 FD 번호를 적어줘야 하지만 그렇지 않을경우 standard stream 이 사용됩니다. 따라서 < 기호는 입력을 나타내므로 좌측값 0 을 기본값으로 >, >> 기호는 출력을 나타내므로 좌측값 1 을 기본값으로 합니다. 그러므로 위의 예제는 다음과 같이 작성하여도 결과가 같습니다.

$ wc < infile > outfile
$ cat outfile
 2  2 12

Redirection 기호 사용시 주의할점

< , >, >> 기호 좌측값은 공백없이 붙여야합니다. 그렇지 않으면 명령의 인수로 인식이 됩니다.

$ wc 0 < infile 1> outfile         # 0 을 wc 명령의 인수로 인식.
wc: 0: No such file or directory

$ wc 0< infile 1 > outfile         # 1 을 wc 명령의 인수로 인식.
wc: 1: No such file or directory

>, >> 기호의 우측값은 파일이름이 올경우는 괜찮지만 FD 번호가 올경우는 & 기호를 붙여줘야 합니다. 그렇지 않으면 FD 숫자가 파일이름이 됩니다.

# '>'  오른쪽에 '&' 를 붙이지 않으면 FD 번호를 파일이름으로 인식
# 에러 출력이 파일 '1' 로 가서 터미널에 메시지가 표시되지 않는다.
$ wc asdfgh  2>1     
$ cat 1               
wc: asdfgh: No such file or directory

# 이번에는 터미널에 메시지가 출력됨
$ wc asdfgh 2>&1
wc: asdfgh: No such file or directory

정리하면 이렇습니다. redirection 기호을 중심으로 왼쪽에 오는 FD 는 명령의 인수로 인식되지 않게 붙여쓰고 오른쪽에 오는 FD 는 파일로 인식되지 않게 & 를 붙여 사용하면 됩니다.

Redirection 기호의 위치

redirection 기호의 명령행상 위치는 어디에 와도 상관없습니다.

$ echo hello > /tmp/out

$ > /tmp/out echo hello

$ echo > /tmp/out hello

$ echo hello >&2

$ echo >&2 hello

$ read -r line < file   # < file 은 0< file 과 같은 것임

$ < file read -r line

$ <<END cat             # <<END 은 0<<END 과 같은 것임
hello
END

$ cat <<END
hello
END

Redirection 기호는 쓰는 순서가 중요하다!

redirection 을 할때 중요한 부분이 순서입니다. 순서가 올바르지 않으면 제대로 redirection 이 되지 않습니다. 다음은 mycomm 실행결과 FD 2 (stderr) 를 FD 1 (stdout) 으로 연결하여 둘 모두를 outfile 에 쓰려고 하지만 이중 하나는 문제가 있습니다.

$ mycomm 2>&1 > outfile     # 첫번째

$ mycomm > outfile 2>&1     # 두번째

위문장을 exec 명령으로 단계적으로 살펴보도록 하겠습니다. exec 명령은 현재 shell 을 지정한 명령으로 대체하기 위해 사용하기도 하지만 redirection 설정을 현재 shell 에 적용시키기 위해서도 사용합니다. 터미널을 열어 ls -l /proc/$$/fd 을 실행해보면 FD 0, 1, 2 모두 터미널 /dev/pts/10 에 (저같은경우) 연결돼 있는것을 볼수있습니다.

# '$$' 는 현재 shell process id 를 나타냅니다.
$ ls -l /proc/$$/fd  
total 0
lrwx------ 1 mug896 mug896 64 07.06.2015 15:04 0 -> /dev/pts/10  # stdin
lrwx------ 1 mug896 mug896 64 07.06.2015 15:04 1 -> /dev/pts/10  # stdout
lrwx------ 1 mug896 mug896 64 07.06.2015 15:04 2 -> /dev/pts/10  # stderr

exec 명령을 테스트 할때 터미널이 중지될수 있으니 조회용 터미널을 하나 더 열어서 사용하는 것이 좋습니다. 이때 shell pid 값은 exec 명령을 실행하는 터미널에서 echo $$ 로 구할 수 있습니다.

첫번째 mycomm 2>&1 > outfile

$ exec 2>&1

FD 2 번을 FD 1 번에 연결 하였습니다. 현재 1, 2 번은 모두 동일한 터미널에 연결돼 있으므로 변경사항이 없습니다. 다만 stderr 메시지가 stdout 을 통해 터미널로 표시됩니다.

$ ls -l /proc/20040/fd
total 0
lrwx------ 1 mug896 mug896 64 07.06.2015 15:56 0 -> /dev/pts/10  # stdin  
lrwx------ 1 mug896 mug896 64 07.06.2015 15:56 1 -> /dev/pts/10  # stdout
lrwx------ 1 mug896 mug896 64 07.06.2015 15:56 2 -> /dev/pts/10  # stdout 으로 변경

$ exec 1> outfile

FD 1 번을 outfile 로 연결하여 1 번 연결이 변경되었습니다. redirection 처리후에 FD 1 번만 outfile 로 연결된것을 볼 수 있습니다. 그러므로 첫번째 명령은 FD 1 번만 outfile 로 출력되고 FD 2 번은 stdout 을 통해 터미널로 출력 됩니다.

$ ls -l /proc/20040/fd
total 0
lrwx------ 1 mug896 mug896 64 07.06.2015 15:56 0 -> /dev/pts/10  # stdin
l-wx------ 1 mug896 mug896 64 07.06.2015 15:56 1 -> /home/mug896/outfile
lrwx------ 1 mug896 mug896 64 07.06.2015 15:56 2 -> /dev/pts/10  # stdout

오류메시지는 stdout 을 통해 터미널로 출력되므로 다음과 같이 하면 errfile 로 저장할 수 있습니다.

# { ;} 를 사용해야 합니다 그렇지 않으면 outfile 내용이 errfile 로 가게 됩니다.
{ mycomm 2>&1 > outfile ;} > errfile

------------------------------------

$ cat x
11111111

$ cat xx
22222222

$ echo 33333333 >> x >> xx

$ cat x
11111111

$ cat xx
22222222
33333333

두번째 mycomm > outfile 2>&1

$ exec 1> outfile

FD 1 번을 outfile 에 연결하여 1 번 연결이 변경되었습니다.

$ ls -l /proc/19779/fd
total 0
lrwx------ 1 mug896 mug896 64 07.06.2015 15:33 0 -> /dev/pts/11
l-wx------ 1 mug896 mug896 64 07.06.2015 15:33 1 -> /home/mug896/tmp/outfile
lrwx------ 1 mug896 mug896 64 07.06.2015 15:33 2 -> /dev/pts/11

$ exec 2>&1

FD 2 번을 FD 1 번에 연결하였습니다. 현재 1 번은 outfile 에 연결된 상태이므로 2 번도 따라서 outfile 로 연결됩니다. redirection 처리후에 FD 1, 2 모두 outfile 로 연결된 것을 볼 수 있습니다. 그러므로 두번째 명령은 FD 1, 2 둘다 outfile 로 출력되게 됩니다.

$ ls -l /proc/19779/fd
total 0
lrwx------ 1 mug896 mug896 64 07.06.2015 15:33 0 -> /dev/pts/11 # stdin
l-wx------ 1 mug896 mug896 64 07.06.2015 15:33 1 -> /home/mug896/tmp/outfile
l-wx------ 1 mug896 mug896 64 07.06.2015 15:33 2 -> /home/mug896/tmp/outfile

bash 에서는 다음과 같이 편리하게 사용할 수 있는 단축형 메타문자를 제공합니다.
mycomm > outfile 2>&1 을 줄여서 mycomm &> outfile
mycomm >> outfile 2>&1 을 줄여서 mycomm &>> outfile
mycomm1 2>&1 | mycomm2 을 줄여서 mycomm1 |& mycomm2

$ ( 3> out 2>&1 1>&3 date -@ ) 1> err

다음은 date -@ 명령의 오류 메시지를 2> err 로 받지 않고 1> err 로 받고 있습니다.
좀 복잡해 보일 수 있는데 단계별로 따라가 보면 어렵지 않습니다.

3> out           ( FD 1 : stdout, FD 2 : stderr, FD 3 : out )

FD 3번을 생성하고 out 파일에 연결합니다. 이 시점에서 FD 3번으로 출력하면 out 파일에 쓰여지겠죠.

2>&1            ( FD 1 : stdout, FD 2 : stdout, FD 3 : out )

FD 2번을 1번에 연결하는데 FD 1, 2 번은 현재 모두 터미널에 연결돼 있으므로 출력이 터미널로 가는 것은 바뀌지 않습니다. 하지만 오류메시지가 기존의 FD 2 번을 통해서 터미널로 가던 것이 이제는 FD 1 번( stdout )을 통해서 터미널로 가게 됩니다.

1>&3            ( FD 1 : out, FD 2 : stdout, FD 3 : out )

FD 3번은 현재 out 파일에 연결되어 있으므로 FD 1 번도 따라서 out 파일로 연결됩니다.

최종적으로 이 상태에서 date 명령의 정상 출력은 FD 1 번을 통해 out 파일로 쓰여지게 되고 에러 출력은 기존과 같이 터미널로 출력이 되지만 stderr 가 아니라 stdout 을 통해서 출력이 됩니다. 따라서 subshell 외부에서 에러 메시지를 잡기 위해서는 2> err 가 아니라 1> err 로 설정을 해야 합니다.

Script 작성시 redirection 의 활용

스크립트 실행 시에 FD 0, 1, 2 번은 연결이 터미널에서 파일이나 파이프로 변경될 수 있으므로 사용하기전 백업과 복구 과정을 거쳐야 합니다. 백업 FD 를 삭제할 때는 간단하게 복구 과정에서 뒤에 - 기호를 붙이면 됩니다.

                    # FD 3 번을 새로 생성하면서 1번에 연결하였습니다. 
exec 3>&1           # 그러므로 1번 연결이 3번에 백업된 것과 같습니다.

exec 1> outfile     # FD 1 번을 outfile 에 연결해 사용
...
...
exec 1>&3-          # FD 1 번을 백업해 두었던 3 번에 다시 연결하고 3 번은 삭제 (3-)

스크립트 실행시 stdin 을 redirect 하여 사용후 복구하는 과정입니다.

!/bin/bash

exec 3>&0              # FD 0 번을 사용하기 전에 3 번에 백업
exec 0< infile         # infile 을 FD 0 에 연결 ( 실행하려면 infile 을 준비하세요 )

read var1              # 2 개의 라인을 읽음.
read var2     

echo read from infile: $var1
echo read from infile: $var2

exec 0>&3-              # FD 0 번을 3 번 으로부터 복구하고 3 번은 삭제 (3-)

# 프롬프트가 보이면 정상적으로 실행된 것입니다.
read -p "enter your favorite number : " var
echo $var

스크립트 실행시 stdout 을 redirect 하여 사용후 복구하는 과정입니다.

#!/bin/bash

echo start--------------

exec 3>&1           # FD 1 번 백업

# FD 1 을 outfile 로 연결. 이제 모든 메시지는 outfile 로 저장됨
exec 1> outfile

echo this message is going to outfile

exec 1>&3-          # FD 1 번 복구

# 마지막 메시지가 보이면 정상적으로 실행된 것입니다.
echo end----------------

다음은 스크립트 실행시 stdin, stdout 을 redirect 하여 사용후 복구하는 과정입니다.

#!/bin/bash

echo start -----------------

exec 3>&0 4>&1        # FD 0, 1 번 백업
exec 0< infile        # ( 실행하려면 infile 파일을 준비하세요 )
exec 1> outfile

# stdin(0) 으로 입력을 받아서 'tr' 처리후 stdout(1) 으로 출력.
# 현재 redirection 을 한 상태이므로 infile 에서 읽어들여 outfile 로 출력하게 됩니다.

cat - | tr a-z A-Z     


exec 0>&3- 1>&4-       # FD 0, 1 번 복구

# 다음 마지막 메시지가 보이고 outfile 파일 내용이 모두 대문자로 변경 돼 있으면
# 정상적으로 실행된 것입니다.

echo end -----------------

Subshell 에서 실행되는 파이프를 피해 redirection 을 이용해 while 로 파일을 처리하는 예입니다.

#!/bin/bash

exec 3>&0
exec 0< infile

lines=0
while read -r line        # 기본적으로 stdin 에서 읽으므로
do
    echo "$line"
    (( lines++ ))
done 

exec 0>&3-

echo number of lines read : $lines

----------------------------------
#!/bin/bash

exec 3< infile

lines=0
while read -r line 
do
    echo "$line"
    (( lines++ ))
done <& 3                  # <&3 은 0<&3 과 같은 것임

exec 3>&-

echo number of lines read : $lines

----------------------------------
#!/bin/bash

exec 3< infile

lines=0
while read -r line <& 3    # <&3 는 파일 포지션이 사용되므로 여기에 놓고 사용해도 된다. 
do
    echo "$line"
    (( lines++ ))
done

exec 3>&-

echo number of lines read : $lines

동일한 파일을 입, 출력에 사용하면?

하나의 파일을 redirection 을 이용해 동시에 입, 출력에 사용하는 것은 두개의 FD 를 열어서 각각의 파일포지션을 이용하는것과 같습니다.

입, 출력에 쓰일 파일 x 의 내용

$ cat x
0000000000000000
1111111111111111

파일 x 로부터 입력을 받고 동일한 파일로 출력을 하여 append 할경우.
순서대로 ls 명령의 출력이 x 파일에 append 되고 다음에 sed 명령에서는 append 된 내용이 함께 입력으로 사용되는걸 볼 수 있습니다. (0 이 모두 X 로 바뀜)

$ { { ls -l /dev/fd/; sed 's/0/X/g' ;} >> x ;} < x

$ cat x
0000000000000000
1111111111111111
total 0
lr-x------ 1 mug896 mug896 64 08.03.2015 10:06 0 -> /home/mug896/tmp/3/x
l-wx------ 1 mug896 mug896 64 08.03.2015 10:06 1 -> /home/mug896/tmp/3/x
lrwx------ 1 mug896 mug896 64 08.03.2015 10:06 2 -> /dev/pts/8
lr-x------ 1 mug896 mug896 64 08.03.2015 10:06 3 -> /proc/15885/fd/
XXXXXXXXXXXXXXXX    # 0 이 모두 X 로 바뀜
1111111111111111
total X
lr-x------ 1 mug896 mug896 64 X8.X3.2X15 1X:X6 X -> /home/mug896/tmp/3/x
l-wx------ 1 mug896 mug896 64 X8.X3.2X15 1X:X6 1 -> /home/mug896/tmp/3/x
lrwx------ 1 mug896 mug896 64 X8.X3.2X15 1X:X6 2 -> /dev/pts/8
lr-x------ 1 mug896 mug896 64 X8.X3.2X15 1X:X6 3 -> /proc/15885/fd/

이번에는 명령 실행전에 파일 x 를 삭제합니다. 파일 x 가 먼저 삭제되었기 때문에 ls 명령의 출력이 파일 제일 위에 위치 하게됩니다. 그리고 여기서 눈여겨 볼점은 sed 명령에서 입력으로 사용되는 데이터가 ls 명령의 출력이 아닌 이전 x 파일의 내용 이라는 점입니다.

$ { rm -f x; { ls -l /dev/fd/; sed 's/0/X/g' ;} >> x ;} < x

$ cat x
total 0
lr-x------ 1 mug896 mug896 64 08.03.2015 10:08 0 -> /home/mug896/tmp/3/x (deleted)
l-wx------ 1 mug896 mug896 64 08.03.2015 10:08 1 -> /home/mug896/tmp/3/x
lrwx------ 1 mug896 mug896 64 08.03.2015 10:08 2 -> /dev/pts/8
lr-x------ 1 mug896 mug896 64 08.03.2015 10:08 3 -> /proc/15951/fd/
XXXXXXXXXXXXXXXX
1111111111111111

그러므로 다음과 같이 하면 파일 x 의 내용을 sed 명령으로 바꾸어 저장할 수 있습니다. 결과적으로 sed -i 's/0/X/g' x 한 결과와 같습니다만 이 방법은 sed -i 와 달리 실행중에 임시파일을 만들지 않습니다. 그러므로 사이즈가 큰 파일을 처리중에 문제가 발생하여 프로세스가 중단된다면 처리되지 않은 나머지 뒷부분은 모두 잃게 됩니다.

$ { rm -f x;  sed 's/0/X/g'  > x ;} < x

$ cat x
XXXXXXXXXXXXXXXX
1111111111111111

Redirection 우선순위

{ ;} 은 명령 grouping 외에 메타문자 우선순위 조절에도 사용됩니다. redirection 기호도 shell 메타문자 중에 하나로 { ;} 를 이용하면 우선순위를 조절할 수 있습니다. 아래와 같은 명령이 실행될 경우 제일 바깥쪽 메타문자가 먼저 실행되고 차례로 안쪽에 있는 메타문자가 실행됩니다.

$ { command  2번 ;} 1
# FD 3 번이 생성되어있지 않아 오류 발생.
$ date >&3        
bash: 3: Bad file descriptor

$ date >&3 3>&1
bash: 3: Bad file descriptor

# 3>&1 가 먼저 실행되므로 FD 3 번이 생성되어 오류가 발생하지 않는다.
$ { date >&3 ;} 3>&1
Sat Aug  8 03:31:05 KST 2015

앞선 예제 { rm -f x; sed 's/0/X/g' > x ;} < x 에서 파일 x 가 삭제되었음에도 불구하고 기존의 데이터를 읽어들일수 있었던 것도 외부에 있는 < x 가 먼저 실행되어 open 되었기 때문입니다. 사실 내부에 있는 x 는 삭제되어 다시 생성된 파일로 외부 x 와는 inode 번호가 다릅니다.

$ stat -c %i x; { rm -f x;  sed 's/0/X/g' > x ;} < x; stat -c %i x 
1709909
1709910

만약에 두 메타문자의 위치를 바꾸어 실행한다면 ?
먼저 > x 가 실행될 것이므로 x 파일 내용이 전부 삭제되겠죠
그다음 < x 이 실행되는데 읽어들일 내용이 없습니다.
다음 rm 명령이 실행되어 x 파일이 삭제됩니다.

$ { { rm -f x; sed 's/0/X/g' ;}  < x ;} > x
$ cat x
cat: x: No such file or directory

함수를 정의할때 redirection 의 사용

함수를 정의할때 redirection 을 사용하면 함께 정의가 됩니다. 다음 color 함수는 명령 실행결과 stderr 로 출력되는 메시지를 stdout 으로 출력되는 메시지와 분류하여 빨간색으로 표시합니다.

color() ( 
    set -o pipefail; 
    "$@" 2>&1 1>&3 | sed $'s/.*/\e[31m & \e[m/' >&2 
) 3>&1

# 1. 함수를 정의할때 { ;} 대신에 ( ) subshell 을 사용한것은 
#    pipefail 옵션설정을 함수에 국한하기 위해서 입니다.
# 2. "$@" 는 color 함수명 뒤에 오는 인수들을 명령문으로 실행합니다.
# 3. "$@" 명령은 '|' 파이프 왼쪽에 위치하므로 stdout 이 파이프가 됩니다.
#    2>&1 : stderr 를 stdout (파이프) 로 redirect 했으므로 파이프를 통해 sed 입력으로 들어갑니다.
#    1>&3 : 현재 연결이 파이프인 stdout 을 fd 3번 ( 함수밖에서 제일먼저 터미널(stdout)로 연결했죠 )
#    으로 연결하므로 sed 입력으로 들어가지 않고 바로 터미널로 출력됩니다.
# 4. sed 입력으로 들어간 stderr 메시지는 color escape 문자처리가 되어 출력됩니다.
#    '\e' escape 문자적용을 위해 $' ' quotes 이 사용되었습니다.
#    & 문자는 앞에 매칭된 라인으로 치환됩니다.

테스트 해보기

f1() {
    echo 111;
    echo ERR >&2;
}

$ color f1
111     # stdout 은 흰색으로 출력
ERR     # stderr 는 빨간색으로 출력

$ color date -%Y
date: invalid option -- '%'     # 빨간색으로 출력

문제점 : 명령의 출력이 "정상메시지; 에러메시지; 정상메시지; 에러메시지;" 순서로 출력될경우 파이프로 인해 메시지 순서가 유지되지 않고 "정상메시지; 정상메시지; 에러메시지; 에러메시지;" 로 출력

Redirection 적용 범위

parent process 에 설정된 file descriptor 들은 기본적으로 child process 에게 상속됩니다. 따라서 다음과 같이 while 문의 stdin 을 /dev/null 로 연결할 경우 이 상태는 t.sh 스크립트가 실행될 때도 유지가 됩니다.

------ t.sh ------
#!/bin/bash

ls -l /dev/fd/
------------------

f1() { ./t.sh ;}

while :; do 
    f1
    break
done < /dev/null

###########  실행결과 ############
# t.sh 스크립트 에서도 stdin 이 /dev/null 로 연결된 것을 볼 수 있습니다.

lr-x------ 1 mug896 mug896 64 2017-01-10 09:13 0 -> /dev/null 
lrwx------ 1 mug896 mug896 64 2017-01-10 09:13 1 -> /dev/pts/9
lrwx------ 1 mug896 mug896 64 2017-01-10 09:13 2 -> /dev/pts/9

Redirection 은 명령이 실행되기 전에 처리된다.

명령문에 redirection 이 포함될 경우 명령 시작 전에 먼저 redirection 이 처리되어야 올바르게 명령 출력이 되겠죠. 이것은 다음과 같은 명령을 실행하는데 있어서 문제가 될 수 있습니다.

datafile 에서 앞의 100 라인만 남기고 삭제하려고 하지만 실행해보면 파일 사이즈가 0 이 되는 것을 볼 수 있습니다. head 명령이 실행되기 전에 > 이 처리되어 파일의 내용이 모두 삭제되었기 때문입니다.

# datafile 은 같은 파일명
$ head -n100 datafile > datafile

다음은 명령이 실행되기 전에 shell 에의해 처리되는 명령 치환, 프로세스 치환, here string 과 redirection 이 함께 존재할 경우 처리되는 우선순위를 알아보겠습니다.

$ seq 5 > file
$ cat file
1
2
3
4
5
# echo 명령이 실행되기 전에 명령치환, redirection 이 처리되지만
# 명령치환이 redirection 보다 우선해 처리되는 것을 알 수 있습니다.
$ echo "$( head -n3 file )" > file
$ cat file
1
2
3
----------------------------------

$ seq 5 > file
# 프로세스 치환 보다는 redirection 이 먼저 처리됩니다.
# 파일 내용이 삭제되어 값이 표시되지 않는걸 볼 수 있습니다.
$ cat <( head -n3 file ) > file
$ cat file 
no output
---------------------------------

$ seq 5 > file
# 같은 redirection 끼리는 왼쪽에서 부터 차례로 실행.
# 만약에 위치를 바꾸면 파일 내용이 먼저 삭제되어 값이 표시되지 않습니다.
$ cat <<< $( head -n3 file ) > file
$ cat file
1
2
3

Quiz

FD 1번 stdout 과 FD 2번 stderr 를 서로 바꾸기

# 초기 FD 상태
lrwx------ 1 mug896 mug896 64 07.06.2015 15:04 0 -> /dev/pts/10  # stdin
lrwx------ 1 mug896 mug896 64 07.06.2015 15:04 1 -> /dev/pts/10  # stdout
lrwx------ 1 mug896 mug896 64 07.06.2015 15:04 2 -> /dev/pts/10  # stderr

# 먼저 FD 1 을 FD 3 으로 백업
$ exec 3>&1

0 -> /dev/pts/10  # stdin
1 -> /dev/pts/10  # stdout
2 -> /dev/pts/10  # stderr
3 -> /dev/pts/10  # stdout 생성

# FD 1 을 FD 2 에 연결
$ exec 1>&2

0 -> /dev/pts/10  # stdin
1 -> /dev/pts/10  # stderr 변경
2 -> /dev/pts/10  # stderr
3 -> /dev/pts/10  # stdout

# FD 2 을 백업해둔 FD 3 에 연결
$ exec 2>&3

0 -> /dev/pts/10  # stdin
1 -> /dev/pts/10  # stderr
2 -> /dev/pts/10  # stdout 변경
3 -> /dev/pts/10  # stdout

# FD 3 삭제
$ exec 3>&-

0 -> /dev/pts/10  # stdin
1 -> /dev/pts/10  # stderr
2 -> /dev/pts/10  # stdout

################# 테스트 #################

$ ( 3>&1 1>&2 2>&3 3>&- date ) 2> out

$ cat out
Sat Aug  1 15:51:45 KST 2015

$ ( 3>&1 1>&2 2>&3 3>&- date -@ ) 1> out

$ cat out
date: invalid option -- '@'
Try 'date --help' for more information.

2.

set -o pipefile 옵션을 이용하면 파이프에 연결된 명령들 중에서 하나라도 실패가 생길 경우 비정상 종료 상태 값을 구할 수 있습니다. 하지만 sh 에서는 pipefail 옵션을 사용할 수 없는데요. sh 에서 파이프에 연결된 명령의 종료 상태 값을 구하려면 어떻게 할까요?

#!/bin/sh

# 1. "date" 명령은 파이프에 연결되어 있으므로 출력이 sed 명령으로 전달됩니다.
# 2. echo $? >&4 에 의해 종료 상태 값이 명령치환 프로세스의 FD 4 로 전달되고 
# 3. FD 4 번은 exec 4>&1 에의해 stdout 으로 연결되어 있으므로 status 변수로 입력됩니다.
# 4. sed 명령의 stdout 출력은 명령치환 프로세스의 exec 1>&3 에의해 FD 3 번으로 전달되고 
# 5. 다시 FD 3 번은 현재 shell 프로세스의 exec 3>&1 에의해 stdout 으로 출력되게 됩니다.

exec 3>&1                                          # 현재 shell 프로세스
status=$( exec 4>&1 1>&3                           # 명령치환 subshell 프로세스
    { date; echo $? >&4 ;} | sed 's/.*/XXX&XXX/'   # 파이프 subshell 프로세스
)
exec 3>&-

echo "YYY ${status} ZZZ"
------------------------------------------------

# date 명령이 정상 종료할 경우
$ ./test.sh 
XXXWed Jun 20 22:04:08 KST 2018XXX        # sed 명령의 출력
YYY 0 ZZZ                                 # date 명령의 종료 상태 값

# 오류를 위해 date 명령을 "date -@" 로 변경하여 실행
$ ./test.sh 
date: invalid option -- '@'
Try 'date --help' for more information.
YYY 1 ZZZ                                 # date 명령의 종료 상태 값

만약에 FD 3 번 처리 부분을 삭제하면 sed 명령의 출력도 모두 status 변수로 입력되게 됩니다.

#!/bin/sh

status=$( exec 4>&1
    { date; echo $? >&4 ;} | sed 's/.*/XXX&XXX/'
)

echo "YYY ${status} ZZZ"

$ ./test.sh
YYY 0
XXXWed Jun 20 22:16:05 KST 2018XXX ZZZ