home search
OS
Linux
[Linux] Bash (1): REGEX(정규표현식)
2022. 02. 25

REGEX

REGEX는 Regualr Expression(RE, 정규 표현식)의 약어이다. ‘리젝스, 레젝스’라고 읽는다.

관련된 유틸리티로는 grep, sed, awk 등이 있다.

  • grep: 유닉스에서 가장 기본적인 REGEX 평가 유틸리티. Global Regualr Expression Print의 약어이다.
  • sed: stream editor로, REGEX 기능을 일부 탑재
  • awk: 패턴식 다루기 가능한 언어툴로, 프로그래밍 언어의 일종. 문자열 관련한 방대한 기능 가짐.

정규 표현식에도 여러 종류가 있으며 대표적으로는 POSIX REGEX, PCRE 두 개가 있다.

① POSIX REGEX

UNIX 계열 표준 정규표현식으로, 밑에서 볼 REGEX이다.

간단한 패턴 매칭에 사용하며, 복잡한 패턴에서는 약간의 성능 저하가 발생할 수도 있다. 하지만 이 표현이 표준이므로 POSIX 표현식부터 배우는 것이 좋다.

이는 다시 BRE(Basic RE)와 ERE(Extended RE)로 기법이 나뉜다. BRE는 grep 작동의 기본 값이다. 아무 옵션을 적지 않으면 BRE를 기본으로 작동한다. ERE는 기능적으로는 같으나 더 많은 표현식과 편의성을 제공하며, egrep의 기본 값이다. 이것을 기준으로 배우는 것이 좋다.

② PCRE

Perl Compatible Regualr Expression의 약어로, Perl에서 제공되던 REGEX가 우수하여 다른 언어에서도 제공하기 위해 만들어졌다. C언어로 만들어져 있으며, POSIX REGEX에 추가된 확장 기능 가지고 성능이 더 좋다.

C, C++, Python 등에서 추가 라이브러리의 형태로 대부분의 언어에서 지원된다. 현재는 PCRE2 버전을 사용하며, 실무에선 PCRE를 사용하는 것이 좋다고 한다.

grep

matcher

grep을 실행할 떄는 matcher(matcing을 실행하는 엔진)을 고를 수 있다.

matcher 설명
-G 디폴트 값으로, BRE를 사용해 작동
-E ERE를 사용해 작동. egrep 작동값과 동일
-P PCRE 사용해 작동. pcre2grep 작동값과 동일
-F 고정 길이 문자열 탐색 모드. 잘 안 씀

주요 옵션

옵션 설명
–color 매칭되는 문자열에 색을 칠한다
-o 매칭에 성공한 부분만 잘라서 알려준다
-e Pattern 패턴을 여러 개 사용할 때 쓰나, 사용 빈도는 낮다
-v, –invert-match o 옵션과는 반대로 매치되지 않는 부분을 알려준다. (대문자 V는 버전을 의미)

POSIX REGEX meta char.

검색 몇 가지를 해보자

$ grep --color "z[abcd]\+s" /usr/share/dict/words

z가 나오고, [a-d] 중 적어도 한 번 이상이 나오고, 그 다음에 s가 있는 경우를 찾는다. 참고로 /usr/share/dict/words에는 각종 테스트를 위하여 문자들이 저장되어 있다.

$ grep --color "a[abcd]\+as$" /usr/share/dict/words
$ grep --color "^a[abcd]\+as" /usr/share/dict/words

i18n을 만족하는 리눅스의 경우 UTF-8로 되어 있다면 한글 처리도 가능하다.

$ cat <<HEREDOC >hangul-utf8.txt 
	→ redirection의 HERE document기법
$ egrep --color '한.' hangul-utf8.txt 
$ egrep --color '한[글자]' hangul-utf8.txt
$ egrep --color '..字' hangul-utf8.txt
$ egrep --color '[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]+' hangul-utf8.txt
	→ 한글만 출력

