Process Substitution(Subshell)

이 전 글에선 Here Decuments 에 관한 내용을 정리했다.
여기선 비슷한 개념의 Redirection 인, Process Substitution 에 대해 적어본다.
지난 글과 마찬가지로, 간단한 정리는 아래, ‘내 나름대로 정리’를 참고하라.

내가 정리한 모든 내용에 대해서, 다음 글에서 아주 간단하고 명료한 설명을 볼 수 있다.

Stack Exchange : pipe and stdin redirection to cat


문자열 abcd 를 cat 으로 넘기려면?? 지금껏 알고 있는 방법은 이거다.

echo "abcd" | cat

파이프(|)를 사용하면 앞의 Std. Output 을 뒤의 Std. Input 으로 넘긴다.
여기까진, 초보인 나도 알고, 많이 쓰고 있다.

그런데, cat 을 앞에 쓰고 싶다면? 이렇게 되면 일이 복잡해진다.
얕은 지식을 써서 이렇게 해봤다.

cat < echo "abcd"

결과로, 가벼운 코웃음와 함께 오류(echo 라는 파일을 찾을 수 없음)가 되돌아온다.

cat 은 인수로 파일과 Std. Input 을 받아들인다. 따라서 다음 두 명령은 같은 결과를 보여준다.

cat efgh.txt
cat < efgh.txt

따라서, cat echo "abcd" 라고 하면, ‘echo’ 를 파일로 인식해서 그 파일로 부터 입력을 받으려 한다. 그러나 그런 파일은 없기에 오류가 발생한다.

더 머리를 굴려서 이렇게 바꿔보면? 결과는 마찬가지다.

$ cat < $(echo "aoeu")
bash: $(echo "aoeu"): 그런 파일이나 디렉터리가 없습니다

$(echo “aoeu”) 는 스크립트에서 이 명령을 실행하고 Std. Output 으로 출력하라는 뜻인데, 그렇게 해봐야 cat 이 제대로 이해하지 못함은 같다.

다음도 마찬가지.

$ cat < "aoeu"
bash: "aoeu" 그런 파일이나 디렉터리가 없습니다

즉, Pipeline 을 이용하지 않고는 방법이 없다.(??)

** 꼭 그렇지만은 않다. Here String 을 쓰면 간단하게 해결된다.

cat <<< "aoeu"
또는,
cat <<< $(echo "aoeu")

위 명령을 내리면, aoeu 가 출력된다.
Here String/Document 에 대해선 이 전 글을 참고하라.
간단하게 정리하자면 다음과 같다.

< : 일반 Redirection
<< : Here Documents Redirection
<<< : Here String Redirection

이를 위해서 Process Substitution 이라는 방법이 생겨났다. The Linux Command Line(1st ed. p425) 에서는 ‘to work around’ 라는 표현을 썼다.

cat echo "abcd" 가 사용자 생각대로 작동하게 하기 위해서는, ‘echo “abcd”‘ 의 결과가 ‘파일’로 저장되어 있으면 된다. 즉, echo "abcd" 를 먼저 실행하고, 결과값을 어딘가에 파일로 저장해놓고, cat 이 그 파일을 불러오게 하면 모든 문제는 해결이 된다.

위와 같은 가정을 현실화 하기 위한 문법은 다음과 같다.

cat < <(echo "abcd")

마치 < 가 띄어쓰기로 쓰여진 듯 하지만, 앞의 < 와 뒤의 < 는 별 관계가 없다.
<(command) 형식을 쓰면, command 의 결과를 특정 파일처럼 취급할 수 있게 된다.
이건 다음 명령으로 확인해볼 수 있다.

$ echo <(echo "abcd")
/dev/fd/63

