Process Substitution

<( <COMMANDS> )
>( <COMMANDS> )

프로세스 치환은 표현식 내의 명령문이 background 프로세스로 실행되고 파이프를 통해 데이터가 전달됩니다. 명령의 인수로 파일을 사용하는 곳에서 사용될 수 있으나 일반적인 파일과는 다르므로 프로그램 내에서 파일 타입을 직접 체킹하는 경우 실행되지 않을수도 있습니다. 파일을 읽거나 쓰기를 할때 random access 를 할 수 없으므로 한번에 처음부터 끝까지 읽거나 써야합니다. 파이프가 가지는 방향성을 > < 문자를 이용해 표시하는 named pipe 파일이라고 생각하면 됩니다.

프로세스 치환은 sh 에서는 사용할 수 없습니다.

# '>( )' 표현식 내의 명령은 subshell 에서 실행되므로 '$$' 값이 같게나온다.

$ { echo '$$' : $$ >&2 ;} > >( echo '$$' : $$ )
$$ : 504
$$ : 504

# 하지만 '$BASHPID' 는 다르게 나온다.

$ { echo '$BASHPID' : $BASHPID >&2 ;} > >( echo '$BASHPID' : $BASHPID )
$BASHPID : 504
$BASHPID : 22037


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

$ ls -l <( : ) 
lr-x------ 1 mug896 mug896 64 02.07.2015 22:29 /dev/fd/63 -> pipe:[681827]

$ [ -f <( : ) ]; echo $?  # 일반 파일인지 테스트
1
$ [ -p <( : ) ]; echo $?  # pipe 인지 테스트
0

sleep 명령은 현재 shell 에서 실행되고 표현식 내의 cat 명령은 subshell 에서 실행되는 것을 볼 수 있습니다.

{ sleep 10 ;} > >( cat )

process substitution

현재 shell 과 표현식 에서의 FD

command1 > >( command2 ) 명령의 경우 command1 의 stdout 이 command2 의 stdin 과 연결되며 command1 < <( command2 ) 명령의 경우는 command2 의 stdout 이 command1 의 stdin 과 연결됩니다.
현재 shell pid 를 나타내는 $$ 변수는 subshell 에서도 동일한 값을 가지므로 >( ) 표현식 내에서의 FD 상태를 보기 위해서는 $BASHPID 변수를 이용해야 합니다.

>( . . . )

현재 shell 의 stdout 이 파이프에 연결되어 출력되고 표현식 내의 subshell 에서는 stdin 이 파이프에 연결되어 입력을 받고 있습니다.

개별적으로 명령을 실행하여 파이프 번호가 다르게 나오지만 실제 명령이 실행될 때는 같게 됩니다.

<( . . . )

표현식 내의 subshell 은 stdout 이 파이프에 연결되어 출력되고 현재 shell 에서는 stdin 이 파이프에 연결되어 입력을 받고 있습니다.

개별적으로 명령을 실행하여 파이프 번호가 다르게 나오지만 실제 명령이 실행될 때는 같게 됩니다.

사용예 )

프로세스 치환을 사용하는 이유는 임시 파일을 만들지 않아도 된다는 점입니다. 가령 ulimit 명령의 soft limit 과 hard limit 출력값을 서로 비교한다면 아래와 같이 명령 실행 결과를 임시파일로 만든후 비교해야 합니다. 하지만 프로세스 치환을 이용하면 내부적으로 파이프를 이용해 처리하기 때문에 임시파일을 만들 필요가 없습니다.

$ ulimit -Sa > ulimit.Sa.out

$ ulimit -Ha > ulimit.Ha.out

$ diff ulimit.Sa.out ulimit.Ha.out

프로세스 치환을 사용해 비교

# 임시파일을 만들 필요가 없다

$ diff <( ulimit -Sa ) <( ulimit -Ha )   
1c1
< core file size          (blocks, -c) 0
---
> core file size          (blocks, -c) unlimited
8c8
< open files                      (-n) 1024
---
> open files                      (-n) 65536
12c12
< stack size              (kbytes, -s) 8192
---
> stack size              (kbytes, -s) unlimited

위의 프로세스 치환을 이용한 비교는 다음과 동일하다고 볼 수 있습니다.

 mkfifo fifo1
 mkfifo fifo2
 ulimit -Sa > fifo1 &
 ulimit -Ha > fifo2 &
 diff fifo1 fifo2
 rm fifo1 fifo2
$ echo hello > >( wc )
      1       1       6

$ wc < <( echo hello )
      1       1       6

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

# 입력과 출력용 프로세스 치환을 동시에 사용
$ f1() {
    cat "$1" > "$2"
}

$ f1 <( echo 'hi there' ) >( tr a-z A-Z )
HI THERE

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

