Substitute

s / regex / replacement / [flags]

sed 에서 pattern space 에 있는 내용을 수정할 때 사용하는 메인 명령으로 여러 flags 를 통해서 다양한 기능을 제공합니다.

i, I 플래그는 차이가 없이 같은 역할을 합니다.
[flags] 자리에 s/foo/bar/{ ... } 와 같이 명령 그룹을 사용할 수 없습니다.

명령 성공에 상관없이 모든 라인이 표시된다

sed 는 기본적으로 명령 사이클이 종료될 때 pattern space 에 있는 내용을 출력하므로 s 명령의 성공에 상관없이 모든 라인이 출력됩니다. 이때 변경에 성공한 라인만 출력하고자 한다면 sed 명령의 -n 옵션과 s 명령의 p flag 를 사용할 수 있습니다.

# 파일 내용
$ cat file
1_11
2_22
X_33
3_44
Y_55

# 기본적으로 's' 명령의 성공에 상관없이 모든 라인이 표시된다.
$ sed 's/^[A-Z]/@@/' file
1_11
2_22
@@_33
3_44
@@_55

# '-n' 옵션과 'p' flag 를 이용하면 's' 명령이 성공한 라인만 표시할 수 있다.
$ sed -n 's/^[A-Z]/@@/p' file
@@_33
@@_55

매칭 번호를 사용하지 않으면 default 는 1 이다.

# 매칭 번호를 사용하지 않았으므로 s/../../1 과 같다
$ echo "11 aa 22 bb 33 cc" | sed -E 's/[a-z]+/X&X/'
11 XaaX 22 bb 33 cc

$ echo "11 aa 22 bb 33 cc" | sed -E 's/[a-z]+/X&X/2'
11 aa 22 XbbX 33 cc

$ echo "11 aa 22 bb 33 cc" | sed -E 's/[a-z]+/X&X/3'
11 aa 22 bb 33 XccX

$ echo "11 aa 22 bb 33 cc" | sed -E 's/[a-z]+/X&X/g'
11 XaaX 22 XbbX 33 XccX

$ echo "11 aa 22 bb 33 cc" | sed -E 's/[a-z]+/X&X/2g'  # 두번째 이후로 전부
11 aa 22 XbbX 33 XccX
-----------------------------------------------------

$ cat file
PHP is a server-side scripting language.
PHP is an open-source language and PHP is case-sensitive.
PHP is platform-independent.

# 두번째 PHP 단어 뒤에 New Text 추가
$ sed 's/\(PHP\)/\1 (New Text added)/2' file
PHP is a server-side scripting language.
PHP is an open-source language and PHP (New Text added) is case-sensitive.
PHP is platform-independent.

# PHP 단어가 2 번 존재하는 라인만 삭제
$ sed -e 's/PHP/PHP/2; tX; b' -e ':X d' file
PHP is a server-side scripting language.
PHP is platform-independent.

regex | (or) 을 사용할 때는 g 플래그를 사용해야 합니다.

$ echo "11 aa 22 bb 33 cc" | sed -E 's/aa|bb/X&X/'
11 XaaX 22 bb 33 cc

$ echo "11 aa 22 bb 33 cc" | sed -E 's/aa|bb/X&X/g'
11 XaaX 22 XbbX 33 cc

Capturing Groups and Backreferences

$ echo "SED language" | sed  's/\(SED\)/Learn \1 programming/'
Learn SED programming language

$ echo "Learn SED language programming" | sed 's/\(language\) \(programming\)/\2 \1/'
Learn SED programming language

$ sed -En '/^(.)(.)(.)\3\2\1$/p' /usr/share/dict/words
redder

& 문자는 매칭된 영역을 나타냅니다.

$ echo "SED Language" | sed 's/[A-Z]\+/Learn & programming/'
Learn SED programming Language

# 일반 문자로 사용하려면 escape 해야합니다.
$ sed 's/user=&uid/user=\&sysuserid/'

