리눅스 파이프(|) 사용법: 명령어를 이어 붙여 쓰는 법
쉘을 처음 쓰시다 보면 누군가의 블로그나 README에서 이런 명령어를 자주 보게 됩니다.
$ ps -ef | grep node | awk '{print $2}'
가운데 들어 있는 | 기호가 도대체 무슨 역할을 하는지 처음에는 감이 잘 잡히지 않으실 텐데요.
이 기호를 파이프(pipe) 또는 파이프라인(pipeline)이라고 부릅니다.
이름 그대로 한 명령어의 출력을 다음 명령어의 입력으로 흘려보내는 관이라고 생각하시면 됩니다.
이번 포스팅에서는 리눅스 파이프의 기본 개념부터 실제로 자주 쓰는 조합 패턴까지 차근차근 살펴보겠습니다. 앞으로 파이프가 들어간 명령어를 보실 때 마음이 한결 편해지실 거예요. 😊
파이프란 무엇인가
파이프는 한 명령어의 표준 출력(Standard Output, stdout)을 다음 명령어의 표준 입력(Standard Input, stdin)으로 연결해 주는 쉘 문법입니다.
표준 출력과 표준 입력이라는 표현이 처음에는 낯설 수 있지만, 일단은 “터미널에 찍히는 결과물”과 “터미널에서 받는 입력값” 정도로 이해하셔도 충분합니다.
명령어1 ──[stdout]──▶ | ──[stdin]──▶ 명령어2
원래 명령어 하나는 자기 결과를 그대로 터미널에 뿌리고 일을 끝냅니다. 하지만 파이프를 끼워 넣으면 그 결과물이 터미널 화면 대신 다음 명령어의 손으로 곧장 건너갑니다. 다음 명령어 입장에서는 마치 누가 키보드로 그 내용을 직접 타이핑해 준 것처럼 받아서 처리하게 되죠.
이런 단순한 발상 하나가 유닉스 쉘을 이렇게나 강력하게 만들었습니다. 복잡한 일을 하나의 거대한 프로그램으로 처리하는 대신에, 작은 도구들을 조합해서 해결하는 유닉스 철학의 핵심이 바로 파이프거든요.
가장 기본적인 사용법
문법은 단순합니다.
두 명령어 사이에 |만 넣어 주면 됩니다.
$ 명령어1 | 명령어2
가장 흔한 예제로 ls 명령어의 결과에서 특정 파일만 찾는 것을 들 수 있습니다.
ls는 디렉토리 안의 파일 목록을 출력해 주는데요.
파일이 너무 많아서 원하는 파일을 눈으로 찾기 어려울 때가 있죠.
이럴 때 grep을 파이프로 이어 붙이면 결과를 걸러낼 수 있습니다.
$ ls | grep ".md"
README.md
CHANGELOG.md
여기서 일어난 일은 다음과 같습니다.
우선 ls가 디렉토리의 모든 파일 이름을 표준 출력으로 내보냈습니다.
원래대로라면 그 목록이 터미널에 그대로 찍혔겠지만, 파이프 덕분에 그 출력이 grep ".md" 명령어의 입력으로 전달됐습니다.
grep은 입력으로 받은 줄 중에서 .md라는 패턴이 들어간 줄만 골라서 다시 표준 출력으로 내보냈고요.
그 결과 우리 눈에는 .md 파일들만 깔끔하게 보이게 된 것입니다.
다른 예로, 어떤 텍스트를 모두 대문자로 바꾸고 싶다면 tr 명령어와 조합할 수 있습니다.
$ echo "hello world" | tr 'a-z' 'A-Z'
HELLO WORLD
echo는 인자로 받은 문자열을 그대로 출력하고, tr은 입력으로 받은 문자를 다른 문자로 변환해 주는 도구입니다.
이 두 명령어를 파이프로 묶었을 뿐인데 “문자열을 받아 대문자로 변환하는 새로운 명령어” 하나가 즉석에서 만들어진 셈입니다.
여러 명령어를 줄줄이 연결하기
파이프의 진짜 매력은 두 개를 넘어서 여러 명령어를 한 줄로 엮을 수 있다는 점입니다. 한쪽 끝에서 흘려보낸 데이터가 여러 단계의 처리를 거쳐 반대편으로 나오게 만들 수 있죠.
$ 명령어1 | 명령어2 | 명령어3 | 명령어4
자주 쓰이는 예시로 단어 빈도수 세기를 들어 보겠습니다. 다음과 같은 과일 목록 파일이 있다고 가정해 볼게요.
$ cat fruits.txt
apple
banana
cherry
apple
date
banana
각 과일이 몇 번씩 등장하는지 세고, 자주 등장한 순서대로 정렬하고 싶다면 이렇게 쓸 수 있습니다.
$ cat fruits.txt | sort | uniq -c | sort -rn
2 banana
2 apple
1 date
1 cherry
데이터가 어떻게 변형되며 흘러가는지 한 단계씩 따라가 볼까요.
처음 cat fruits.txt는 파일의 내용을 그대로 출력합니다.
다음 단계의 sort는 받은 줄을 알파벳 순으로 정렬해 줍니다.
이어지는 uniq -c는 인접한 중복 줄을 합치면서 앞에 등장 횟수를 붙여 줍니다.
uniq은 정렬된 입력에서만 제대로 동작하기 때문에 sort를 먼저 거쳐야 한다는 점에 주의하세요.
마지막으로 sort -rn이 숫자(-n) 기준으로 내림차순(-r) 정렬해서 가장 많이 등장한 과일이 위로 올라옵니다.
이렇게 네 개의 작은 도구를 파이프로 엮었을 뿐인데 “단어 빈도수 분석기”가 완성되었습니다. 같은 일을 파이썬이나 자바스크립트로 짠다면 훨씬 더 많은 줄의 코드가 들어갔을 겁니다.
자주 쓰는 파이프 조합 패턴
실무에서 자주 마주치는 파이프 조합 몇 가지를 모아 봤습니다. 한번 익혀 두시면 두고두고 손에 붙으실 거예요.
특정 프로세스를 찾아 그 PID만 뽑아낼 때는 ps, grep, awk를 묶어 쓰는 것이 거의 관용구처럼 굳어져 있습니다.
$ ps -ef | grep node | grep -v grep | awk '{print $2}'
여기서 grep -v grep이 들어간 이유는 grep node 자체도 프로세스 목록에 잡혀서 결과에 끼어들기 때문입니다.
이를 제외하기 위해 -v(invert) 옵션으로 grep이 들어간 줄을 빼 주는 거죠.
긴 출력에서 처음 몇 줄만 보고 싶다면 head를, 마지막 몇 줄만 보고 싶다면 tail을 끝에 붙입니다.
$ ls -la | head -5
$ tail -f app.log | grep ERROR
특히 tail -f로 로그 파일을 실시간으로 따라가면서 grep으로 특정 키워드만 필터링하는 패턴은 운영 환경 디버깅의 단골 메뉴입니다.
파일이나 디렉토리 개수를 세고 싶다면 wc -l이 유용합니다.
wc는 단어, 줄, 글자 수를 세 주는 도구이고 -l 옵션은 줄 수만 세 줍니다.
$ ls *.md | wc -l
*.md로 매칭된 파일들이 한 줄씩 출력되고, wc -l이 그 줄 수를 세서 결과적으로 마크다운 파일 개수를 알려 줍니다.
JSON API 응답을 처리할 때는 curl과 jq를 묶어 쓰는 조합이 사실상 표준입니다.
$ curl -s https://api.github.com/users/octocat | jq '.name'
"The Octocat"
curl -s로 받아 온 JSON 응답을 jq로 파싱해서 원하는 필드만 뽑는 패턴이죠.
프론트엔드 개발자도 백엔드 API를 빠르게 찔러 볼 때 자주 쓰게 됩니다.
파이프와 리다이렉션의 차이
파이프 |와 리다이렉션 >는 처음 보면 비슷해 보이지만 연결하는 대상이 다릅니다.
이 차이를 이해하면 둘을 헷갈릴 일이 없어집니다.
리다이렉션은 명령어의 출력을 파일로 보내거나 파일로부터 입력을 받습니다. 반면 파이프는 명령어의 출력을 다른 명령어로 흘려보냅니다.
$ ls > files.txt # ls의 결과를 files.txt 파일에 저장
$ ls | grep ".md" # ls의 결과를 grep 명령어로 전달
둘은 함께 쓰일 수도 있습니다. 파이프로 여러 명령어를 거친 최종 결과를 파일로 저장하는 패턴은 매우 흔합니다.
$ cat fruits.txt | sort | uniq -c > result.txt
cat부터 uniq -c까지의 처리 결과가 result.txt 파일에 저장됩니다.
즉, 파이프로 데이터를 가공하는 라인을 길게 만든 다음, 마지막에 리다이렉션으로 마무리하는 식입니다.
리다이렉션의 자세한 사용법은 쉘 리다이렉션 사용법에서 별도로 다루었으니 함께 보시면 도움이 됩니다.
표준 오류는 파이프로 흐르지 않는다
파이프를 처음 쓰시는 분들이 자주 만나는 함정 하나를 짚고 넘어가야겠습니다. 파이프는 표준 출력만 전달하고, 표준 오류는 그대로 터미널에 찍습니다.
예를 들어 존재하지 않는 파일을 cat으로 열고 그 결과를 grep으로 거르려고 시도해 볼게요.
$ cat nonexistent.txt | grep "hello"
cat: nonexistent.txt: No such file or directory
cat이 만들어 낸 오류 메시지가 grep에 전달되지 않고 터미널에 그대로 출력된 것을 볼 수 있습니다.
이게 바로 오류 출력이 표준 오류 스트림(Standard Error, stderr)으로 나갔기 때문이에요.
파이프는 표준 출력(stdout)만 전달하므로 오류 메시지는 다음 단계로 넘어가지 않습니다.
만약에 오류 메시지까지 함께 다음 명령어로 넘기고 싶다면, 오류 출력을 표준 출력으로 합쳐 주는 트릭을 써야 합니다.
$ cat nonexistent.txt 2>&1 | grep "No such"
cat: nonexistent.txt: No such file or directory
2>&1은 표준 오류를 표준 출력으로 합쳐 주는 리다이렉션 문법입니다.
이 문법의 자세한 의미는 쉘 리다이렉션 사용법에서 다루고 있어요.
Bash에서는 |&라는 축약형도 제공하니 알아 두시면 편합니다.
$ cat nonexistent.txt |& grep "No such"
2>&1 |을 |& 한 글자로 줄여 쓸 수 있다는 점이 매력적이지만 POSIX 표준은 아닙니다.
/bin/sh 같은 환경에서도 동작해야 하는 스크립트라면 2>&1 |을 쓰는 게 안전합니다.
파이프의 동작 원리 살짝 들여다보기
파이프를 만나면 쉘은 내부적으로 어떤 일을 할까요? 원리를 살짝만 알아 두면 가끔 만나는 이상한 동작을 이해하기 쉬워집니다.
쉘은 파이프를 만나면 양쪽 명령어를 동시에 실행합니다. “왼쪽이 끝난 다음 오른쪽을 실행한다”가 아니거든요. 운영체제 수준에서 두 프로세스 사이에 메모리 버퍼를 마련해 놓고, 왼쪽이 출력하는 즉시 오른쪽이 읽어 갈 수 있게 만들어 줍니다.
이 점이 중요한 이유는 tail -f처럼 끝나지 않는 명령어도 파이프 입력으로 쓸 수 있기 때문입니다.
$ tail -f app.log | grep ERROR
tail -f는 파일이 자라나는 만큼 계속 새 줄을 출력하는 명령어입니다.
“왼쪽이 다 끝나야 오른쪽이 시작한다”라면 이 명령은 영원히 결과를 보여 주지 못할 텐데, 동시에 실행되기 때문에 새 로그가 추가되는 즉시 grep이 받아서 필터링한 결과를 우리에게 보여 줄 수 있습니다.
또 한 가지 알아 두면 좋은 점은 파이프라인의 종료 코드 처리입니다. 기본적으로 Bash는 파이프라인 전체의 종료 코드로 마지막 명령어의 종료 코드를 사용합니다.
$ false | true
$ echo $?
0
false는 항상 실패(종료 코드 1)를 반환하지만, 마지막 true가 성공(종료 코드 0)을 반환했기 때문에 파이프라인 전체가 성공으로 처리됩니다.
스크립트에서 파이프라인 중간의 실패를 놓치고 싶지 않다면 set -o pipefail 옵션을 켜 주세요.
이 옵션을 켜면 파이프라인 중 하나라도 실패하면 전체가 실패로 처리됩니다.
$ set -o pipefail
$ false | true
$ echo $?
1
운영 환경 스크립트라면 set -euo pipefail을 스크립트 첫 줄에 박아 두는 것이 정석으로 자리 잡았습니다.
마치며
지금까지 리눅스 파이프의 기본 개념부터 자주 쓰는 조합 패턴까지 살펴보았습니다. 파이프는 문법 자체는 너무도 단순하지만, 작은 도구들을 자유롭게 엮어 주는 유닉스의 가장 우아한 발명품 중 하나입니다. 처음에는 “이걸 어디에 쓰지” 싶다가도, 한번 손에 익으면 마우스로는 따라 할 수 없는 속도로 데이터를 가공하시게 됩니다.
다음 단계로는 출력의 흐름을 더 정밀하게 제어하는 쉘 리다이렉션을 익혀 보시기를 추천드립니다. 파이프와 리다이렉션은 형제 같은 문법이라서 둘을 함께 알아 두면 시너지가 큽니다. 또 텍스트 처리에 자주 쓰이는 grep, sed 같은 도구를 더 깊이 익히면 파이프의 진가를 더 잘 느끼시게 됩니다.
파이프 연산자의 더 정확한 정의는 POSIX Shell 명세의 Pipelines 항목에서 확인하실 수 있습니다.
This work is licensed under
CC BY 4.0