Featured image of post [Bash Shell] 14. sed - 스트림 편집

[Bash Shell] 14. sed - 스트림 편집

sed는 파일을 한 줄씩 패턴 공간에 읽어 치환·삭제·출력 스크립트를 적용하는 비대화형 스트림 편집기다. s/d/p/q 명령과 정규식 플래그, 주소 지정을 예제로 정리하고 GNU sed -i와 BSD/macOS sed -i의 접미사 인자 차이 등 실무 함정을 다룬다.

sed(stream editor)는 파일이나 표준 입력을 한 줄씩 읽어 “패턴 공간(pattern space)“이라는 임시 버퍼에 넣고, 지정한 스크립트(치환·삭제·출력 명령)를 그 버퍼에 적용한 뒤 결과를 흘려보내는 비대화형 스트림 편집기다. vi나 nano처럼 화면을 띄우고 커서를 옮기며 고치는 대신, 스크립트 한 줄로 파일 수천 개를 무인 배치로 일괄 변환할 수 있다.

이 장을 읽기 전에

선행 챕터: 이 장은 13장: grep에서 패턴에 맞는 줄을 찾는 법을 다룬 뒤 이어진다. grep이 패턴을 찾기만 했다면, sed는 찾은 패턴을 그 자리에서 바꾼다 — “찾기"에서 “찾아서 바꾸기"로 넘어가는 자연스러운 전환이다. 정규식 자체(BRE/ERE 메타문자, 캡처 그룹)에 대한 기초 감각이 있으면 예시를 읽기 더 수월하다.

이 장의 깊이: 입문–중급 난이도다. 기본 치환·삭제·출력부터 여러 명령 조합, 주소 범위 지정, 백참조까지 실무에서 자주 쓰는 범위를 다룬다. 다루지 않는 것: 줄 단위를 넘어 필드(컬럼) 단위로 데이터를 다루는 처리는 15장: awk에서 다룬다. sed의 홀드 공간(hold space)을 활용한 다줄 스크립트, 라벨(:label)과 분기(b, t)를 쓰는 고급 스크립팅은 이 장의 범위 밖이며 GNU sed 매뉴얼의 심화 절을 참고해야 한다.

당신의 수준에 맞는 경로

수준읽을 부분핵심 목표
텍스트를 빨리 치환·삭제해야 하는 사람“개요 + 정신 모델”, “사용법 · 옵션”, “예시"의 “기본 치환”·“삭제·출력”s///d, -n+p 조합으로 파일 내용을 즉시 바꾸거나 원하는 줄만 뽑아낼 수 있다
스크립트에 sed를 안전하게 넣으려는 사람“예시"의 “여러 명령 조합”·“주소 범위”, “주의사항·함정” 전체여러 파일을 자동으로 편집하는 스크립트에서 GNU/BSD 이식성 문제 없이 -i를 안전하게 쓸 수 있다

개요 + 정신 모델

sed를 이해하는 가장 빠른 방법은 “이 도구가 파일을 어떻게 본다"는 관점을 바꾸는 것이다. sed는 파일 전체를 메모리에 올려놓고 편집하는 워드프로세서가 아니라, 입력을 줄 단위로 순회하는 파이프라인 필터다. 각 줄을 읽을 때마다 패턴 공간에 넣고, 스크립트에 적힌 명령을 순서대로 적용한 뒤, 명시적으로 -n을 주지 않았다면 그 결과를 자동으로 표준 출력에 흘려보내고 다음 줄로 넘어간다. 이 사이클이 파일 끝까지 반복된다.

이 모델에서 세 가지가 자연스럽게 따라나온다. 첫째, sed는 기본적으로 한 줄 앞뒤를 동시에 보지 못한다 — 각 줄이 독립적으로 패턴 공간에 들어왔다 나가기 때문에, 여러 줄에 걸친 패턴을 다루려면 홀드 공간 같은 별도 메커니즘이 필요하다(이 장의 범위 밖). 둘째, sed는 원본을 직접 수정하지 않는다 — 표준 출력으로 결과를 흘려보내는 것이 기본 동작이며, “파일을 직접 고치는 것처럼 보이는” -i 옵션조차 실제로는 임시 파일에 새 내용을 쓰고 끝에 원본과 바꿔치기하는 방식으로 동작한다(자세한 내용은 아래 흔한 오개념 참고). 셋째, grep이 “매칭되는 줄을 찾아 그대로 보여주는” 도구였다면, sed는 그 매칭 지점에서 치환·삭제·삽입 같은 변형을 가한 뒤 흘려보낸다는 점에서 grep의 자연스러운 다음 단계다.

