Regex

문자를 다루는 Regular expression 은 소프트웨어 공학에서 제일 중요하고 기초가 되는 부분입니다. 쉘에서 명령을 실행했을때 발생하는 출력도 문자고, 컴파일러가 파싱을 위해 읽어들이는 소스코드도 프로그래머가 작성한 문자들이기 때문입니다. Regular expression 은 규칙이 있는 표현식이란 의미로 특정 메타문자를 이용해 규칙을 표현해 주면 해당 규칙에 맞는 스트링을 매칭할 수 있습니다. regex 는 보통 3가지로 분류가 됩니다. BRE( Basic Regular Expressions ), ERE( Extended Regular Expressions ), PRE( Perl Regular Expressions ) 인데요. 기본적으로 사용되는 문법은 같은데 기능은 PRE 가 제일 많습니다. 당연히 perl 에서 사용되는데 grep 같은 명령에서도 툴 특성상 부분적으로 제공하고 있습니다. 하지만 보통 대부분의 명령에서 제공하는 것은 BRE, ERE 입니다. 굳이 PRE 까지 기능을 제공하지 않아도 대부분 문제 해결이 되기 때문이기도 하겠습니다.

regex 이 암호문 같다고 싫어하는 사람이 많은데 생각보다 중요하고 많은 능력을 가지고 있습니다.
입력값을 파싱하는데 수십줄 소요되는 코드를 regex 한, 두개로 처리하는 사람도 있습니다.
Regular Expressions 을 테스트할 수 있는 사이트 : https://regex101.com/

BRE, ERE

예전에는 ERE 가 BRE 에 비해 좀더 확장된 기능을 가지고 있었다고 하는데 지금은 기능상의 차이는 없고 단지 syntax 만 다르다고 합니다.

?, +, |, ( ), { }

이 문자들은 regex 에서 특수한 기능을 하는 메타문자 들인데요. 이 문자들을 기본적으로 escape 해서 사용하면 BRE 이고 그렇지 않으면 ERE 입니다.

# BRE
$ echo AA22-CC1234 | sed -n 's/[A-Z]\{2\}[0-9]\+-\([A-Z]\{2\}[0-9]*\)/\1/p'
CC1234

# ERE
$ echo AA22-CC1234 | sed -En 's/[A-Z]{2}[0-9]+-([A-Z]{2}[0-9]*)/\1/p'
CC1234
------------------------------------

# BRE
$ echo ABCD | sed -n '/CC\?\|EE\?/p'
ABCD

# ERE
$ echo ABCD | sed -En '/CC?|EE?/p'
ABCD

위 예제에서는 ERE 가 간결해 보이지만 만약에 스트링에 위의 regex 메타문자가 많이 포함되어 있다면 BRE 로 작성하는게 더 식이 간단해질 수 있습니다. ERE 에서는 모두 escape 해야 되니까요. sed 는 default 가 BRE 입니다. 그러므로 ERE syntax 를 사용하기 위해선 다음과 같이 -E 또는 -r 옵션을 사용해야 합니다.

# BRE 가 더 간단하다.
$ echo '{AA22}+(CC1234)' | sed -n '/{AA22}+(CC1234)/p'
{AA22}+(CC1234)

# ERE 는 모두 escape 해야 한다.
$ echo '{AA22}+(CC1234)' | sed -En '/\{AA22\}\+\(CC1234\)/p'
{AA22}+(CC1234)

$ sed '...'         # BRE (default)

$ sed -E '...'      # ERE

$ sed -r '...'      # ERE

BRE, ERE 공통 문자

다음 문자들는 공통적으로 escape 하지 않고 사용하는 regex 메타문자 인데요. 그러므로 일반 텍스트 문자로 사용하려면 BRE, ERE 모두에서 \ 를 이용해 escape 해야 합니다.

[ ]{ } 를 escape 할 때는 첫 번째 문자만 하면 됩니다.