대, 소문자 변환을 위한 \U \u \L \l \E

# '\U' 는 이후로 모든 영역을 대문자로 변환합니다.
$ echo "sed language" | sed 's/[a-z]\+/learn \U& programming/'
learn SED PROGRAMMING language

# 만약에 중간 중단하고 싶으면 '\E' 를 사용합니다.
$ echo "sed language" | sed 's/[a-z]\+/learn \U&\E programming/'
learn SED programming language

# '\u' 는 바로 뒤의 첫문자만 대문자로 변환합니다.
$ echo "sed language" | sed 's/[a-z]\+/learn \u& programming/'
learn Sed programming language

# '\L' 는 이후로 모든 영역을 소문자로 변환합니다.
$ echo "SED LANGUAGE" | sed 's/[A-Z]\+/LEARN \L& PROGRAMMING/'
LEARN sed programming LANGUAGE

# 만약에 중간 중단하고 싶으면 '\E' 를 사용합니다.
$ echo "SED LANGUAGE" | sed 's/[A-Z]\+/LEARN \L&\E PROGRAMMING/'
LEARN sed PROGRAMMING LANGUAGE

# '\l' 는 바로 뒤의 첫문자만 소문자로 변환합니다.
$ echo "SED LANGUAGE" | sed 's/[A-Z]\+/LEARN \l& PROGRAMMING/'
LEARN sED PROGRAMMING LANGUAGE
..............................................................

# 다음은 모든 단어의 첫문자를 대문자로 변환합니다.
$ echo "learn sed programming language" | sed -E 's/\b[a-z]+\b/\u&/g'
Learn Sed Programming Language

이전에 사용된 regex 중복 작성 방지하기

명령문을 작성하다 보면 종종 address 에서 사용된 regex 이나 이전 s 명령에서 사용된 regex 이 동일하게 다시 사용되는 경우가 있는데요. 이럴 경우 sed 는 중복 작성을 방지하기 위해 s//.../ 형식을 제공합니다. 여기서 // 부분이 이전에 사용된 regex 으로 대체됩니다.

# 값 123 을 id 값에 붙이기 
$ echo '<pagenum page="normal" id="page">123</pagenum>' |
sed -E '
    /(<pagenum page="normal" id=")(\w+)(">)([0-9]*)(<\/pagenum>)/ {

        # 첫 번째 방법
        # s/(<pagenum page="normal" id=")(\w+)(">)([0-9]*)(<\/pagenum>)/\1\2\4\3\4\5/

        # 두 번째 방법
        # "//" 를 사용하면 중복 작성을 피할 수 있다.
        s//\1\2\4\3\4\5/
    }
'
# 결과 "page123"
<pagenum page="normal" id="page123">123</pagenum>

-------------------------------------------------

# 이전 's' 명령에서 사용된 '[A-Z]' 가 두 번째 's' 명령에서 재사용됨
$ echo "111 A 222 B 333 C 444 D 555" | sed '/111/{ s/[A-Z]/XXX/4; s//YYY/ }'

111 YYY 222 B 333 C 444 XXX 555

/address1/,/address2/ 와 같은 range address 를 사용할 경우 기본적으로 매칭 되는 라인도 함께 출력에 포함됩니다. 하지만 경우에 따라서 매칭 라인은 제외해야 될 때가 있는데요. 이럴 경우 // 를 이용하여 식을 간단히 할 수 있습니다.

$ readelf -WS /bin/date | sed -n '/Section Headers:/,/Key to Flags:/p'      
Section Headers:    <----- 매칭 라인이 출력에 포함됨
  [Nr] Name            Type         Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                 NULL         0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp         PROGBITS     0000000000000318 000318 00001c 00   A  0   0  1
  . . . .
  . . . .
  [26] .gnu_debuglink  PROGBITS     0000000000000000 01a0a0 000034 00      0   0  4
  [27] .shstrtab       STRTAB       0000000000000000 01a0d4 00011d 00      0   0  1