사용법 · 옵션

1
2
3
sed [옵션] '스크립트' [파일...]
sed [옵션] -e '스크립트1' -e '스크립트2' [파일...]
sed [옵션] -f 스크립트파일 [파일...]
  • 스크립트: 작은따옴표로 감싼 편집 명령. 파일을 생략하면 표준 입력에서 읽는다.
  • 명령을 여러 개 적용하려면 -e를 반복하거나, 스크립트 안에서 세미콜론(;)으로 구분한다.

명령 (스크립트 안에서 쓰는 편집 동작)

명령설명
s/패턴/대체/플래그치환 — 가장 많이 쓰는 명령
d패턴 공간(해당 줄) 삭제 — 자동 출력도 생략됨
p패턴 공간 출력 — 보통 -n과 함께 써서 원하는 줄만 뽑는다
q지금까지 처리한 결과를 출력하고 즉시 종료
a\텍스트해당 줄 다음에 텍스트 추가
i\텍스트해당 줄 앞에 텍스트 삽입
c\텍스트해당 줄(또는 범위)을 텍스트로 치환
y/집합1/집합2/문자 단위 변환(tr과 유사, 정규식 아님)

옵션

옵션설명
-n, --quiet패턴 공간을 자동 출력하지 않음 (p 명령과 조합해 원하는 줄만 출력)
-e SCRIPT스크립트를 추가로 지정(여러 개 조합 가능)
-f FILE스크립트를 파일에서 읽음
-i[SUFFIX]파일을 직접 수정(in-place). GNU와 BSD/macOS의 동작 차이가 커서 아래 주의사항에서 별도로 다룬다
-E, -r, --regexp-extended확장 정규식(ERE) 사용. 기본값은 기본 정규식(BRE)
-s, --separate여러 파일을 각각 독립된 스트림으로 취급(기본은 하나로 이어붙임)

