리 맥마흔(Lee E. McMahon)이 개발한 스트림 에디터.

역사

리 맥마흔은 [[/cmd/grep]]{grep}의 성공에 자극을 받았고, 파일에서 읽어 들인 텍스트에 대해 간단한 바꾸기 작업을 하는 유사 프로그램인 gres를 작성했다. sed 편집기에서 바꾸기 명령어다. 리는 곧 프로그램을 더 일반화된 버전으로 대체했는데, 'Sed'(세드)라는 스트림 편집기다. Sed는 텍스트가 입력 스트림으로 들어와서 출력 스트림으로 나가기 전까지 일련의 편집 명령어를 적용했다. grepgres는 둘 다 Sed의 특별한 경우에 해당했다. Sed가 사용하는 명령어는 표준 ed 텍스트 편집기에 있는 편집 명령어와 동일하다. Sed는 요즘도 셸 스크립트에서 흔히 사용되는데, 데이터 스트림을 일정한 방식으로 변형할 때 사용된다. 문자를 교체하거나, 공백을 추가하거나, 원하지 않는 공백을 제거하거나, 불필요한 뭔가를 지우는 등의 용도로 쓰인다.

– 유닉스의 탄생 5장

문법

sed는 복잡한 문법을 갖고 있지만, 실제로 가장 흔하게 사용하는 것은 s 명령어다.

s 명령은 다음과 같이 사용한다.

s/regular-expression/replacement/flags

이 때 구분자인 /는 다양한 다른 기호들로 대체해서 사용할 수 있지만 몇 가지 주의할 점이 있다.

/는 디렉토리 경로 구분자이기 때문에 파일 경로를 sed로 작업할 때 /를 쓰면 잘 안 되거나 실수하기 좋다.

  • 내가 자주 사용하는 대체 기호
    • | , #
  • 주의할 필요가 있어서 잘 안 쓰는 대체 기호
    • : : PATH 값을 작업할 때 주의해야 한다.
    • ; : sed에서 ;는 각 명령어를 구분하는 기호이기 때문에 주의해야 한다.
    • ~ : HOME이 생각나서 쓰기가 좀 그렇다.
    • ^ * +
      • sed와 함께 많이 사용하는 정규식을 헷갈리게 한다. 안 쓰는 게 낫다.
    • ! : 부정 의미와 헷갈린다.
    • @ : 이메일 처리할 때 주의할 필요가 있다.

Examples

macOS

 # 여러 파일에서 foo를 bar로 replace하고, orig라는 원본 파일을 남겨둔다
sed -i.orig s/foo/bar/g file1.txt file2.txt

 # file1, file2에서 `if(`나 `for(`를 찾아 모두 `if (`, `for (`로 바꿔준다
sed -i.orig -E 's/(if|for)\(/\1 (/' file1 file2

 # 패턴이 match된 라인에서만 replace 작업을 한다
echo -e '1\n2\n3' | sed -E '/2/ s/./999/'

 # 패턴이 match 되지 않은 라인에서만 replace 작업을 한다
echo -e '1\n2\n3' | sed -E '/2/! s/./999/'

공백 문자 replace

  • space 교체
 # 하위 경로에서 `if(`를 찾아 모두 `if (`로 바꿔준다
ag 'if\(' -l | xargs sed -i.orig 's/if\(/if /'
  • tab 교체
 # 모든 java 파일에서 탭 문자를 찾아 2개의 스페이스로 교체한다
find . -name '*.java' | xargs ag '\t' -l | xargs sed -E -i.orig "s/[[:cntrl:]]/  /g"
  • 개행 문자 삽입
 # 모든 package 단어 위에 공백 1줄을 추가한다
ag '\S\npackage' -l | xargs sed -i '' 's,package,\'$'\npackage,'

& 의 사용