Key to Flags:       <----- 매칭 라인이 출력에 포함됨

# '//d' 으로 매칭 라인을 제거할 수 있습니다.
$ readelf -Wa /bin/date | sed -n '/Section Headers:/,/Key to Flags:/{//d;p}'
  [Nr] Name            Type         Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                 NULL         0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp         PROGBITS     0000000000000318 000318 00001c 00   A  0   0  1
  . . . .
  . . . .
  [26] .gnu_debuglink  PROGBITS     0000000000000000 01a0a0 000034 00      0   0  4
  [27] .shstrtab       STRTAB       0000000000000000 01a0d4 00011d 00      0   0  1

//d 를 사용할때 한가지 주의할 점은 특정 라인부터 마지막까지 출력할 때입니다. 이때는 마지막 라인을 나타내는 $ 를 사용하면 안되고 매칭이 되지 않는 임의의 스트링을 사용해야 합니다.

# 결과가 올바르지 않다. Displaying notes ... 라인이 모두 삭제가 된다.
$ readelf -Wa /bin/date | sed -En '/Displaying notes/,${ //d; p }'

# OK. 첫번째 Displaying notes ... 라인만 삭제가 된다.
$ readelf -Wa /bin/date | sed -En '/Displaying notes/,/\a/{ //d; p }'

/../../ 구분자를 다른 문자로 사용하기

이 기능은 addresses 메뉴에서도 나왔는데요. s 명령에서도 동일하게 이용할 수 있습니다.
차이점은 s 명령에서는 구분자로 사용되는 첫 번째 문자를 escape 할 필요가 없습니다.

# '\' 문자를 모두 escape 해야 되기 때문에 가독성이 떨어진다.
$ sed -E 's/\/usr\/local\/bin\/(\w+)/\1/'

# 구분자로 임의의 문자를 선정해서 사용할 수 있다.
$ sed -E 's@/usr/local/bin/(\w+)@\1@'

$ sed -E 's#/usr/local/bin/(\w+)#\1#'

$ sed -E 's,/usr/local/bin/(\w+),\1,'

\a non-printable 문자의 활용

s 명령을 사용하다 보면 이런 문제가 생기는 경우가 있습니다. 예를 들어 문서에 포함된 스트링을 ab -> bc 로 변경하고 bc -> ab 로 변경하는 것입니다. 두 가지 경우를 모두 s 명령으로 연이어 처리하면 안 되는 것이 ab 를 bc 로 변경한 후에 bc 를 ab 로 변경하면 모두 ab 가 돼 버리기 때문입니다.

이때는 약간 문서 수정을 해야 합니다. 먼저 ab 를 __ab__ 로 변경하고 bc 를 ab 로 변경한후에 마지막으로 __ab__ 를 bc 로 바꾸는 것입니다. 하지만 문서 내에 __ab__ 스트링이 존재할 수도 있기 때문에 주로 사용하는 방법은 non-printable 문자인 \a 를 이용하는 것입니다.

# ab -> bc
# bc -> ab 

$ echo 'abbc' | sed 's/ab/bc/g; s/bc/ab/g'
abab

$ echo abbc | sed 's/ab/\a/g; s/bc/ab/g; s/\a/bc/g'
bcab

\a 문자를 이용하는 방법은 여러 가지 상황에서 활용될 수 있습니다.

w filename 플래그 사용시 주의할 점

filename 은 해당식에서 마지막에 위치해야 하고 뒤에 이어서 다른 문자나 명령이 오면 안됩니다. 이후 연이어서 명령을 사용하려면 -e 옵션을 이용해 식을 분리해야 합니다.

Quiz

입력 라인에서 foozoo 단어 사이에 bar 가 있을 경우만 출력하려면 .* 메타문자를 이용해서 다음과 같이 간단히 처리할 수 있는데요.