# --log-file 옵션 값으로 입력 프로세스 치환이 사용됨
$ rsync -avH --log-file=>(grep -Fv .tmp > log.txt) src/ host::dst/

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

# tee 명령을 이용해 결과를 4개의 입력 프로세스 치환으로 전달하여 처리
$ ps -ef | tee >(grep tom > toms-procs.txt) \
               >(grep root > roots-procs.txt) \
               >(grep -v httpd > not-apache-procs.txt) \
               >(sed 1d | awk '{print $2}' > pids-only.txt)

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

# dd 명령에서 입력 파일로 사용
dd if=<( cat /dev/urandom | tr -dc A-Z ) of=outfile bs=4096 count=1

스크립트 작성 시에 명령 실행 결과를 받아서 처리하고자 할때 파이프를 사용하는데요. 파이프는 연결된 모든 명령들이 각자의 subshell 에서 실행되어 parent 변수에 연산 결과를 저장할 수가 없습니다. 이때 프로세스 치환을 이용하면 문제를 해결할 수 있습니다.

i=0
sort list.txt | while read -r line; do
  (( i++ ))
  ...
done

echo "$i lines processed"  

# 파이프로 인해 parent 변수 i 에 값을 설정할수 없어 항상 0 이 표시된다.
0 lines processed

------------------------------------
i=0
while read -r line; do
  (( i++ ))
  ...
done < <(sort list.txt)

echo "$i lines processed"   

# 프로세스 치환을 이용해 while 문이 현재 shell 에서 실행되어 i 값을 설정할수 있다.
12 lines processed

명령 실행 결과 stderr 만 전달하려고 할 때 프로세스 치환을 이용하면 쉽게 할 수 있습니다.

$ command 2> >( command ... )

# 파이프를 이용할 경우
$ command 2>&1 > /dev/null | command ...

# cmd1 은 stdout 을 처리, cmd2 는 stderr 를 처리
$ command > >( cmd1 ) 2> >( cmd2 )

표현식 내 명령은 background 로 실행됩니다.

다음을 보시면 exec 명령 실행 후에 tr 명령이 subshell 에서 background 로 실행되고 있는 것을 볼 수 있습니다.

따라서 다음 첫 번째 명령을 파이프를 이용해 나타내면 두 번째와 같게 되므로 실행을 하면 오류가 발생합니다.

$ ls > >( vi - )    # 실행시 오류 발생

$ ls | { vi - & }   

$ ls | vi -         # 정상적으로 실행

main 프로세스와 PGID 가 다르다.

위의 그림은 프롬프트 에서 join <(sleep 10) <(sleep 10) 명령을 실행했을 때의 ps 를 나타내는데요. 파이프로 명령을 실행했을 때는 연결된 명령들이 같은 PGID 를 갖는데 반해 프로세스 치환은 main 명령에 해당하는 join 과 PGID 가 다른 것을 볼 수 있습니다. 따라서 다음과 같이 시간이 소요되는 명령을 실행 중에 Ctrl-c 로 종료를 시도한다면 join 프로세스는 바로 종료되겠지만 large_data 파일을 처리중인 sort 는 종료되지 않고 남아있게 됩니다.

join -t, -1 1 -2 3 <(sort -t, -k1,1 keys) <(sort -t, -k3,3 large_data)

종료 동기화 하기

프로세스 치환에서 사용되는 명령은 background 로 실행됩니다. 그러므로 실행시간이 오래 걸릴경우 main 프로세스가 먼저 종료할 수 있습니다. 이와같은 경우 모든 명령이 종료된 후에 main 프로세스가 종료하기를 원한다면 다음과 같은 동기화 방법을 사용할 수 있습니다.

#!/bin/bash

sync1=`mktemp`
sync2=`mktemp`

# 여기서 subshell ( ) 을 사용한것은  >( while read -r line ... ) 명령이 child process 가 되어 
# 종료되지 않고 계속해서 read 대기상태가 되는 것을 방지하기 위해서입니다.
( while read -r line; do
    case $line in
        aaa* ) echo "$line" >& $fd1 ;;
        bbb* ) echo "$line" >& $fd2 ;;
    esac
done ) \
    {fd1}> >( while read -r line; do echo "$line" | sed -e 's/x/y/g'; sleep 1; done; \
            rm "$sync1" ) \
    {fd2}> >( while read -r line; do echo "$line" | sed -e 's/x/z/g'; sleep 2; done; \
            rm "$sync2" ) \
    < <( for ((i=0; i<4; i++)); do echo aaaxxx; echo bbbxxx; done; echo ooops );

echo --- end 1 ---
while [ -e "$sync1" -o -e "$sync2" ]; do sleep 1; done
echo --- end 2 ---