Anchor을 쓸 수도 있다. 각각 그것으로 끝나는, 시작하는 단어를 출력한다.

grep의 다른 옵션을 써서 조금 더 활용할 수 있다. 일례로, grep으로는 log data를 검색할 때 많이 사용하는데, -A 옵션(after)으로 해당 매칭 바로 다음 행을 뽑을 수 있다. 반대로 -B(before)은 전 행을 뽑는다.

$ grep --color -A 1 "param_systemd" exjornal.log

위 코드는 ‘exjornal.log’에서 ‘param_systemd’를 찾아 그 다음 행까지 보여준다. 이처럼 line contol 옵션을 아래와 같다.

옵션 설명
-A # 또는 –after-context=# 매칭되는 곳의 #줄 아래를 출력
-B # 또는 –before-context=# 매칭되는 곳의 #줄 위를 출력
-C # 또는 -# 또는 –context=# 매칭되는 곳의 위아래 #줄을 출력
–group-separator=SEP 해당 구분자를 이용해 N번째 검색을 출력. 디폴트는 ‘–’

⑤ REGEX Greedy/Non-Greedy matching

패턴은 최대한 많은 수의 매칭을 하려고 하는 것을 Greedy matching이라고 한다.

$ var="It's gonna be <b>real</b>It's gonna <i>change everything</i> I feel"
$ echo $var | egrep -o "<.+>"
<b>real</b>It's gonna <i>change everything</i>

위를 살펴보면 출력 결과가 <b>가 아닌 마지막 꺽쇠>까지인 것을 볼 수 있다. 이렇게 최대한 많은 매칭을 찾는 것이 greedy matching이다. 만약 <b>만 뽑고 싶다면 <.+>에서 임의의 문자인 .[^<>]로 바꿔 여집합 기능을 해 뽑을 수 있다.

$ echo $var | egrep -o "<[^<>]+>"
<b>
</b>
<i>
</i>

의도한 대로 태그만 뽑아낸 것을 볼 수 있다.

반대로 Non-Greedy matching도 있다. 최소 매칭 기능으로, POSIX RE에서는 non-greedy matching 수량자가 없어 패턴을 수정해 해당 효과와 같은 결과를 만들 수 있다. 반면 PCRE는 수량자를 제공한다(lazy quantifier). suffix로 ?만 더하면 된다. grep 명령어로 실행할 떄는 PCRE를 쓰기 위해 옵션 -P를 함께 써야 한다. 특이하게 vim은 POSIX BRE를 사용하나, non-greedy 수량자인 \{-}를 제공한다.

먼저 vim의 경우를 보자. 아까 변수로 주었던 부분을 타이핑 하고 두 가지 버전으로 검색을 해본다.

첫 번째 경우에는 BRE이기 때문에 +앞에 역슬래시를 붙인다. 두 번째 경우에는 \{-}를 제공하므로 non-greedy를 사용하라 수 있었다.

이번에는 PCRE에서 Lazy quantifier을 써보자.

$ echo $var | grep -P -o "<.+?>"
<b>
</b>
<i>
</i>

⑤ escape(백슬래시)

백슬래시는 메타 문자의 의미를 없앤다(=’escape 시킴’). 맨 처음에서 그 예시를 본 바가 있다.

또한 BRE에서 ERE 일부 기능을 표현할 때도 사용한다. 일례로 ERE의 {m,n}을 BRE로 표현할 떄는 \{m,n\}으로 하고, ERE의 ?을 BRE로는 \?로 한다. 즉, BRE에서 ?, +, { }, |, ( )에 대해 백슬래시를 사용한다. ERE(egrep 명령어)에서는 역슬래시를 쓸 필요가 없기 때문에 좀 더 쉽다.

$ var3='abc cab cbb ccb zxy cdb clb c2b c.b c*b 123'
$ echo $var3 | grep --color '[0-9bc]*' 
$ echo $var3 | grep --color '[0-9bc] +'
    → +가 ERE이므로 아무것도 뜨지 않음
