Pipe
프롬프트 명령행 상에서 모듈 프로그래밍을 할 수 있습니다. 바로 파이프를 이용하는 것인데요.
가령 ,
문자로 구분된 필드를 가진 inventory.txt 라는 파일을 읽어들여 3번째 필드를 선택해서
그중 첫 자가 c 로 시작하는 항목들만 뽑아내어 알파벳 순서로 정렬하여 프린트한다면
일반적인 프로그래밍 언어로도 그렇게 간단한 작업이 아닐 수 있습니다.
하지만 명령 행상에서 파이프를 이용한다면 다음과 같이 간단하게 처리할 수 있습니다.
cat inventory.txt | cut -d ',' -f 3 | grep '^c' | sort
먼저 필요한 명령들을 고르고, 적절한 옵션을 준 후에 파이프로 연결을 하면 프로그래밍을 한 것과 같이 훌륭하게 결과를 만들어 냅니다. 유닉스의 기본 철학을 보통 모듈화라고 합니다. 다시 말해서 "각기 독립적인 역할을 하는 프로그램을 만들어놓고 필요에 따라 선택해서 서로 조합하여 전체를 완성한다" 는 것은 바로 이 파이프라는 기능이 있기에 가능하다고 할 수 있습니다.
파이프는 IPC (Inter Process Communication) 의 한 방법입니다. 위의 예제에서 파이프로 연결된 명령들은 각각 독립적인 주소공간을 갖는 프로세스입니다. 그러므로 기본적으로 어느 한 프로세스가 가지고 있는 정보를 다른 프로세스에서 알 수가 없습니다. 이때 프로세스 간 정보를 전달하기 위해서 IPC 방법을 사용하는데 shell 에서는 파일을 이용하거나 위에서처럼 파이프를 이용할 수 있습니다.
파이프의 stdin, stdout, stderr
아래 그림은 파이프로 연결된 명령들이 실행될때 표준입력 (stdin), 표준출력 (stdout) , 표준에러 (stderr) 의 관계를 보여줍니다. program1 의 표준출력은 파이프를 통해 program2 의 표준입력 으로 전달되고 마찬가지로 program2 의 표준출력은 program3 의 표준입력이 되며 마지막으로 program3 표준출력이 터미널로 표시됩니다. 표준에러 같은 경우는 모두 터미널로 연결되어 있는 것을 볼 수 있습니다. 그러므로 명령이 파이프로 연결돼 있더라도 stderr 로 출력을 하면 파이프로 전달되지 않고 바로 터미널로 표시됩니다.
파이프로 연결된 명령은 subshell 에서 실행된다.
아래는 프롬프트 상에서 { echo; sleep 10 ;} | { echo; sleep 10 ;} | { echo; sleep 10 ;}
명령을 실행했을때의 프로세스 상태인데 파이프로 연결된 세 명령 모두 subshell 이 생성된후에 그 아래에서 실행되는것을 볼수있습니다. 그러므로 마지막 명령에서 결과를 어떤변수에 할당한다면 그값은 파이프 실행이 종료되면 사라지게 됩니다.
shopt -s lastpipe
옵션을 설정하면 마지막 명령이 현재 shell 에서 실행되게 할수있습니다. 이 옵션은 job control 이 disable 되있어야 사용할수 있는데 non-interactive shell 인 script 실행시에는 기본적으로 disable 됩니다.
파이프로 연결된 shell 의 FD
다음은 파이프로 연결된 shell 의 FD (File Descriptor) 상태입니다.
현재 shell pid 를 나타내는 $$
변수는 subshell 에서도 동일한 값을 가지므로
subshell 에서의 FD 상태를 보기 위해서는 $BASHPID
변수를 이용해야 합니다.
첫 번째 명령은 $$
변수를 사용했으므로 현재 shell 의 FD 상태에 해당하고
나머지는 파이프에 연결된 명령 순서 대로입니다.
첫 번째 명령은 stdout 이 파이프에 연결돼 있고,
두 번째 가운데에 위치한 명령은 stdin, stdout 둘 다 파이프에 연결되어 있습니다.
마지막 명령은 stdin 이 파이프가 연결돼 있는 것을 볼 수 있습니다.
개별적으로 명령을 실행하여 그림에는 파이프 번호가 각각 다르게 나오지만 실제 세 개의 명령이 함께 실행될 때는 첫 번째 명령의 stdout 파이프 번호와 두 번째 명령의 stdin 파이프 번호가 같고, 두 번째 명령의 stdout 파이프 번호와 세 번째 명령의 stdin 파이프 번호가 같게 됩니다.
명령이 파이프에서 실행되는지 구분하기
다음은 grep 명령이 파이프에서 실행될 경우 옵션설정을 달리하여 실행하기 위한 함수입니다.
# 함수이름이 우선순위가 높으므로 원본명령 지정을 위해 command 명령을 사용해야 합니다.
# /dev/fd 는 /proc/self/fd 의 심볼릭 링크로 프로세스 자신을 나타냅니다.
# /dev/stdin -> /proc/self/fd/0
# /dev/stdout -> /proc/self/fd/1
# /dev/stderr -> /proc/self/fd/2
# 이므로 /dev/fd/1 대신에 /dev/stdout 을 사용해도 됩니다.
grep() {
# FD 1번 (stdout) 이 터미널에 연결되어 있는지 테스트
if [ -t 1 ]; then
command grep -n "$@"
# stdout 이 (named, unnamed) pipe 에 연결되어 있는지 테스트
elif [ -p /dev/fd/1 ]; then
command grep "$@"
# stdout 이 일반 파일에 연결되어 있는지 테스트
elif [ -f /dev/fd/1 ]; then
command grep ...
fi
}
파이프로 연결된 명령들은 순서대로 실행될까?
보통 파이프에 대해 설명할때 command1 | command2
가 있을경우 command1 의 실행결과가 command2 의 입력으로 들어간다고 말합니다. 그렇다면 command1 이 종료된 후에 command2 가 실행될것 같지만 실은 그렇지 않습니다. 다음을 한번 보시죠
$ ps | grep ".*"
PID TTY TIME CMD
3773 pts/0 00:00:00 bash
3784 pts/0 00:00:00 ps
3785 pts/0 00:00:00 grep
만약에 ps 실행이 종료되고 그 결과가 grep 명령의 입력으로 들어간다면 최종 결과에는 grep 명령이 보이면 안되겠지만 ps 와 grep 명령이 동시에 나타나고 있습니다. 파이프로 연결된 프로그램들이 실행될때는 순서대로 실행되지 않고 모두 동시에 실행됩니다. 또한 command2 의 상태에 따라 command1 의 실행이 완료되기 전에 종료할 수도 있습니다.
다음과 같은 경우 grep 명령의 실행이 완료되기 전에 결과가 나오는 것을 볼 수 있습니다.
grep pattern very-large-file | tr a-z A-Z
다음과 같은 경우는 head 명령의 실행이 먼저 종료됨에 따라 grep 명령이 실행을 완료하기 전에 종료하게 됩니다.
grep pattern very-large-file | head -n 1
다음의 경우를 보면 t2.sh 에서 read 명령 실행으로 중지된 상태 지만 이미 t3.sh 을 거쳐 정보가 표시되고 있습니다.
--------- t1.sh ----------
#!/bin/bash
echo t1.sh
--------- t2.sh ----------
#!/bin/bash
echo t2.sh
cat # t1.sh 에서 받은 정보를 전달하기 위한 cat
read -r var < /dev/tty
echo $var
--------- t3.sh ----------
#!/bin/bash
echo t3.sh
cat # t2.sh 에서 받은 정보를 전달하기 위한 cat
############ 실행결과 ###########
# t2.sh 실행이 끝나지 않은 상태이지만 이미 t3.sh 을 거쳐 정보가 표시된다.
$ ./t1.sh | ./t2.sh | ./t3.sh
t3.sh
t2.sh
t1.sh
<---- t2.sh 에서 read 입력 대기 상태
time 명령으로 &&
연산자와 |
파이프 비교
$ time { sleep 2 && sleep 3 && sleep 4 ;} # && 연산자
real 0m9.010s <--- 2 + 3 + 4 = 9 초
user 0m0.005s
sys 0m0.005s
$ time { sleep 2 | sleep 3 | sleep 4 ;} # | 파이프
real 0m4.005s <--- 최대값 4 초
user 0m0.010s
sys 0m0.001s
파이프로 연결된 명령들의 종료 상태 값은?
파이프로 연결된 명령들 간에는 $?
변수로 이전 명령의 종료 상태 값을 확인할 수 없습니다. 그리고 파이프 실행이 종료됐을 때의 종료 상태 값은 마지막 명령의 종료 상태 값이 사용됩니다. 그래서 중간에 false 로 종료된 명령이 있더라도 마지막 명령이 true 로 종료되면 파이프 종료 상태 값은 true 가 됩니다. set -o pipefail
옵션을 설정하면 중간에 false 로 종료된 명령이 있을 경우 파이프 종료 상태 값은 false 가 됩니다. shell 변수로 PIPESTATUS
라는 array 변수가 있는데 이변수는 파이프로 연결된 모든 명령들의 종료 상태 값을 담고 있습니다.
파이프로 연결된 명령들은 process group 을 형성한다.
파이프를 이용해 여러 명령을 동시에 실행시키면 process group 이 만들어지는데 이때 파이프로 연결된 명령들 중에서 첫 번째 명령의 PID 가 Process Group ID (PGID) 가 됩니다. 이후 jopspec 을 이용하여 job control 을 하게 되면 동일한 process group 에 속한 명령들이 모두 같이 적용을 받게 됩니다.
파이프에서 복수의 명령 사용하기
파이프로 명령을 연결할때 꼭 하나의 명령만 사용할 수 있는것은 아닙니다.
# 다음의 경우는 첫번째 명령의 결과를 중간의 date 명령이 전달하지 못하고 있습니다.
$ echo "What is the date today?" | date | cat
Mon Jul 27 10:51:34 KST 2015
$ echo "What is the date today?" | { cat ; date ;} | cat
What is the date today?
Mon Jul 27 11:01:57 KST 2015
$ echo "What is the date today?" | { date; cat ;} | cat
Mon Jul 27 11:02:05 KST 2015
What is the date today?
명령이 command1 | command2
와 같이 실행될 경우 command2 에서는 stdin 이 파이프에 연결되어 있어 read 명령으로 사용자로부터 입력을 받을 수 없는데요.
이때는 현재 터미널에 연결되어 있는 stdout 이나 stderr 또는 /dev/tty 를
이용해 사용자로부터 입력을 받을 수 있습니다.
command ... | { read -p 'enter: ' var < /dev/tty; command ... ;}
# 또는
command ... | { read -p 'enter: ' var <&2; command ... ;}
Command line 개념
find 명령의 -exec 액션이나, xargs, 또는 sudo 같은 명령들은 자체 명령도 실행되지만 입력받은 child 명령도 실행하죠. 그럼 여기서 파이프나 redirection 을 사용하게 되면 자체 명령에 적용돼야 하는지 아니면 child 명령에 적용돼야 하는지 문제가 생깁니다.
다음 명령을 보시면 *.c
파일을 선택해서 grep 한 결과를 *.c.out
파일에 각각 저장하려고
한 것인데요. 그러니까 kkk.c
lll.c
mmm.c
파일이 있을 경우
kkk.c.out
lll.c.out
mmm.c.out
가 생성돼야 하는데 실행을 해보면 grep 한 결과가
{}.out
파일 하나에 모두 저장되는 것을 볼 수 있습니다.
이렇게 되는 이유는 현재 command line 이 find 와 xargs 에 해당되기 때문에
>
redirection 이 grep 명령에 적용되지 않고 find 와 xargs 에 적용되기 때문입니다.
$ find * -name '*.c' -exec grep ABC {} > {}.out \;
$ ls
kkk.c lll.c mmm.c {}.out
$ ls *.c | xargs -i grep ABC {} > {}.out
$ ls
kkk.c lll.c mmm.c {}.out
# 이해를 돕기 위해 위 명령문을 다시 작성해 보면 아래와 동일한 명령문이 됩니다.
# 다시 말해서 '>' redirection 이 find 와 xargs 명령에 해당되는 것입니다.
$ > {}.out find * -name '*.c' -exec grep ABC {} \;
$ ls *.c | > {}.out xargs -i grep ABC {}
그러면 원래 의도한 대로 >
redirection 이 grep 명령의 command line 에 적용되게 하려면 어떻게 해야 될까요?
sh -c
or bash -c
형식을 이용하여 다음과 같이 grep 명령의 command line 을 만들어 주어야 합니다.
$ find * -name '*.c' -exec sh -c 'grep ABC "{}" > "{}".out' \;
$ ls *.c | xargs -i sh -c 'grep ABC "{}" > "{}".out'
$ ls
kkk.c kkk.c.out lll.c lll.c.out mmm.c mmm.c.out
이것은 다음과 같이 파이프를 이용해 명령을 연결할 때도 동일하게 적용됩니다.
마찬가지로 kkk.c
lll.c
mmm.c
각각의 파일에서 grep 한결과를 sort 해서
제일 위의 라인 하나만 출력하려고 한 것인데요.
그러니까 파일이 3 개 이므로 3 개의 라인이 출력되어야 합니다.
하지만 다음과 같은 방법으로는 오류가 발생하거나 의도한 대로 결과가 출력되지 않습니다.
# find 명령의 command line 은 sort 명령 앞에 있는 파이프까지인데
# find 명령의 인수에 해당하는 '\;' 가 문장 끝에 위치하므로 오류가 됩니다.
$ find * -name '*.c' -exec grep ABC {} | sort | head -1 \;
head: cannot open ';' for reading: No such file or directory
find: missing argument to '-exec'
Try 'find --help' for more information.
# 따라서 다음과 같이 하게 되면 결과적으로 하나의 라인만 출력이 되겠죠.
$ find * -name '*.c' -exec grep ABC {} \; | sort | head -1
line ABC ...
kkk.c
lll.c
mmm.c
각각의 파일에서 sort 한 결과 첫 라인이 출력되게 하려면
다음과 같이 sh -c
을 이용하여 grep 명령의 command line 을 만들어 주어야 합니다.
$ find * -name '*.c' -exec sh -c 'grep ABC "{}" | sort | head -1' \;
line ABC.... # from kkk.c
line ABC.... # from lll.c
line ABC.... # from mmm.c
$ ls '*.c' | xargs -i sh -c 'grep ABC "{}" | sort | head -1'
이번에는 sudo 명령을 살펴보겠습니다.
/proc/sys/kernel/yama/ptrace_scope
는 strace 명령과 관련된 커널 옵션인데요.
아래 보시면 퍼미션이 -rw-r--r--
로 되어있어서 일반 사용자가 값을 읽을수는 있어도 쓸수는 없습니다.
그래서 값을 변경하기 위해 sudo 명령을 이용하고 있는데 Perminssion denied 가 발생하는 것을 볼 수 있습니다.
$ ls -l /proc/sys/kernel/yama/ptrace_scope
-rw-r--r-- 1 root root 0 2017-01-25 02:34 /proc/sys/kernel/yama/ptrace_scope
$ cat /proc/sys/kernel/yama/ptrace_scope
1
$ sudo echo 0 > /proc/sys/kernel/yama/ptrace_scope
bash: /proc/sys/kernel/yama/ptrace_scope: Permission denied
위 명령에서 >
는 sudo 하고 echo 명령 중에 어느 명령에 해당될까요?
현재 sudo 명령의 command line 이므로 >
는 echo 명령이 아닌 sudo 명령에 해당됩니다.
sudo 명령은 일반 유저에 의해 실행되죠. 그러므로 >
도 역시 일반 유저 권한으로 처리되어
permission 에러가 발생합니다.
따라서 sudo 명령에 의해 echo 명령과 >
가 root 권한으로 실행되려면
다음과 같이 sh -c
를 이용해 echo 명령의 command line 을 만들어 주어야 합니다.
$ sudo sh -c 'echo 0 > /proc/sys/kernel/yama/ptrace_scope'
다음을 보시면 sudo 명령의 command line 에 해당되는 > x2
파일은 일반 유저로 나오고
echo 명령의 command line 에 해당되는 > x1
파일은 root 로 나오는 것을 볼 수 있습니다.
$ sudo sh -c 'echo > x1' > x2
$ ls -l x1 x2
-rw-r--r-- 1 root root 1 2017-01-25 02:44 x1
-rw-rw-r-- 1 mug896 mug896 0 2017-01-25 02:44 x2
참고로 예제로든 sudo 명령은 다음과 같이 할 수도 있습니다.
$ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
0
$ cat /proc/sys/kernel/yama/ptrace_scope
0
Quiz
fuser
명령을 이용하면 특정 디렉토리나 마운트된 파일시스템을 current directory 로 사용 중인 프로세스들을
알아볼 수 있고 또는 특정 파일을 사용 중인( 실행하거나 open 한 ) 프로세스들을 알아볼 수도 있습니다.
다음은 /proc
파일시스템을 사용 중인 프로세스들을 출력해 본 것인데요.
여기에는 실제 stdout 출력값과 stderr 출력값이 섞여있습니다.
어떤 값이 stdout 로 출력된 값이고, 어떤 값이 stderr 로 출력된 값일까요?
$ fuser -m /proc
/proc: 697rc 1080 2415 2719 2922 3009 3317 4019 4599rc
7596rc 7597rc 7600rc 7673 7741rc 7771rc 7861rc 7922rc 7941rc 7953rc
7971rc 7979rc 8170rc 8189rc 8396rc 8417rc 8447rc 8461rc 8477rc 8503rc
다음과 같이 해보면 stdout 출력값으로 pid 만 남게 되는 것을 볼 수 있습니다.
따라서 위 fuser 명령을 파이프를 이용해 다른 명령들과 연결하게 되면
pid 뒤에 붙은 rc
나 /proc:
같은 문자들은 전달되지 않고
pid 들만 전달되겠죠
$ fuser -m /proc 2> /dev/null
697 1080 2415 2719 2922 3009 3317 4019 4599
7596 7597 7600 7673 7741 7771 7861 7922 7941 7953
7971 7979 8170 8189 8396 8417 8447 8461 8477 8503
# pid 만 xargs 명령으로 전달된다.
$ fuser -m /proc | xargs ps
2.
가령 현재 디렉토리에 k.c
l.c
m.c
n.c
4 개의 파일이 있고 각각 ABC 스트링과 매칭 되는
라인을 가지고 있다고 할 경우 아래의 명령을 실행해 보면 처음 한 라인만 출력이 되고
나머지 grep 명령은 signal 에의해 terminated 되는 것을 볼 수 있습니다.
왜 이런 현상이 발생할까요?
$ find * -name '*.c' -exec grep ABC {} \; | head -1
line ABC .....
find: ‘grep’ terminated by signal 13
find: ‘grep’ terminated by signal 13
find: ‘grep’ terminated by signal 13
먼저 find 명령의 -exec 옵션을 이용해 명령을 실행하는 방법은
다음과 같이 \;
와 {} +
를 이용하는 2 가지가 있습니다.
# 이것은 grep 명령이 각각의 {} 파일에 대해 4 번 실행됩니다.
$ find * -name '*.c' -exec grep ABC {} \;
# grep ABC k.c
# grep ABC l.c
# grep ABC m.c
# grep ABC n.c
# 이것은 grep 명령이 4 개의 파일을 인수로 갖고 한번 실행됩니다.
$ find * -name '*.c' -exec grep ABC {} +
# grep ABC k.c l.c m.c n.c
# -exec 옵션의 마지막에 '\;' 문자를 붙이는 이유는 명령의 끝을 나타내기 위해서입니다.
# 그래야 뒤이어서 또 -exec 옵션을 사용할 수 있겠죠
$ find * -name '*.c' -exec grep ABC {} \; -exec stat -c '%n: %s' {} \;
질문의 예제 명령에서 -exec 명령을 \;
로 실행시켰으므로 grep 명령이 find 에의해 4 번 실행
됩니다. 하지만 파이프에 연결된 head -1
명령은 첫 라인을 출력하고 종료되므로
이후에는 파이프가 close 되어서 signal 13 ( SIGPIPE ) 에 의해 나머지 grep 명령들이 terminated
되게 됩니다.