<(echo “abcd”) 의 정체는 /dev/fd/63 으로 드러났다. 따라서 저 명령은 (내부로는) cat /dev/fd/63 을 사용함과 동일하다. (그러나 실제로 이 명령을 내리면 오류가 발생한다. 저 파일은 <(command> 가 실행되고 바로 사라지는 듯 하다. 계속 존재하진 않는다.)

이걸 Process Substitution 이라고 한다.
프로세스를 대체한다? 뭘 바꿔친다는 뜻인지 이해하기가 좀 어렵다.
이에 대한 설명은, 나에게 리눅스에 대한 깊은 이해를 준 책, The Linux Command Line 1st Ed. p424 에 나와있다.

이를 위해서 다음과 같은 예를 들고 있는데,

echo "foo" | read
echo $REPLY
(출력없음)

read 도 파일이나 Std. Input 을 인자로 받는 명령이다. 여기서 read 는 문자열 “foo” 를 받고, 변수에 저장을 하는데, 위 명령의 read 에서 변수를 특정하지 않았으므로, 기본값인 $REPLY 에 저장이 된다.
그리고 그 결과는 빈공간이다.

**
이 부분에서 bash 와 zsh 가 확연한 차이를 보여준다. zsh 에선 foo 가 그대로 출력이 된다. 위 설명은 bash 기준이다.
물론, zsh 에서도 Process Substitution 은 잘 작동한다.

왜 빈공간인가?
Bash 에선 Pipeline 이 실행될 때 Subshell 로 실행이 된다고 한다. 이건 ‘서브루틴에서 지역변수의 생존’과 같은 맥락이다. 즉, Subshell 에서 할당된 변수가 서브셸이 끝남과 동시에 사라져버리기 때문에, $REPLY 에는 아무 값이 없는게 당연한 이치다.

이 Process Substitution 은 read 와 while 의 조합에서 많이 사용한다.

#!/bin/bash
# pro-sub: demo of process substitution
while read attr links owner group size date time filename; do
cat <<- EOF
    Filename: $filename
    Size: $size
    Owner: $owner
    Group: $group
    Modified: $date $time
    Links: $links
    Attributes: $attr
EOF
done < <(ls -l | tail -n +2)

몇 줄 안되는 위 코드를 이해하고자, 이 글과 이 이전 글까지 쓰게 됐다.

while read 문은 다음과 같은 형식을 취한다.

while read 변수...; do
명령...
done < 입력파일

read 는 밑천(입력파일)이 떨어질 때까지 위 작업을 반복한다.
아래는 좀 더 실제 사례.

while read distro version release; do
    printf "Distro: %s\tVersion: %s\tReleased: %s\n" \
        "$distro" \
        "$version" \
        "$release"
done < distros.txt

위에서 read 는 파일 또는 Std. Input 를 인수로 취한다고 했다.
read 는, 파일을 행별로 읽어온 뒤, 구분자(공백) 별로 나누어 각각 변수에 할당한다. 따라서, distros.txt 로부터 한 행씩 읽어온 뒤, 첫번째 컬럼 내용을 distro($distro) 에, 나머지를 각각 정해진 변수에 할당하게 된다.
간단히 생각해서, csv 파일을 읽어와서 자동으로 변수화 하는 작업을 해준다. (정말 csv 를 읽으려면, IFS=’,’ 를 할당해서 작업을 수행해야 한다.)

이 큰 틀을 이해하고, 위의 복잡한(?) 명령으로 다시 넘어가보자.
while read attr links owner group size date time filename; do 로 각 변수에 값을 할당한다.
입력 파일은? <(ls -l | tail -n +2) 이다. 이게 뭔 소린지 이해가 되지 않으면, 이 글을 다시 읽어보고, 그래도 안되면, 내가 글을 잘 쓰지 못한 탓을 할 수 밖에.
이걸 설명하려고 이렇게 장황한 글을 쓸 수 밖에 없었다.

while 문 아래에 있는 cat 이하 명령도, 이해가 되지 않았다. 그 내용은 바로 전 글에 정리했다. 저런 구문을 Here Documents 라고 한단다.


내 나름대로 정리!

Process Substitution 은, Standard Input(<) 을 사용할 때, < 기호 오른쪽에 문자열을 출력한 뒤, 그 내용을 왼쪽으로 Redirection 해주기를 원할 때 사용하는 문법이다.

복잡한 이론은 뒤로하고, 기억해야 할 내용은 위 문장이면 충분하다.
즉, read < xxx 등의 상황에서, xxx 가 파일이 아니고 문자열(즉 Std. Output)일 때, xxx 를 사용하기 위해 <(command) 문법을 준수해야만 한다.

One Comment

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