치환 플래그 (s/// 뒤에 붙는 옵션)

플래그설명
g줄 안의 모든 매칭을 치환(기본은 줄당 첫 번째만)
N줄 안의 N번째 매칭만 치환
p치환이 일어났을 때만 결과 출력(-n과 함께 자주 씀)
i, I대소문자 구분 없이 매칭
w FILE치환된 줄을 FILE에 저장

주소 지정 (어느 줄에 명령을 적용할지)

형식설명
NN번째 줄
N,MN번째 줄부터 M번째 줄까지
/정규식/정규식과 일치하는 줄
N,/정규식/N번째 줄부터 정규식이 처음 일치하는 줄까지
N~MN번째 줄부터 M줄 간격으로(GNU 확장, BSD/POSIX sed에는 없음)
$마지막 줄
!주소 뒤에 붙이면 조건 반전(해당하지 않는 줄에 적용)

예시

기본 치환

1
2
3
4
5
6
7
8
# foo를 bar로 치환 (줄당 첫 번째 매칭만)
sed 's/foo/bar/' file.txt

# 줄 안의 모든 foo를 bar로 (g 플래그)
sed 's/foo/bar/g' file.txt

# 대소문자 구분 없이 치환
sed 's/error/ERROR/gi' log.txt

삭제 · 출력

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 빈 줄 삭제
sed '/^$/d' file.txt

# 주석 줄(#으로 시작) 삭제
sed '/^#/d' config.conf

# 2번째 줄만 출력 (-n으로 자동 출력을 끄고 p로 원하는 줄만)
sed -n '2p' file.txt

# 10번째 줄부터 20번째 줄까지만 출력
sed -n '10,20p' file.txt

여러 명령 조합

1
2
3
4
5
# -e를 여러 번 써서 순서대로 두 가지 치환을 적용
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# 세미콜론으로 한 줄에 여러 명령 연결
sed 's/foo/bar/g; s/baz/qux/g' file.txt

주소 범위·조건 반전

1
2
3
4
5
6
7
8
# 5번째 줄부터 10번째 줄까지만 삭제
sed '5,10d' file.txt

# "START"가 나온 줄부터 "END"가 나온 줄까지 삭제
sed '/START/,/END/d' file.txt

# 3번째 줄이 아닌 나머지 줄만 출력
sed -n '3!p' file.txt

캡처 그룹과 백참조

1
2
3
4
5
# 괄호로 감싼 매칭 부분을 \1로 재사용해 순서 바꾸기
sed -E 's/(김|이|박)([가-힣]+)/\2 \1/' names.txt

# 매칭된 전체 문자열을 &로 감싸기(대괄호 추가)
sed 's/[0-9]\+/[&]/g' numbers.txt

슬래시가 많은 경로 치환 (구분자 변경)

1
2
3
4
5
# 구분자를 / 대신 |로 바꿔 경로 안의 슬래시를 이스케이프하지 않아도 되게 한다
sed 's|/usr/local/bin|/opt/bin|g' script.sh

# 구분자로 #도 흔히 쓴다
sed 's#/var/www/old#/var/www/new#g' nginx.conf

파일 직접 수정 (in-place)

1
2
3
4
5
6
7
8
# GNU sed: 접미사 없이 -i만 써도 원본을 그대로 덮어쓴다
sed -i 's/old/new/g' file.txt

# GNU sed: 백업 파일(.bak)을 남기고 수정
sed -i.bak 's/old/new/g' file.txt

# BSD/macOS sed: 백업 없이 수정하려면 빈 문자열을 별도 인자로 명시해야 한다
sed -i '' 's/old/new/g' file.txt

주의사항 · 함정

GNU sed -i와 BSD/macOS sed -i는 접미사 인자 요구 방식이 다르다 — 몰랐다가 macOS에서 파일이 깨지는 사고가 매우 흔하다. GNU sed 매뉴얼은 -i 옵션을 -i[SUFFIX], --in-place[=SUFFIX]로 명시한다. 대괄호가 뜻하는 대로 SUFFIX는 완전히 생략 가능한 선택 인자이고, 매뉴얼 본문도 “If no extension is supplied, the original file is overwritten without making a backup”(접미사를 주지 않으면 백업 없이 원본을 덮어쓴다)라고 밝힌다. 즉 sed -i 's/a/b/' file.txt처럼 접미사 없이 -i만 써도 GNU sed는 정상적으로 원본을 즉시 수정한다.

반면 BSD 계열(macOS 기본 sed 포함) man 페이지는 SYNOPSIS에서 -i extension을 대괄호 없이 표기한다 — -i를 쓰는 순간 그 다음 토큰을 반드시 접미사로 소비한다는 뜻이다. FreeBSD sed(1) man 페이지는 “-i extension … Edit files in-place … If a zero-length extension is given, no backup will be saved"라고 설명하는데, 여기서 “zero-length extension을 줄 수 있다"는 말은 접미사 인자를 아예 생략해도 된다는 뜻이 아니라 빈 문자열('')을 명시적으로 인자 자리에 채워 넣어야 한다는 뜻이다. 이 차이를 모른 채 macOS에서 sed -i 's/a/b/' file.txt를 실행하면, BSD sed는 s/a/b/를 접미사 인자로 오인하고 file.txt를 편집 스크립트로 삼으려 시도한다 — 스크립트도 없이 실행되니 오류가 나거나, 최악의 경우 의도와 다른 파일이 백업 접미사 s/a/b/가 붙은 이름으로 만들어지고 원본은 스크립트로 잘못 해석되며 꼬인다. 크로스 플랫폼 스크립트를 짤 때는 sed -i.bak '...'처럼 접미사를 항상 붙이거나(백업 파일이 남는다), OS를 감지해 GNU면 -i, BSD/macOS면 -i ''를 분기하는 방식이 안전하다. macOS에 GNU sed를 그대로 쓰고 싶다면 Homebrew로 brew install gnu-sed를 설치해 gsed로 실행하는 방법도 있다.

구분자를 / 대신 다른 문자로 바꾸면 경로 치환이 훨씬 읽기 쉬워진다. s///의 구분자는 /로 고정된 것이 아니라 임의의 문자로 바꿀 수 있다. 파일 경로처럼 /가 반복되는 문자열을 치환할 때 기본 구분자를 그대로 쓰면 s/\/usr\/local\/bin/\/opt\/bin/g처럼 슬래시마다 백슬래시 이스케이프가 필요해 가독성이 급격히 떨어진다. 이럴 때는 s|/usr/local/bin|/opt/bin|g처럼 |# 등 패턴 안에 나오지 않을 문자를 구분자로 쓰는 관용구가 표준이다.

BRE와 ERE의 메타문자 이스케이프 규칙이 다르다. sed는 기본적으로 기본 정규식(BRE)을 쓰므로 +, ?, |, (, )는 리터럴 문자로 취급되고, 메타문자로 쓰려면 \+처럼 백슬래시를 붙여야 한다. -E(ERE)를 쓰면 이 이스케이프가 필요 없어지지만, BRE 스크립트와 ERE 스크립트를 섞어 복사·붙여넣기 하다 이스케이프를 빠뜨리거나 반대로 불필요하게 남겨 매칭이 조용히 실패하는 실수가 흔하다.

-i는 원본을 영구적으로 덮어쓴다. 백업 접미사 없이 -i(GNU) 또는 -i ''(BSD)를 실행하면 원본 파일은 즉시 사라지고 복구할 수 없다. 여러 파일에 일괄 적용하기 전에는 반드시 -i 없이 먼저 표준 출력으로 결과를 확인하거나, 백업 접미사를 붙여 실행하는 습관을 들여야 한다.

흔한 오개념

"-i는 파일을 그 자리에서 직접 고친다"는 표현이 실제 구현을 오해하게 만든다. GNU sed 매뉴얼은 -i가 “creating a temporary file and sending output to this file rather than to the standard output”(임시 파일을 만들고 표준 출력 대신 그 파일에 결과를 보낸다)이라고 명시한다. 처리가 끝나면 이 임시 파일이 원래 파일 이름으로 rename된다. 즉 -i도 내부적으로는 “새 파일을 만들고 이름을 바꿔치기"하는 것이지, 원본 파일의 바이트를 그 자리에서 고쳐 쓰는 것이 아니다. 이 사실은 디스크 공간이 부족한 상황에서 -i 편집이 중간에 실패하면 원본이 아예 사라질 수 있다는 실무 위험과도 연결된다.

치환 명령의 &가 리터럴 앰퍼샌드로 출력될 거라 기대하는 실수도 흔하다. s///의 대체 문자열 안에서 &매칭된 전체 문자열을 가리키는 특수 기호다(sed 's/[0-9]\+/[&]/g'처럼 매칭 부분을 괄호로 감싸는 관용구가 여기서 나온다). 대체 문자열에 리터럴 & 글자 자체를 넣고 싶다면 \&로 이스케이프해야 한다.

다음 장에서는

다음은 15장: awk — sed가 줄 단위로 치환·삭제했다면, awk는 줄을 필드(컬럼) 단위로 쪼개 집계·리포트를 만드는 한 단계 더 나아간 텍스트 처리 도구를 다룬다.

평가 기준

  • sed가 각 줄을 패턴 공간에 읽어 스크립트를 적용한 뒤 흘려보내는 비대화형 스트림 편집기라는 정신 모델을 설명할 수 있다.
  • s///의 치환 플래그(g, p, i)와 주소 지정(N,M, /정규식/, !)을 조합해 원하는 줄만 골라 편집할 수 있다.
  • GNU sed의 -i[SUFFIX](접미사 생략 가능)와 BSD/macOS sed의 -i extension(접미사 인자 필수) 차이를 설명하고, 크로스 플랫폼 스크립트에서 안전하게 처리할 수 있다.
  • 경로처럼 /가 반복되는 패턴을 치환할 때 구분자를 바꾸는 관용구(s|old|new|)를 쓸 수 있다.
  • -i가 실제로는 임시 파일을 만들고 rename하는 방식으로 동작한다는 점과, 그로 인한 데이터 손실 위험을 설명할 수 있다.

참고