명령 그룹과 코프로세스 — ( ) · { } · coproc
이 글은 GNU Bash 매뉴얼 3.2.5.3 Grouping Commands와 3.2.6 Coprocesses를, 처음 배우는 분도 따라올 수 있게 쉬운 말로 풀어 정리합니다. 여러 명령을 하나의 덩어리로 묶는 두 가지 방법 ( )와 { }의 차이, 그리고 명령과 양방향으로 대화 하는 coproc를 살펴봅니다.
이 글을 읽고 나면
( )(서브셸)와 { }(현재 셸)의 차이를 정확히 구분할 수 있습니다.
여러 명령의 출력을 한 번에 리다이렉션하는 방법을 압니다.
언제 서브셸이 “변수 변경을 잃어버리는지” 이해합니다.
코프로세스가 무엇인지, 언제 쓰는지 감을 잡습니다.
왜 명령을 묶을까
지금까지는 명령을 한 줄씩, 또는 &&/|로 이어서 실행했습니다. 그런데 “여러 명령을 한 덩어리처럼 다루고 싶은” 순간이 있습니다.
여러 echo의 출력을 한 파일로 모으고 싶다.
몇 개의 명령을 하나의 조건 으로 판단하고 싶다.
일부 작업만 격리된 환경 에서 돌리고, 끝나면 원래 상태로 돌아오고 싶다.
이럴 때 명령을 괄호로 묶습니다. Bash에는 묶는 방법이 두 가지 있고, 딱 하나의 차이 로 갈립니다.
Text
{ 명령들; } → 지금 이 셸에서 실행 → 변수·디렉터리 변경이 남는다 ( 명령들 ) → 새 '서브셸'(자식 셸 프로세스)에서 실행 → 안에서 변경한 변수·디렉터리 등은 “부모 셸(현재 스크립트 바깥)에는 영향을 주지 않는다” 즉, 안에서 값을 바꿔도 서브셸이 끝나면 바뀐 내용이 바깥에는 남지 않습니다.
{ } — 현재 셸에서 묶기
중괄호로 묶으면, 명령들이 지금 쓰고 있는 셸 그대로 실행됩니다. 새 프로세스를 만들지 않으므로, 안에서 바꾼 변수나 디렉터리가 밖에도 그대로 전달됩니다.
Bash
value="before" { value="after"; echo "inside: $value"; } echo "outside: $value" # → after (변경이 남음)
문법에서 자주 실수하는 두 가지가 있습니다.
규칙 |
이유 |
|---|---|
{ 뒤에 공백 필요 |
{가 예약어라 단어로 떨어져야 인식됨 |
마지막 명령 뒤 ; 또는 줄바꿈 필요 |
} 앞에서 명령이 끝났음을 알려야 함 |
즉 { echo hi; }처럼 { 뒤 공백과 } 앞 ;가 모두 있어야 합니다.
( ) — 서브셸에서 묶기
소괄호로 묶으면, 명령들이 새로운 서브셸(자식 프로세스)에서 실행됩니다. 서브셸은 부모의 값을 복사해서 시작하지만, 안에서 무엇을 바꿔도 부모에게는 돌아가지 않습니다.
Bash
value="before" ( value="changed"; echo "inside: $value" ) echo "outside: $value" # → before (변경이 사라짐)
디렉터리 이동도 마찬가지입니다.
Bash
( cd /tmp; echo "여기선 /tmp"; ) # 서브셸 안에서만 이동 echo "여기선 원래 위치" # 밖은 그대로
{ } vs ( ) — 한눈에
{ list; } |
( list ) |
|
|---|---|---|
실행 위치 |
현재 셸 |
새 서브셸(자식 프로세스) |
변수 변경 |
밖에 남음 |
밖에 안 남음 |
cd 영향 |
밖에 남음 |
밖에 안 남음 |
비용 |
거의 없음 |
프로세스 생성 비용 약간 |
문법 |
{ 뒤 공백, } 앞 ; |
공백 규칙 느슨 |
고르는 기준은 하나입니다. “안에서 바꾼 것이 밖에 남아야 하나?” 남아야 하면 { }, 격리하고 싶으면 ( )입니다.
여러 명령 출력을 한 번에 리다이렉션하는 방법
묶는 가장 실용적인 이유는 여러 명령의 출력을 한 번에 보낼 수 있다는 점입니다.
Bash
{ echo "line 1" echo "line 2" } > out.txt
echo마다 >> out.txt를 붙이지 않아도, 블록 전체의 출력이 out.txt로 갑니다. 로그를 모으거나 파일을 만들 때 좋습니다.
여러 명령을 하나의 조건 으로 묶을 수도 있습니다.
Bash
if { grep -q 'line 1' out.txt && grep -q 'line 2' out.txt; }; then echo "both found" fi
코프로세스(coproc) — 명령과 양방향으로 대화
보통의 파이프 A | B는 “A의 출력 → B의 입력” 한 방향입니다. 그런데 어떤 프로그램은 내가 한 줄 보내면 한 줄 답하는 식으로, 계속 켜 둔 채 주고받아야 합니다. 이때 쓰는 것이 코프로세스입니다.
Text
┌───────────────┐ 써넣기 → │ │ coprocess │ (백그라운드에서 계속 실행) 읽어오기 ← │ └───────────────┘
coproc는 Bash 4.0 이상에서 사용할 수 있는 기능으로, 하나의 명령(예: 내부적으로 반복적으로 읽거나 써야 하는 프로그램)을 백그라운드에서 독립적으로 실행 하면서, 그 프로그램과 내 셸(스크립트) 사이를 2개의 파이프(쓰기용, 읽기용) 로 연결해 줍니다.
즉, coproc NAME { some_command; }처럼 실행하면,
지정한 명령이 별도의 백그라운드 프로세스로 돌아가고
내 쪽에서는 NAME[1](쓰기용)에 데이터를 echo ... >&"${NAME[1]}"처럼 써넣을 수 있고
NAME[0](읽기용)에서 read ... <&"${NAME[0]}"처럼 출력을 읽어올 수 있습니다.
이 방식은 단방향 파이프(A | B)와 달리, 명령과 스크립트가 양방향으로 데이터(줄이나 명령)를 주고받으며 상호작용 할 수 있게 해줍니다.
예를 들어, 프롬프트를 기다리는 해석기, stdin에 입력을 받고 응답을 돌려주는 서버형 CLI, 혹은 줄 단위로 명령을 주고받아야 하는 프로그램 등과 효과적으로 연동할 때 활용할 수 있습니다.
권장 형태는 이렇습니다.
Bash
coproc MYPROC { some_long_running_command; } # MYPROC[1] : 코프로세스에 '써넣는' 통로 # MYPROC[0] : 코프로세스에서 '읽어오는' 통로 echo "input line" >&"${MYPROC[1]}" # 보내기 read -r reply <&"${MYPROC[0]}" # 받기
이름 |
의미 |
|---|---|
NAME[1] |
코프로세스의 입력으로 써넣는 파일 디스크립터 |
NAME[0] |
코프로세스의 출력에서 읽어오는 파일 디스크립터 |
NAME_PID |
코프로세스 프로세스 ID (wait로 종료 대기) |
이름을 생략하면 기본 이름은 COPROC가 됩니다.
실행 예제
아래 스크립트는 { }와 ( )의 차이, 그룹 리다이렉션, 그룹을 조건으로 묶는 방법을 한 화면에서 보여 줍니다. (코프로세스는 이식성 문제로 제외했습니다.)
Bash
#!/usr/bin/env bash # ( ) 서브셸 vs { } 현재 셸, 그리고 그룹 리다이렉션을 비교합니다. set -eu demo_dir="$(mktemp -d "${TMPDIR:-/tmp}/b06.XXXXXX")" cleanup() { rm -rf -- "$demo_dir"; } trap cleanup EXIT echo "=== 1) { } : 현재 셸에서 실행 → 변수 변경이 남는다 ===" value="before" { value="after"; echo "inside braces: value=$value"; } # (1) 같은 셸이라 대입이 유지됨 echo "outside braces: value=$value" # (2) after 로 바뀌어 있음 echo echo "=== 2) ( ) : 서브셸에서 실행 → 변수 변경이 사라진다 ===" value="before" ( value="changed"; echo "inside subshell: value=$value" ) # (3) 별도 셸이라 안에서만 유효 echo "outside subshell: value=$value" # (4) 여전히 before echo echo "=== 3) ( ) 안의 cd 는 밖에 영향 없음 ===" here="$PWD" ( cd "$demo_dir"; echo "inside : $(pwd | sed 's#.*/##')" ) # (5) 서브셸에서만 디렉터리 이동 echo "outside: $(cd "$here" && pwd | sed 's#.*/##')" # (6) 원래 위치 그대로 echo echo "=== 4) 그룹 리다이렉션: 여러 명령 출력을 한 파일로 ===" { echo "line 1" echo "line 2" } >"$demo_dir/out.txt" # (7) 블록 전체 출력을 파일로 echo "file has $(wc -l <"$demo_dir/out.txt" | tr -d ' ') lines" echo echo "=== 5) 그룹으로 여러 명령을 한 조건에 묶기 ===" if { grep -q 'line 1' "$demo_dir/out.txt" && grep -q 'line 2' "$demo_dir/out.txt"; }; then echo "both lines found" # (8) 블록이 하나의 결과로 취급됨 fi echo echo "ok — 묶을 때 '변수가 남아야 하나?'로 { } 와 ( ) 를 고릅니다."
주요 라인별 설명
1–2: { }는 현재 셸에서 돌아, 안에서 바꾼 value가 밖에도 after로 남습니다.
3–4: ( )는 서브셸에서 돌아, 안에서 바꾼 값이 밖에는 반영되지 않아 before 그대로입니다.
5–6: 서브셸 안의 cd도 밖에 영향을 주지 않습니다. 잠깐 이동했다 돌아올 때 편리합니다.
7: 블록 전체의 출력을 > out.txt 한 번으로 파일에 모읍니다.
8: 두 grep을 하나의 조건 덩어리로 묶어 if에 넘깁니다.
실행하면 대략 이런 결과가 나옵니다.
Text
=== 1) { } : 현재 셸에서 실행 → 변수 변경이 남는다 === inside braces: value=after outside braces: value=after === 2) ( ) : 서브셸에서 실행 → 변수 변경이 사라진다 === inside subshell: value=changed outside subshell: value=before === 3) ( ) 안의 cd 는 밖에 영향 없음 === inside : b06.XXXXXX outside: blog-post === 4) 그룹 리다이렉션: 여러 명령 출력을 한 파일로 === file has 2 lines === 5) 그룹으로 여러 명령을 한 조건에 묶기 === both lines found
(3번의 inside에 보이는 임시 폴더 이름은 실행할 때마다 다릅니다.)
헷갈리기 쉬운 부분
파이프 뒤 while에서 변수가 안 남음 — cmd | while ...의 while 부분은 서브셸에서 돌 수 있어, 안에서 바꾼 변수가 밖에 남지 않습니다. 이건 ( )가 변수를 격리하는 것과 같은 이유입니다. (해결책은 이후 글에서 다룹니다.)
{/} 문법 실수 — {뒤 공백, }앞 ;(또는 줄바꿈)이 없으면 문법 오류가 납니다. {echo hi}(X) → { echo hi; }(O).
서브셸에서 바꾼 값을 밖에서 기대 — ( VAR=1 ) 뒤에 $VAR는 비어 있습니다. 값을 남기려면 { }를 쓰거나, 결과를 출력해 받으세요.
coproc를 어디서나 되는 것으로 가정 — Bash 4+ 전용입니다. 3.2(예: 옛 macOS)에서는 동작하지 않습니다.
한 줄 요약
여러 명령을 묶을 때, 변경을 남기려면 { }(현재 셸), 격리하려면 ( )(서브셸)를 쓰고, 묶음 전체에 리다이렉션을 걸 수 있습니다. 명령과 양방향으로 계속 주고받아야 할 때만 coproc를 고려하세요.