$ echo $var3 | grep --color '[0-9bc] \+'\+으로 역슬래시 붙여주어 성공

$ var4='URLS : http://asdf.com/en/ , https://asdf.com/en/'
$ echo $var4 | grep --color 'http://[A-Za-z./]*' 
URLS : http://asdf.com/en/ , https://asdf.com/en/

$ echo $var4 | grep --color 'http://[A-Za-z./]+' 
    → +가 ERE이므로 아무것도 뜨지 않음

$ echo $var4 | grep --color 'http://[A-za-z./]\+' 
URLS : http://asdf.com/en/ , https://asdf.com/en/
	→ \+으로 역슬래시 붙여주어 성공

$ echo $var4 | egrep --color 'http://[A-Za-z./]+' 
URLS : http://asdf.com/en/ , https://asdf.com/en/
    → egrep은 ERE로 작동하므로 \ 없이도 작동

‘www.naver.com’ 페이지에서 URL 링크를 추출해보자. ⭐ curl : 터미널에서 웹 페이지를 접속하는 유틸

$ curl -s https://www.naver.com | egrep -o 'http://[0-9A-Za-z.]+/' 
    → ERE를 사용하는 egrep은 +앞에 \를 사용하지 않아도 된다
http://news.naver.com/
http://entertain.naver.com/
http://sports.news.naver.com/
http://news.naver.com/
http://m.sports.naver.com/
http://newsstand.naver.com/
(이하 생략)

$ curl -s https://www.naver.com | egrep --color 'http://[0-9A-Za-z.]+/' 

이번에는 중복되는 링크를 제거해본다.
sort
정렬한 결과를 보여준다. ⭐ unique
유일한 값들로 추려 보여준다. 둘을 동시에 쓸 땐 sort -u로 줄일 수도 있다. ```bash (1) ERE ver. $ curl -s https://www.naver.com | egrep -o ‘http://[0-9A-Za-z.]+/’ | sort | uniq

(2) BRE ver. $ curl -s https://www.naver.com | grep -o ‘http://[0-9A-Za-z.]+/’ | sort | uniq

http://entertain.naver.com/ http://m.sports.naver.com/ http://news.naver.com/ http://newsstand.naver.com/ http://sports.news.naver.com/ http://whale.naver.com/


`img` 태그만 추출해볼 수도 있다.
```bash
(1) ERE ver.
$ curl -s https://www.naver.com | egrep -o "<img [^<>]+>"

(2) BRE ver.
$ curl -s https://www.naver.com | grep -o "<img [^>]\+>"

BRE/ERE를 기본으로 사용하는 명령어를 정리하면 아래와 같다.

  • BRE를 사용: grep, vim, sed 등
  • ERE를 사용: egrep, awk / PCRE는 ERE를 베이스로 함

⑥ 소괄호(parenthesis)

소괄호에는 두 가지 기능, back-reference와 group / alternation 이 있다. 둘은 동시에 사용된다.

back-reference는 매칭된 결과를 다시 사용하는 패턴으로,( )로 묶인 패턴을 \# 식으로 재사용할 수 있다. #엔 숫자가 순서대로 나오고, 0번은 전체 매칭 결과이다.

/etc/passwd란 파일에는 유저의 정보를 담고 있다. :를 구분자로 하여 7개의 필드로 구성되어 있는데, 루트 유저나 시스템 유저가 아닌 일반 유저의 경우, 홈 디렉토리 경로가 /home/user_ID 식으로 되어있다. 따라서 일반 유저만 이 패턴으로 골라내고 싶다면 아래와 같이 사용한다.

$ egrep "^(.+):x:[0-9]+:[0-9]+:.*:/home/\1:" /etc/passwd