^, $, *, ., [ \

# BRE (escape 해서 사용)
$ echo '^*.\[]$' | sed 's/\^\*\.\\\[]\$/xxx/'
xxx

# ERE (escape 해서 사용)
$ echo '^*.\[]$' | sed -E 's/\^\*\.\\\[]\$/xxx/'
xxx

Character Classes and Bracket Expressions

[ ] 표현식은 안에 있는 문자들 중에 하나를 의미합니다. 그러므로 다음과 같은 경우 gray or grey 단어가 blue 로 치환됩니다.

sed 's/gr[ae]y/blue/'

- 기호를 사용해서 두 문자를 연결할 수 있는데 이때는 사이에 존재하는 모든 문자를 포함합니다.

gr[a-d]y : gray grby grcy grdy

문자들을 비슷한 의미를 가진 그룹으로 나누어놓은 것이 character classes 입니다. [[:alnum:]] 의미는 [ ] 표현식 내에서 [:alnum:] 이라는 클래스가 사용되어 결과적으로 [A-Za-z0-9] 의미를 가지게 됩니다. 따라서 [[:upper:][:digit:]] 의 의미는 [A-Z0-9] 와 같게 됩니다.

Class Represented Description
[[:alnum:]] [A-Za-z0-9] Alphanumeric characters
[[:alpha:]] [A-Za-z] Alphabetic characters
[[:lower:]] [a-z] Lower-case alphabetic characters
[[:upper:]] [A-Z] Upper-case alphabetic characters
[[:digit:]] [0-9] Numeric characters
[[:xdigit:]] [0-9a-fA-F] Hexadecimal digit characters
[[:space:]] [ \t\n\v\f\r] All whitespace chars (form feed \x0c)
[[:blank:]] [ \t] Space, tab only
[[:punct:]] [!@#$%^&*(){}[]...] 키보드에 있는 숫자, 대,소문자 빼고 모든 문자
[[:graph:]] [!-~] Printable and visible characters
(ASCII 테이블에서 ! 부터 ~ 까지)
[[:print:]] [ -~] Printable (non-Control) characters
(ASCII 테이블에서 space 부터 ~ 까지)
[[:cntrl:]] [\x00-\x19\x7F] Control characters

[[:graph:]][[:print:]] 는 space 를 포함하고 안 하고 차이.

[ ] 표현식 에서 - 를 일반 문자로 사용할 때는 두 문자 사이에 위치하지 않게 처음이나 마지막에 위치시키면 되고 [, ] 문자는 처음에 위치시키면 됩니다.

# 'allow-all' 'vm-process-priority' 와 같은 소문자와 '-' 문자로 이루어진 단어 매칭
[[:lower:]-]+ or [a-z-]+

# 전달된 스트링에서 ']' or '[' or '@' 문자를 삭제 
$ echo '[@foo]' | sed 's/[][@]//g'
foo

# 전달된 스트링이 ']' or '[' or '[:alnum:]' 으로 구성되면 매칭
$ echo '-t[imestamp]' | sed -En '/^-[][[:alnum:]]+$/p'
-t[imestamp

기본적으로 [ ] 표현식 안에서는 *, ?, ., | 같은 regex 메타문자를 escape 하지 않아도 됩니다.

# * + ? [ ] { } ( ) | . ^ $  모두 escape 하지 않아도된다.
$ echo '*+?[]{}()|.^$' | sed -En '/[][{}()*+?|.^$]/p'
*+?[]{}()|.^$
logical NOT 을 나타내는 ^

[ ] 표현식의 맨앞에 ^ 를 위치시키면 logical NOT 의 역할을 합니다.

# '\n' 을 제외한 모든 문자 (newline 만 아니면 매칭 성공)
[^\n]

# 숫자를 제외한 모든 문자 (숫자만 아니면 매칭 성공)
[^[:digit:]] or [^0-9]

# ']', '[', '|', '-' 문자가 아니면 매칭 성공
[^][|-]

Regular expression Extensions

이 기능은 BRE, ERE 모두에서 사용할 수 있습니다. 소문자와 대문자가 같이 있는데 대문자는 소문자의 logical NOT 과 같습니다. 이 확장 기능은 [ ] 에서는 사용할 수 없습니다. 다시 말해서 [\w]\, w 두 문자를 나타냅니다.

\s, \S

\s[[:space:]]\S[^[:space::]] 를 나타냅니다.

\w, \W

\w[[:alnum:]_] 를 나타냅니다. 즉 [:alnum:] 클래스에 _ 가 추가된 것입니다.
(- 는 포함되지 않습니다.)

\W[^[:alnum:]_] 을 나타냅니다.

\b, \B

word boundary 를 나타냅니다.

# \b 의 경우
$ echo 'abc %-= def.' | sed 's/\b/X/g'
XabcX %-= XdefX.

# \B 의 경우
$ echo 'abc %-= def.' | sed 's/\B/X/g'
aXbXc X%X-X=X dXeXf.X

\<, \>

\< 는 word boundary 인데 word 의 시작 부분을, \> 는 word 의 끝부분을 나타냅니다.

# '\<' 의 경우
$ echo "abc %-= def." | sed 's/\</X/g'
Xabc %-= Xdef.

# '\>' 의 경우
$ echo "abc %-= def." | sed 's/\>/X/g'
abcX %-= defX.

Subexpressions and Back-references

regex 에서 ( ) 를 하면 subexpression 이 됩니다. 이 subexpression 은 { }, +, * 와 같은 수량 한정자와 같이 사용될 수 있습니다.

$ echo "aaa@bbb@" | sed -En '/^\w+@$/p'

$ echo "aaa@bbb@" | sed -En '/^(\w+@){2}$/p'
aaa@bbb@

$ echo "aaa@bbb@" | sed -En '/^(\w+@)+$/p'
aaa@bbb@

# 중복 매칭 구하기, 'sed' 단어가 2번 이상 포함될 경우만 프린트
$ echo "111sed22sss333" | sed -En '/(sed.*){2,}/p' 

$ echo "111sed22sed333" | sed -En '/(sed.*){2,}/p' 
111sed22sed333

# sed 두 단어가 붙어 있을 수도 있으므로 '.*' 를 사용
$ echo "111sedsed333" | sed -En '/(sed.*){2,}/p' 
111sedsed333

또한 ( ) 에서 OR 연산자를 사용할 수 있습니다.

$ echo 'AA11 AA22 AA33 AA44 AAAA' | sed -E 's/AA(11|22|[^0-9]{2})/XX/g'
XX XX AA33 AA44 XX

# %pK 는 놔두고 %p 만 %pK 로 변경
$ echo '%pK %p-%p' | sed -E 's/%p([^K])/%pK\1/g'
%pK %pK-%p

$ echo '%pK %p-%p' | sed -E 's/%p([^K]|$)/%pK\1/g'
%pK %pK-%pK
---------------------------------------------------------------------

# 다음 두 식은 같은 결과를 같습니다. 
# 참고로 [ ] 안에서 '.' 문자를 사용할땐 escape 하지 않아도 되는걸 알 수 있습니다.
$ echo '__123-45.67__' | sed -En '/^(0|1|2|3|4|5|6|7|8|9|\.|_|-)+$/p'
__123-45.67__

$ echo '__123-45.67__' | sed -En '/^[0-9._-]+$/p'
__123-45.67__

( ) 를 하면 back-reference 를 위한 번호가 매겨지고 \1 \2 \3 와 같은 형태로 참조할 수 있습니다.

$ echo "aaa@bbb@ccc" | sed -E 's/(\w+)@(\w+)@(\w+)/\1/'
aaa

$ echo "aaa@bbb@ccc" | sed -E 's/(\w+)@(\w+)@(\w+)/\2/'
bbb

$ echo "aaa@bbb@ccc" | sed -E 's/(\w+)@(\w+)@(\w+)/\3/'
ccc

이 back-reference 번호는 regex 에서도 사용될 수 있습니다.

# 여기서 '\1' 은 (\w+) 를 나타내는 것이 아니고 매칭 된 결과 그러니까 aaa 를 나타냅니다.
# aaa 와 ccc 는 틀리므로 매칭이 안됨
$ echo "aaa@bbb@ccc" | sed -En '/(\w+)@\w+@\1/p'

# aaa 와 aaa 는 매칭이 되므로 내용이 프린트가 된다
$ echo "aaa@bbb@aaa" | sed -En '/(\w+)@\w+@\1/p'
aaa@bbb@aaa

# 이번에는 word boundary 로 'sed' 단어가 2번 이상 포함될 경우만 프린트
$ echo "111 sedsed 33" | sed -En '/(\bsed\b).*\1/p' 

$ echo "111 sed sed 33" | sed -En '/(\bsed\b).*\1/p' 
111 sed sed 33

( ) 는 nesting 해서 사용할 수 있습니다. 바깥쪽 괄호가 작은 번호가 됩니다.

$ echo "aaa@bbb@ccc" | sed -E 's/(((\w+)@\w+)@\w+)/\1/'
aaa@bbb@ccc

$ echo "aaa@bbb@ccc" | sed -E 's/(((\w+)@\w+)@\w+)/\2/'
aaa@bbb

$ echo "aaa@bbb@ccc" | sed -E 's/(((\w+)@\w+)@\w+)/\3/'
aaa

regex 로 매칭된 전부를 나타내는 & 문자

$ echo '111 AB 222 CD 333' | sed -E 's/.*/X&X/'
X111 AB 222 CD 333X

$ echo '111 AB 222 CD 333' | sed -E 's/[A-Z]+/X&X/'
111 XABX 222 CD 333

$ echo '111 AB 222 CD 333' | sed -E 's/[A-Z]+/X&X/g'
111 XABX 222 XCDX 333

$ echo '111 AB 222 CD 333' | sed -E 's/[A-Z]+.*/X&X/'
111 XAB 222 CD 333X

*+ 는 greedy 하므로 주의가 필요하다.

regex 에서 *, + 를 이용하여 매칭을 시도할 때 한가지 문제점이 있는데요. 가령 아래와 같은 라인이 있을 때 /AA.*AA/ or /AA.+AA/ 로 매칭을 시도하면 어디까지 매칭을 해야 되는가 하는 것입니다.

AA 111 AA 222 AA 333 AA              # lazy matching
^^^^^^^^^

AA 111 AA 222 AA 333 AA              # greedy matching
^^^^^^^^^^^^^^^^^^^^^^^

regex 에서는 기본적으로 두번째 greedy (longest) matching 을 합니다.
따라서 regex 를 작성할 때 다음과 같은 경우 주의할 필요가 있습니다.

# 숫자 부분을 추출하려고 하는데 마지막 문자만 표시된다.
$ echo "abc123" | sed -En 's/.*([0-9]+)/\1/p'
3

# 첫 번째 'img' 태그를 삭제하려고 하는데 '<html>' 만 남고 모두 삭제된다. 
$ echo '<html><img src="file1.png">abc<img src="file2.png"></html>' |
  sed -En 's/<img.*>//p'
<html>

# 앞쪽의 'aa=11,xx=00,bb' 를 'cc' 로 변경하려고 하는데 마지막 'bb' 까지 매칭이 된다.
$ echo 'aa=11,xx=00,bb=22,aa=33,yy=00,bb=44' | sed -E 's/aa.*bb/cc/'
cc=44

sed 에서 lazy matching 을 하려면 다음과 같이 하면됩니다.

$ echo "abc123" | sed -En 's/[^0-9]*([0-9]+)/\1/p'
123

$ echo '<html><img src="file1.png">abc<img src="file2.png"></html>' |
  sed -En 's/<img[^>]*>//p'
<html>abc<img src="file2.png"></html>

$ echo 'aa=11,xx=00,bb=22,aa=33,yy=00,bb=44' | sed -E 's/aa[^b]*bb/cc/'
cc=22,aa=33,yy=00,bb=44

# 다음은 앞쪽이 아니라 뒤쪽을 cc 로 바꿀경우
# 마지막 '=44' 는 regex 매칭에는 포함되지 않은 hidden 이다.
$ echo 'aa=11,xx=00,bb=22,aa=33,yy=00,bb=44' | sed -E 's/(.*)aa.*bb/\1cc/'
aa=11,xx=00,bb=22,cc=44

# 다음의 경우는 두 개의 greedy (.*) 가 사용되었는데요.
# 결과적으로 앞쪽이 먼저 처리되는 것을 볼수 있습니다.
$ echo 'aa=11,xx=00,bb=22,aa=33,yy=00,bb=44' | sed -E 's/aa.*bb(.*)/cc\1/'
cc=44

특정 스트링을 lazy 매칭하는 방법은 이곳 에서 볼 수 있습니다.

*+ 의 backtracking 기능

*+ 는 임의의 수량을 나타내기 때문에 만약에 다음에오는 값이 매칭이 안될경우 매칭하기 위해 다시 뒤로 갈수가 있습니다. 일종의 고급 기능으로 특정 상황에서 유용하게 사용될 수 있습니다.

$ echo  'model <dell,847483992,16G> AMD' | sed -En 's/<[^>]*/</p'
model <> AMD

$ echo  'model <dell,847483992,16G> AMD' | sed -En 's/<[^>]*,/</p'     # "," 추가 
model <16G> AMD

1. 먼저 <[^>]* 식에 의해서 <dell,847483992,16G 까지 매칭이 됩니다.

2. 그런데 <[^>]* 식 뒤에 "," 가 와서 "> AMD" 와 매칭이 안되므로 매칭이 될때까지 
   다시 backtracking 을 하게되어 <dell,847483992, 에서 매칭이 되게 됩니다.

다음은 Go 언어에서 사용되는 함수 형식인데 backtracking 을 이용해 함수명을 추출하는 예입니다.

func TeeReader(r Reader, w Writer) Reader { ...
func (l *LimitedReader) Read(p []byte) (n int, err error) { ...
func Print[T1 any, T2 any](a T1, b T2) { ...

$ echo 'func (l *LimitedReader) Read(p []byte) (n int, err error) { ...' | 
    sed -En 's/^func[^{]* (\w+)[[(].*/\1/p'
Read

1. 먼저 ^func[^{]* 식에 의해 func 에서부터 { 문자 앞부분까지 매칭이 됩니다.
2. 그다음 ^func[^{]* 식에 이어서 " (\w+)[[(].*" 이 오는데 "{" 와는 매칭이 안되죠.
3. 따라서 매칭이 될때까지 backtracking 을 하면 ... Read( 에서 매칭이 되게 됩니다.

.*.+ 는 greedy 하지만 구분자로 사용되는 , 문자가 사이에 존재하면 매칭이 되어 필드값을 추출할 수가 있습니다.

$ echo "111,222,333,444" | sed -En 's/(.*),(.*),(.*),(.*)/\1:\2:\3:\4/p'
111:222:333:444

1. 첫번째 (.*) 식에 의해 111,222,333,444 전체가 매칭이 됩니다.
2. 그런데 다음에 "," 가 오므로 backtracking 을 하면 111,222,333, 에서 매칭이 됩니다.
3. 두번째 (.*) 식에 의해 앞서 매칭된 111,222,333, 와 444 가 매칭이 됩니다.
4. 그런데 다음에 또 "," 가 오므로 매칭이 실패하게 되어 첫번째 와 두번째 (.*) 가 모두
   다시 backtracking 을 하게 되어 첫번째는 111,222, 에서 매칭이 되고 두번째는
   333, 에서 매칭이 되게 됩니다.
5. 세번째 (.*) 식에 의해 앞서 매칭된 111,222, 와 333, 와 444 가 매칭이 됩니다.
6. 그런데 다음에 또 "," 가 와서 매칭이 실패하게 되어 ...
( 기본적인 로직이 이렇다는 것이고 실제는 빠른 처리를 위해 최적화가 이루어집니다.)

regex 를 이용한 추출

입력값에서 특정 스트링만 추출할 때는 grep -oawk 명령을 사용하는 것이 편리하지만 sed 에서도 regex 을 이용해 추출을 할 수가 있습니다.

# 다음은 스트링에 숫자가 포함되어 있을 경우 특정 위치의 숫자만 추출합니다.

$ echo "aaa123bbb456ccc789ddd" | sed -E 's/([a-z]+([0-9]+)).*/\2/'        # 첫번째 숫자
123
$ echo "aaa123bbb456ccc789ddd" | sed -E 's/([0-9]+).*|./\1/g'
123

$ echo "aaa123bbb456ccc789ddd" | sed -E 's/([a-z]+([0-9]+)){2}.*/\2/'     # 두번째 숫자
456

$ echo "aaa123bbb456ccc789ddd" | sed -E 's/([a-z]+([0-9]+)){3}.*/\2/'     # 세번째 숫자
789

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

# "숫자%" 로 구성된 항목만 추출
$ df -h | sed -E 's/([0-9]+%)|./\1/g'
0%
1%
49%
1%

$ echo '/dev/sda1 511% 7.1M 504M 24% /boot/efi' |
    sed -E 's/([0-9]+% )|./\1/g' 
511% 24%

1. "/dev/sda1 " 앞부분은 ([0-9]+% ) 식과 매칭이 되지 않으므로 "." 와 매칭이 되어 출력에서 제외가 됩니다.
2. "511% " 는 ([0-9]+% ) 식과 매칭이 되어 \1 에의해 출력에 남게 됩니다.
3. "7.1M 504M " 부분도 ([0-9]+% ) 식과 매칭이 되지 않으므로 "." 와 매칭이 되어 출력에서 제외가 됩니다.
4. "24% " 는 ([0-9]+% ) 식과 매칭이 되어 \1 에의해 출력에 남게 됩니다.
5. "/boot/efi" 부분도 ([0-9]+% ) 식과 매칭이 되지 않으므로 "." 와 매칭이 되어 출력에서 제외가 됩니다.

# 다음은 "[숫자]" or "<숫자>" 형태만 추출합니다.
$ echo "aa [12] bb <34> cc [5X6] dd <78> ee [99] ff" | 
    sed -E 's/(\[[0-9]+] |<[0-9]+> )|./\1/g'
[12] <34> <78> [99]

{ } 수량 한정자

*{,} 와 같고 +{1,} 과 같습니다.

$ echo "XYXYXY12345" | sed -E 's/^(XY)*/ZZZ/' 
ZZZ12345
$ echo "XYXYXY12345" | sed -E 's/^(XY){,}/ZZZ/'
ZZZ12345
$ echo "XYXYXY12345" | sed -E 's/^(XY)+/ZZZ/'
ZZZ12345
$ echo "XYXYXY12345" | sed -E 's/^(XY){1,}/ZZZ/'
ZZZ12345

$ echo "12345" | sed -E 's/^(XY)*/ZZZ/'
ZZZ12345
$ echo "12345" | sed -E 's/^(XY){,}/ZZZ/'
ZZZ12345
$ echo "12345" | sed -E 's/^(XY)+/ZZZ/'
12345
$ echo "12345" | sed -E 's/^(XY){1,}/ZZZ/'
12345

?{,1} 와 같습니다.

$ echo "XY12345" | sed -E 's/^(XY)?/ZZZ/' 
ZZZ12345
$ echo "XY12345" | sed -E 's/^(XY){,1}/ZZZ/' 
ZZZ12345
$ echo "12345" | sed -E 's/^(XY)?/ZZZ/' 
ZZZ12345
$ echo "12345" | sed -E 's/^(XY){,1}/ZZZ/' 
ZZZ12345

hidden 에 주의

regex 매칭에 포함되지 않는 부분은 hidden 으로 출력에 포함되므로 주의해야 합니다.

# 여기서 'AAA' 는 매칭에 포함되지 않은 hidden 이다.
$ echo 'AAA CONFIG_FOO BBB' | sed -E 's/(CONFIG_\w+).*/\1/'
AAA CONFIG_FOO

# hidden 이 생기지 않게 하려면 다음과 같은 방법으로 전체 라인을 매칭을 해야 한다.
$ echo AAA CONFIG_FOO BBB | sed -E 's/.*(CONFIG_\w+).*/\1/'
CONFIG_FOO

# 여기서는 2017 이 hidden 이고 greedy '.*' 에 의해 1000 까지 매칭이 된다.
$ echo '2017-01-19:31:51 15333 37723 - MATCH: 1000 [text]' |
  sed -E 's/-.* ([0-9]+) .*/-\1/'
2017-1000

다음은 strace 출력에서 함수 이름을 추출하려고 한것인데요. 출력 결과를 보면 마지막 라인은 표시되지 말아야 하는데 출력이 되고 있습니다.

$ cat some.strace
26074 exit(0)                           = ?
26074 +++ exited with 0 +++
26073 close(65)                         = 0
26073 gettid()                          = 26073
26074 <... madvise resumed> )           = 0
26073 gettid()                          = 26073
26073 <... futex resumed> )             = -1 EAGAIN (Resource temporarily unavailabl

$ sed -En 's/[0-9]+ (\w+).*/\1/p' some.strace
exit
close
gettid
gettid
26073 <... futex resumed> )             = -EAGAIN

26073 <... futex resumed> ) = - 부분이 hidden 이 되기 때문입니다.

$ sed -En 's/[0-9]+ (\w+).*/XXX\1YYY/p' some.strace
XXXexitYYY
XXXcloseYYY
XXXgettidYYY
XXXgettidYYY
26073 <... futex resumed> )             = -XXXEAGAINYYY

따라서 다음과 같이 앞에 ^ anchor 를 붙여주어야 합니다.

$ sed -En 's/^[0-9]+ (\w+).*/\1/p' some.strace
exit
close
gettid
gettid

예제 1

다음 데이터는 CSV( comma-separated values ) 인데요. 여러 가지 방법을 이용해서 필드의 위치를 변경합니다.

# 2번째 필드와 3번째 필드 바꾸기
# 여기서 매칭이 안되는 111, 444 는 hidden 이다.
$ echo '111,222,333,444' | sed -E 's/,(.*),(.*),/,\2,\1,/'
111,333,222,444

# 4번째 필드를 1번째로, 1,2번째 필드를 마지막으로
# '.*' 가 복수개 이상 사용되면 첫 번째 것이 greedy 가 된다.
# 결과적으로 매칭은 (111,222),(333),(444)
$ echo '111,222,333,444' | sed -E 's/(.*),(.*),(.*)/\3,\2,\1/'
444,333,111,222

# 4번째 필드와 1번째 필드 바꾸기
$ echo '111,222,333,444' | sed -E 's/(.*),(.*),(.*),(.*)/\4,\2,\3,\1/'
444,222,333,111

예제 2

IP 주소는 4 개의 숫자로 이루어지는데 각각 범위가 0 ~ 255 까지입니다.
regex 을 이용해서 IP 주소가 맞는지 확인하는 것입니다.

$ sed -En '/(([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]{1,2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])/p' <<\@
192.168.0.1
127.0.0.1
123.456.789.101      # 456 789 는 IP 주소가 아니다.
10.1.2.4
@

192.168.0.1
127.0.0.1
10.1.2.4

예제 3

다음은 "[숫자 - 숫자]" 형태의 필드에서 첫 번째 숫자만 추출해서 표시합니다.

# 숫자에 소수점이 포함되므로 '[0-9.]' 을 사용
$ echo '10368,"Verizon DSL",DSL,NY,NORTHEAST,-5,-4,"[1.1 - 3.0]","[0.384 - 0.768]"' |
sed -E 's#"\[([0-9.]*)[^"]*"#"\1"#g'

10368,"Verizon DSL",DSL,NY,NORTHEAST,-5,-4,"1.1","0.384"

예제 4

두 번째 double quote 으로 둘러싸인 값을 [ ] 안으로 넣고 각각의 값들을 comma 로 분리하여 double quote 합니다.

# Before
{"name": "john , jane, gordon ,matthew"} 

# After    
{"name": ["john","jane","gordon","matthew"]}

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

$ sed -E '
    s/"([^"]+)".*"([^"]+)"/"\1": ["\2"]/    # quotes 으로 둘러싸인 첫번째, 두번째값 분리
    s/ ?, ?/","/g                           # comma 로 분리된 각각의 값들을 quote 처리
' file

{"name": ["john","jane","gordon","matthew"]}

예제 5

이번 예제는 ( ) 의 nesting 기능과 .* 의 greedy 한 속성을 이용해서 데이터를 변경합니다.

# Before
     //svn.server.address/repos/module1/branches/issue-001-name1
     //svn.server.address/repos/module2/branches/issue-002-name2
     //svn.server.address/repos/module3/branches/issue-003-name3

$ sed -E 's#\s+(.*/(.*))#svn co \1 \2#' file

# After
svn co //svn.server.address/repos/module1/branches/issue-001-name1 issue-001-name1
svn co //svn.server.address/repos/module2/branches/issue-002-name2 issue-002-name2
svn co //svn.server.address/repos/module3/branches/issue-003-name3 issue-003-name3

Quiz

사용자에게 C/C++ 함수의 원형을 입력받아서 매칭이 존재하는지 확인하는 스크립트를 만들려고 합니다. sed 명령문을 어떻게 작성하면 좋을까요?

  1. C/C++ 함수의 원형에는 regex 에서 사용되는 (, ) 문자가 존재하므로 ERE 보다는 BRE 로 작성하는것이 좋겠습니다.

  2. 그리고 * , . , [ 문자는 BRE, ERE 모두에서 regex 문자로 사용되므로 escape 해줘야 합니다.

# C/C++ 함수의 원형에는 regex 에서 사용되는 ( , ) , [ , * 문자가 존재한다.

int foobar(const char*, int (*) [3])

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

# 사용자가 위의 함수 원형을 입력했다고 가정하고 변수에 대입해서 매칭을 해보겠습니다.
$ input='int foobar(const char*, int (*) [3])'

# sed 명령문을 BRE 로 작성하였지만 매칭이 되지 않는다.
$ echo 'int foobar(const char*, int (*) [3])' | sed -n '/'"$input"'/p'
$

# shell 의 매개변수 확장 기능을 이용해 * 문자를 \* 로 [ 문자를 \[ 로 escape 해 줍니다.
$ input=${input//\*/\\*}
$ input=${input//[/\\[}
$ echo "$input"
int foobar(const char\*, int (\*) \[3])

# 이제 정상적으로 매칭이 되는것을 볼 수 있습니다.
$ echo 'int foobar(const char*, int (*) [3])' | sed -n '/'"$input"'/p'
int foobar(const char*, int (*) [3])