$ echo -e 'x foo bar zoo x' | sed -En '/.*foo.*bar.*zoo.*/p'
foo bar zoo

$ echo -e 'x foo xxx zoo x' | sed -En '/.*foo.*bar.*zoo.*/p'
$

반대로 foozoo 단어 사이에 bar 가 없을 경우만 출력하려면 어떻게 할까요?

$ echo -e 'x foo bar zoo x' | sed -En -e 'h; s/.*foo(.*)zoo.*/\1/; tX; b; :X /bar/!{g;p}'
$

$ echo -e 'x foo xxx zoo x' | sed -En -e 'h; s/.*foo(.*)zoo.*/\1/; tX; b; :X /bar/!{g;p}'
x foo xxx zoo x

1. h; 먼저 입력값을 hold space 에 저장해 놓습니다.
2. s/.*foo(.*)zoo.*/\1/ 매칭에 실패하면 b; 명령으로 종료합니다.
   매칭에 성공하면 foo 와 zoo 단어가 있는 것이므로 tX; 명령으로 :X 레이블로 분기합니다.
3. :X 레이블에서는 현재 pattern space 에 남아있는 값에서 bar 있는지 체크하고
   없을 경우 g; 명령으로 저장해둔 원본 값을 가져와 출력합니다.

---------------------------------------------------------------------

# perl 의 경우는 다음과 같이 negative lookahead 를 사용할 수 있습니다.
$ echo -e 'x foo xxx zoo x' | perl -ne '/.*foo(?!.*bar).*zoo.*/ && print'
x foo xxx zoo x

2 .

bc 명령을 이용하면 입력과 출력에 사용되는 base ( 진법 ) 을 마음대로 조정할 수가 있는데요. 출력을 2 진법으로 했을 때 byte 단위로 숫자를 8 개씩 구분해서 출력하려면 어떻게 할까요?

# 입력은 16 진수, 출력은 2 진수로 했을때 byte 단위로 구분하기가 어렵다.
$ echo "obase=2; ibase=16; EAB080" | bc
111010101011000010000000

$ echo "obase=2; ibase=16; EAB080" | bc | sed -E 's/.{8}/& /g'
11101010 10110000 10000000 

$ echo "obase=2; ibase=16; EAB080" | bc | fold -b8
11101010
10110000
10000000

bc 명령을 사용할땐 obase 가 ibase 앞에 와야 합니다.

3 .

다음 file 내용의 마지막 라인을 "Unix and Linux" 로 만들려고 합니다. >> redirection 을 이용해 append 를 하면 결과가 오른쪽과 같이 되는데요. 어떻게 하면 될까요?

$ cat file                          $ echo " and Linux" >> file
Hello                               
Welcome to                          $ cat file
Unix                                Hello
                                    Welcome to
                                    Unix
                                     and Linux

아래 첫 번째 $ 문자는 sed 에서 사용되는 메타문자로 마지막 라인을 의미하고 두 번째 $ 문자는 regex 에서 사용되는 메타문자로 라인의 끝을 나타냅니다. 결과적으로 마지막 라인의 끝에 " and Linux" 가 추가되게 됩니다.

$ sed '$ s/$/ and Linux/' file
Hello
Welcome to
Unix and Linux

4 .

다음 파일 내용을 보면 시작과 끝부분에 여분의 newline 들이 존재하는데요. 제거 하려면 어떻게 할까요?

sh$ cat file

                       <---- newlines
Hello

Welcome to
Unix and Linux

                       <---- newlines

이때는 sed 의 -z 옵션을 사용해 해결할 수 있습니다. sed 의 -z 옵션을 사용하면 파일 내용을 한 번에 전부 읽어 들일 수 있습니다. 그다음 시작 부분 (^) 과 마지막 부분 ($) 의 newline 들을 제거하면 됩니다.

$ sed -Ez 's/^\n+//; s/\n+$/\n/' file
Hello

Welcome to
Unix and Linux