제일 처음에 (.+)를 통해 유저 아이디를 패턴으로 넣었고, 홈 디렉토리 바로 뒤에\1를 넣어 해당 패턴을 재사용했다. 처음 쓰이므로 1을 넣은 것이다. 또한 앞에 붙은 유저 아이디가 달라져도 똑같은 명령어를 매 행마다 적용할 수 있다.

앞에서 본 태그 추출 예제를 다시 사용해보자. 이번에는 시작 태그와 끝 태그로 묶인 부분을 추출하고자 한다.

$ var2="It's gonna be <b>real</b>It's gonna <i>change everything</i> I feel"
$ echo $var2 | egrep -o '<([a-zA-Z0-9]+)>.*</\1>'
$ echo $var2 | egrep --color '<([a-zA-Z0-9]+)>.*</\1>'

패턴 group을 묶거나 alternation을 위해서도 사용된다. 괄호 안에 |을 넣는 것이다.

$ echo "cat and dog" | egrep -o "(cat|dog)"
cat
dog

$ echo "My Childhood~~~ bye bye" | egrep -o "(child|boy)?hood"
hood

위 예에서 볼 수 있듯 cat 혹은 dog 이 있으면 출력하고, child 혹은 boy가 있으면 출력한다. 그러나 이 때 대소문자를 구별하여 뒷 예제에서는 Childhood로 출력하지 않음에 주의한다.

이번에는 <a> 태그로 감싸진 부분을 추출하되, ERE와 BRE 두 버전으로 해보자. BRE를 쓸 때는 |()을 이스케이프 시켜줘야 한다.

(1) ERE ver.
$ curl -s https://www.naver.com | egrep -o "<(a|A) [^>]+>.</\1>"

(2) BRE ver.
$ curl -s https://www.naver.com | grep -o "<\(a\|A\) [^<>]\+>.\+</\1>"

jpg, png 확장자를 가지는 URL 링크만 추출해본다. 확장자는 대문자일 수도 있으므로 alternation 기능을 통해 둘 다 잡아낸다.

$ curl -s https://www.naver.com | egrep -o 'https://[0-9A-za-z./_]+\.(jpg|JPG|png|PNG)' 

여기서 쓰인 \..을 이스케이프시킨 것이며, -i 옵션으로 대소문자 구별을 하지 않도록 해도 된다.

IPv4 주소를 표현하면 아래와 같다.

(2[0-5][0-5]|1[0-9][0-9]|[1-9][0-9]|[0-9])

4가지 alternation을 가지고 있는데, IPv4 주소는 0~255까지 표현 가능하므로 200~255, 100~199, 10~99, 0~9를 따로 표현해준 것이다.


substitution

특정 문자열 패턴을 찾아 바꾸거나 지우는 일도 빈번하다. 이 기능은 sed, awk라는 툴에서 자주 사용한다.

sed

sed에서 제일 빈번히 쓰이는 기능이 교체이다. 해당 기능은 vim의 substitution command와 동일하다.

sed는 기본적으로 BRE로 작동한다. 따라서 옵션으로 -r를 해야 ERE를 쓴다. 옵션 -e는 생략해도 무관하나 보통 붙여 사용한다.

vim의 기능처럼, 아래 예에서 \는 seperator이다. 다르게 쓰고 싶다면 , 등도 쓸 수 있다.

$ echo $var2 | sed -e "s/<[^>]\+>//g" 
It's gonna be real It's gonna change everything I feel

$ echo $var2 | sed -re "s,<[^>]+>,,g"

<a>로 감싸진 태그 부분 중 안쪽 내용만 추출하고 싶다면 아래와 같이 한다.

(1) BRE ver.
$ curl -s https://www.naver.com | sed -n "s,<\(a\|A\) [^>]\+>\(.\+\) </\1>,\2,gp"

(2) ERE ver. → 잘 되나 해보기!!!
$ curl -s https://www.naver.com | sed -rn "s,<(a|A) [^>]+>(.+) </\1>,\2,gp" 

