Sed basics

stream 에디터의 장점은 아무리 사이즈가 큰 파일이라도 처리하는데 메모리를 거의 사용하지 않는다는 점입니다. 일반 텍스트 에디터의 경우에는 파일 내용을 모두 메모리로 읽어 들여야 하지만 stream 에디터의 경우는 한 번에 하나의 라인만 읽어들이면 되기 때문입니다.

스트림 에디터가 가지는 단점은 한번 입력된 라인이 지나가면 다시 사용할 수가 없다는 것입니다. 그러므로 텍스트 에디터에서처럼 특정 라인을 손쉽게 앞부분으로 옮길 수가 없습니다. 그렇다고 불가능한 것은 아니고 버퍼에 이전 라인을 저장해놓는 과정이 필요합니다.

쉘 스크립트에서 사용되는 sed 명령의 첫인상은 난해하다, regex 에 더해서 암호문 같다. 정도 일 텐데요. 이것은 sed 가 가지는 몇 가지 특징에 기인하는 것으로 알고 보면 실제 구성은 간단한 것을 알 수 있습니다. sed 는 원시적인 프로그래밍 언어로 생각해도 될 정도로 프로그래밍 언어에서 볼수 있는 문장 종료 문자 ;, 명령 그룹을 구성할 수 있는 { }, branch 기능을 모두 제공합니다.

sed 는 모든 명령을 하나의 문자로 표시합니다.

프로그래밍 언어에서는 함수명을 이용해서 print, delete 와 같이 작성하지만 sed 에서는 p, d 와 같이 단일 문자를 사용합니다. 이것은 sed 가 제공하는 명령이 몇개 안되는 이유도 있지만 sed 는 기본적으로 shell 에서 스크립트를 작성할때 간단히 사용하기 좋게 만들어졌기 때문입니다. ( 명령 실행시 출력 결과를 잠시 조작하려는데 매번 if, else, while ... 프로그래밍을 할수는 없겠죠? )

간혹 sed 를 perl 과 비교하는 경우가 있는데요. perl 은 완전한 하나의 스크립트 언어로 sed + awk + α 라고 할 수 있습니다. 그러니까 sed 나 awk 로 할 수 있는 것은 perl 로도 다 할 수 있고 속도적인 면에서도 그렇게 차이가 나지 않습니다. 하지만 쉘 스크립트에서 sed 를 많이 사용하는 이유는 sed 는 스트림 조작이 필요할때 간단히 사용하기에 좋기 때문입니다.

sed 는 시스템 디렉토리에서 /bin/sed 에 위치합니다. 이것은 sed 가 시스템을 구성하는데 있어 기본 명령이라는 의미입니다. 다시 말해서 sed 는 텍스트 스트림 조작을 위한 전용 명령이라고 생각하면 되겠습니다.

sed 는 모든 텍스트 조작을 regex 을 이용해 합니다.

이것은 바꾸어 말하면 대부분 필요한 텍스트 조작을 regex 를 이용해 처리할 수 있는다는 의미이기도 합니다. regex 이 얼마나 강력한 기능인지 알 수 있는 부분입니다.

다른 언어에서처럼 여러 가지 함수를 제공하지 않습니다.

sed 는 간단히 덧셈, 뺄셈을 할 수 있는 산술연산자도 스트링을 조작하는데 사용할 수 있는 함수도 제공하지 않습니다. 제공하는 명령들을 기능적으로 분류해보면 10 가지 정도뿐이 되지 않습니다. 입력되는 모든 데이터는 텍스트 스트링으로만 취급하고 regex 에 의해 처리됩니다.

sed 가 처리하는 단위는 line 이다.

라인은 마지막 문자가 newline(\n) 으로 끝나는 것을 말하는데요. sed 뿐만 아니라 텍스트 스트림을 처리하는 명령들은 기본적으로 라인 단위입니다. 처리를 위해 버퍼( pattern space )에 데이터를 읽어들일때도 라인 단위이고 출력을 할 때도 라인 단위로 합니다. 한가지 많은 분들이 sed 에 대해 잘못 알고 있는 점은 sed 가 처리를 라인 단위로 한다고 해서 한 번에 하나의 라인만 처리할 수 있다고 생각하는 것입니다. 실제는 그렇지 않고 sed 는 multiple lines 을 처리하는데 필요한 명령과 버퍼를 제공합니다.

Sed 기본 구조

Pattern space 와 Hold space

sed 에는 2 개의 버퍼가 존재합니다. 하나는 pattern space (red) 이고 다른 하나는 hold space (blue) 인데요. 용어는 이렇게 분류해 놓았지만 실제는 사용할수 있는 변수가 2 개 있는 것과 같습니다.

sed 가 입력 스트림으로 부터 라인을 하나 읽어들이면 위치하게 되는 곳이 pattern space 입니다. sed 에서 제공되는 프린트 명령, 수정 명령들이 실행되었을때 적용되는 공간도 pattern space 입니다.

hold space 는 단순히 임시 저장소 역할만 합니다. pattern space 에 있는 데이터를 hold space 로 옮겨놓으면 수정 명령이 실행되었을때 영향을 받지 않고, 나중에 필요할때 다시 pattern space 로 옮겨와 사용할 수 있습니다.

위 그림을 보면 pattern space 를 문자 G 로 표시한 것을 볼수 있는데요. 이것은 hold space 에 있는 라인을 pattern space 으로 옮기는 명령이 g,G 이기 때문입니다. 문자 p 는 print 명령으로 사용되므로 데이터를 hold space 에서 get 한다는 의미에서 g,G 를 생각하면 되겠습니다.