An ampersand (“&”) appearing in the replacement is replaced by the string matching the RE. The special meaning of “&” in this context can be suppressed by preceding it by a backslash. The string “#”, where “#” is a digit, is replaced by the text matched by the corresponding backreference expression (see re_format(7)).

&는 정규식과 매치된 문자열을 의미한다. 따라서 replace 구문에서 다음과 같이 활용할 수 있다.

$ echo 'hello world' | sed 's/o/&&&/g'
hellooo wooorld

$ echo 'hello world' | sed 's/o/<&>/g'
hell<o> w<o>rld

$ echo 'hello world' | sed 's/l[od]/-&-/g'
hel-lo- wor-ld-

만약 &를 그대로 사용하고 싶다면 \&로 escape하면 된다.

$ echo 'hello world' | sed 's/o/&\&/g'
hello& wo&rld

$ echo 'hello world' | sed 's/o/\&/g'
hell& w&rld

실제 활용한 명령어들

 # ){ 를 모두 찾아 ) { 로 고쳐라
ag '\)\{' -l | xargs sed -i.orig 's/){/) {/'

 # 프로젝트 전체에서 for( 를 for ( 로 replace하고, .orig 백업 파일을 생성
ag 'for\(' -l | xargs sed -i.orig 's/for(/for (/'

 # 프로젝트 전체에서 if( 를 if ( 로 replace하고, .orig 백업 파일을 생성
ag 'if\(' -l | xargs sed -i.orig 's/if(/if (/'

 # 위의 두 가지를 동시에 한다
ag '(if|for)\(' -l | xargs sed -i.orig -E 's/(if|for)\(/\1 (/'

 # java 프로젝트 전체에서 tab 문자를 2개의 space로 replace
find . -name '*.java' | xargs ag '\t' -l | xargs sed -E -i.orig "s/[[:cntrl:]]/  /g"

 # 좌우에 스페이스가 없는 -> 를 찾아 스페이스를 추가하라
find . -name '*.java' | xargs ag '\-\>(?=\S)|(?<=\S)\-\>' -l \
    | xargs sed -i.orig -E "s,([^ ])->,\1 ->,; s,->([^ ]),-> \1,"

 # if(, for(, switch( 처럼 제어문과 괄호가 붙어 있는 코드를 찾아 스페이스 하나를 추가하라
ag '\b(if|for|switch|catch|while)\(' -l \
    | xargs sed -i.orig -E "s,(if|for|switch|catch|while)\(,\1 (,"

 # package 바로 윗줄에 공백 라인 하나를 추가하라
ag '\S\npackage' -l | xargs sed -i '' 's,package,\'$'\npackage,'

 # Pattern, Matcher 를 사용한 경우를 제외하고 + 연산자 좌우에 스페이스 하나를 추가하라
find . -name '*.java' | xargs ag '([^\s+i]\+|\+[^\s+)])' \
    | grep -v Pattern | cut -d: -f1 \
    | xargs sed -E -i '' "/Pattern|Matcher/! s/([^ ])\+([^ ])/\1 + \2/g"
문제 해결
 # 모든 {} 을 찾아 사이에 개행 문자를 추가하라.
 # 단 새로 추가된 라인의 인덴트는 윗 줄과 같아야 한다.
ag '\{\}$' -l | xargs sed -E -i '' 's/^( *)(p.+){}/\1\2{\
\1}/'

명령 첫째 줄이 \로 끝나고, 이후 엔터 키를 입력해 개행 문자를 넣고, 다음 줄에서 명령이 끝난다는 점에 주목.

이 명령을 실행하면 다음과 같은 결과를 얻을 수 있다.

// before
  public Code() {}

// after
  public Code() {
  }

GNU SED

 # brew를 이용해 macOS에 설치할 수 있고, 이후 gsed로 사용하면 된다
brew install gnu-sed

참고문헌

  • 유닉스의 탄생 / 브라이언 커니핸 저/하성창 역 / 한빛미디어 / 2020년 08월 03일 / 원서 : UNIX: A History and a Memoir