awk

awk는 함수의 형태로 처리가 가능하다. 아래 나오는 gsub은 global substitution에서 REGEX로 교체하는 방법으로, 몇 개가 나오든 교체한다.

awk은 ERE를 사용하여, + 앞에 백슬래시가 없다.

$ echo $var2 | awk '{ gsub(/[ ]*<[^<>]+>[ ]*/, " "); print }' 
It's gonna be real It's gonna change everything I feel

경계 검색(Bounday)

단어(word)의 경계 검색을 써야 할 수도 있다. 해당 기능은 ERE로만 작동하며, \b로 감싸주었을 때는 boundary에 맞는 표현식, 즉 단어 경계면을 검색하고, \B로 묶어주면 그 반대의 경우를 찾는다.

$ var5="abc? <def> 123hijklm"
$ echo $var5 | egrep --color "\b[a-j]+\b"
$ echo $var5 | egrep --color "\B[a-j]+\B"

predefined character class

직접 표현식을 쓰기보다는 미리 만들어진 클래스를 쓸 수도 있다.

다른 클래스들은 차라리 정규표현식을 쓰는 것이 편하나, 제어 문자처럼 조합하기 어려운 경우에는 클래스를 쓰는 것이 좋다. \x## 처럼 16진수 아스키 코드는 PCRE에서만 작동한다.

class 바깥 []는 묶는 의미이다. 예시는 아래와 같다.

$ var6="lumos@gmail.com:010-0000-00**:Eun-Gi HAN:AB-0105R"

$ echo $var6 | egrep -o "^[[:alpha:] @]+"
lumos@email

$ echo $var6 | egrep -o "[[:upper:] [:digit:]-]{8}"
010-0000
AB-0105R

REGEX 테스터 웹에서 정규표현식을 테스트해볼 수 있는 곳이 있다.

  • https://regexer.com/
  • https://www.regextester.com/
  • https://regex101.com/

Glob UNIX 혹은 Linux 명령행에서 파일명 패턴에 쓰이는 문자열이다.wildcard를 사용하는 파일명이 glob이다.REGEX와 비슷하나 REGEX는 아니다. man 7 glob를 검색해보자.


backtracking

Greedy matching 속성으로 인해 역탐색을 하는 행위이다. 이는 성능을 100~1000% 떨어뜨리는 요인으로, 필터 등의 regex test를 하는 구조에서는 backtracking을 제거하는 것이 좋다. 실제로 cloudflare에서 REGEX의 backtracking 때문에 사건이 발생한 바 있었다. Cloudflare blog Posting

실제로 Log 메시지를 이용한 backtracking을 확인해보자. regex101.com로 접속해 아래와 같은 정규 표현식을 넣는다.

(.*) (.*) (.*) (.*) (.*): (.*)

그리고 Test string에는 아래의 로그 한 줄을 넣는다.

Aug 24 02:22:13 localhost.localdomain kernel: IPV6: ADDRCONFCNETDEV_UP): wlp4so: link is not ready

작동시켜보면 아래 사진처럼 4000번이 넘는 스텝이 나온다.

이번에는 백트래킹을 완화시킨 표현식을 넣어본다.

([[:alpha:]]+) ([0-9]+) ([0-9:]+) (.*) (.*): (.*)

약 200번으로 줄어들었다. 조금 더 줄여보자.

/([[:alpha:]]+) ([0-9]+) ([0-9:]+) ([[:alnum:] .-]+) ([[:alnum:]._-]+): (.*)

backtracking을 최소화하기 위한 연습을 하는 것이 중요하다. .*, .+과 같이 greedy한 표현식을 남발하지 않아야 한다. .은 되도록 쓰지 않거나 lazy quantifier를 사용한다(.*?, .+?). 또한 공백을 반복할 수 있는 표현식은 피해야 한다.

arrow_upward arrow_downward
loading