명령 Cycle

위 그림에서 명령의 BEGIN 에서 END 까지를 명령 사이클이라고 합니다. sed 는 기본적으로 명령 사이클을 시작하기 전에 입력 스트림으로 부터 라인을 하나 읽어들여 pattern space 에 저장하는데 어떤 명령은 라인을 읽어들이지 않고 사이클을 시작하기도 합니다. sed 는 명령 사이클 중에 직접 명령을 이용하여 라인을 읽어들일 수도 있습니다.

1. 새로운 사이클을 시작할 때마다 라인을 읽어들이는 경우

입력 스트림으로 부터 라인을 하나 pattern space 로 읽어 들인 후에 BEGIN 부터 명령 사이클을 시작하고 END 를 지나 한 사이클이 종료되는 경우입니다. 이후에는 다시 BEGIN 으로 돌아가 이전과 동일하게 입력 스트림으로 부터 라인을 읽어들이고 새로운 명령 사이클을 시작합니다. 이때 기존의 pattern space 에 있던 데이터는 삭제되고 새로 읽어들인 라인으로 대체됩니다.

2. sed 자체 명령을 이용하여 직접 라인을 읽어들이는 경우

sed 는 사이클 중에 직접 명령을 이용하여 pattern space 로 라인을 읽어들이기도 하고 삭제할 수도 있습니다. 기본적인 명령 사이클을 거칠 경우 기존의 pattern space 내용이 삭제되어 이전 라인을 사용할 수 없게 되므로 필요에 따라서 직접 명령을 이용하여 라인을 읽어들여야 합니다. 위 그림의 예제 같은 경우 sed 명령이 시작하면서 처음 라인을 읽어들인 후 전체 데이터가 처리될 때까지 한번도 END 를 거치지 않고 명령 사이클을 반복하여 모든 데이터를 처리하고 있습니다.

sed 의 default 출력 옵션 -n

sed 는 기본적으로 명령 사이클이 끝날때 현재 pattern space 에 있는 내용을 출력합니다.

$ seq 111 111 555 | sed ''
111
222
333
444
555

이때 2번 라인을 삭제하고 프린트하려면 2d 명령을 사용합니다.

$ seq 111 111 555 | sed '2d'
111
333
444
555

이번에는 2번, 4번 라인을 삭제하겠습니다.

$ seq 111 111 555 | sed '2d; 4d'
111
333
555

이번에는 3번 라인만 남기고 모두 삭제하려면 어떻게 해야 될까요?

$ seq 111 111 555 | sed '1d; 2d; 4d; 5d'
333

물론 이 방법도 틀린 방법은 아닙니다만 처리해야될 데이터가 많을 경우 모두 d 명령을 이용해 삭제할 수가 없습니다. 이때 사용할수 있는 sed 옵션이 -n 입니다. -n 옵션을 사용하면 sed 가 사이클이 끝날때 pattern space 에 있는 내용을 자동으로 출력하지 않습니다. 따라서 원하는 라인을 출력하기 위해서는 직접 p 명령을 사용해야 합니다. 이번에는 -n 옵션을 이용해서 위 예제를 다시 작성해 보겠습니다.

$ seq 111 111 555 | sed -n '3p'
333

이렇게 경우에 따라서 어떤 default 출력 옵션을 사용하느냐에 따라 명령이 간단해질 수가 있습니다.

sed 에서 -n 옵션을 사용하지 않은 상태를 자동 출력 모드 라고 합니다.

sed 는 원시적인 언어다.

sed 를 공부할 때는 1 주일이면 문법을 마스터할 수 있는 프로그래밍 언어로 접근하는 것이 좋습니다. sed 는 문장 종료를 나타내는 ; 문자, 명령 그룹을 구성할수 있는 { }, 그리고 branch 명령을 제공하므로 기본적인 프로그래밍 언어의 형태를 갖추고 있습니다.

명령들이 각 라인별로 위치할 경우는 ; 를 붙이지 않아도 되지만 한 라인에 여러 명령을 연이어 사용할 경우는 ; 를 사용해야 합니다. 특정 address 에 해당하는 명령이 여러개일 경우는 { } 명령 그룹을 사용하고 마찬가지로 뒤에 ; 를 붙여야 합니다.

comment 는 # 문자를 사용하고 라인 마지막까지 실행에서 제외됩니다.

# 해당 address 에 여러개의 명령을 사용하려면 { } 를 사용합니다.
$ seq 5 | sed -n '2p; 4{p;p}; $p'
2
4
4
5

# 명령들이 각 라인별로 위치할 경우는 `;` 를 붙이지 않아도 된다.
LC_ALL=C git merge -s help 2>&1 |
sed -n -e '/[Aa]vailable strategies are: /,/^$/ {
    s/\.$//
    s/.*://
    s/^[     ]*//
    s/[     ]*$//
    p
}'

sed 는 Turing complete 하다

sed 를 처음 사용할 때는 종종 "이건 sed 로 안된다.." 라든가 "여기서 변수를 하나 더 사용할 수 있으면..", "여기서 이런 함수를 사용할 수 있으면 좋을 텐데.." 와 같은 생각이 드는데요. sed 는 연산 결과를 저장할 수 있는 임시 저장공간을 제공하고 조건 분기와 반복을 할 수 있으므로 Turing complete 하다고 합니다. 그러니까 스트링 조작을 하는데 있어서 이론상으로 모든 문제를 sed 로 해결이 가능하다는 이야기입니다. 만약에 Turing machine 의 작동 방법에 대해 궁금하시면 sed 를 하면 됩니다.