참고: 이 문서는 [[/cmd/ag]] 명령을 사용합니다.

역사

다음은 브라이언 커니핸의 글을 인용한 것이다.

정규 표현식은 1950년대 중반에 클린Stephen Kleene이 유한자동자(finite automata)를 위한 하나의 표기법으로 창안한 것이다. 사실 정규식이 나타내고자 하는 바는 유한자동자가 나타내고자 하는 바와 동등하다. 정규식이 실제로 쓰인 최초의 프로그램은 1960년대 중반 켄 톰슨Ken Thompson이 만든 텍스트 편집기 QED의 한 버전이었다. 1967년에 톰슨은 정규식에 근거한 빠른 텍스트 부합을 위한 어떤 메커니즘으로 특허를 신청했다. 그 특허는 1971년에 승인되었는데, 소프트웨어 특허로는 최초의 것들 중 하나이다[U.S. Patent 3,568,156, Text Matching Algorithm, 1971년 3월 2일].1

QED 의 정규식은 Unix용 편집기 ed로 옮겨졌고, 그런 후 필수 Unix 도구인 [[/cmd/grep]]{grep}으로도 이식 되었다(grep은 톰슨이 ed를 크게 뜯어 고쳐서 만든 것이다). 널리 쓰이는 이런 프로그램들 덕분에 정규식은 초기 Unix 공동체 전반에서 잘 알려지게 되었다.

톰슨의 초기 정규표현식 부합기(matcher)는 아주 빨랐는데, 그것은 다음과 같은 두 가지 독립적인 아이디어를 결합한 덕분이었다. 하나는 부합 도중에 즉석에서 기계어 명령들을 생성하고 그 명령들을 컴퓨터에서 직접 실행한다는(소프트웨어 차원에서 패턴을 해석하는 대신) 것이고, 또 하나는 각 단계에서 모든 가능한 부합을 앞으로 넘김으로써 잠재적인 대안 부합들을 찾기 위해 문자열을 뒤로 역추적(backtracking)하지 않아도 되게 한다는 것이다. 톰슨이 이후에 작성한 텍스트 편집기들(ed 등)에서는 필요에 따라 역추적도 실행하는 좀 더 간단한 알고리즘이 부합 코드에 사용되었다. 이론적으로는 이전 방식에 비해 속도가 떨어지지만, 현실에서 쓰이는 대부분의 패턴들에는 역추적이 관여하지 않기 때문에 ed와 grep의 알고리즘과 코드는 대부분의 목적에 대해 충분히 빨랐다.

[[/cmd/grep]]{egrep}이나 fgrep 같은 이후의 정규식 부합기들은 좀 더 풍부한 정규식 문법들을 추가했으며, 패턴의 형태와는 무관하게 빠른 실행을 가능하게 하는 데 초점을 두었다. 이후 더욱 현란한 정규식들이 유명해졌는데, 이들은 C 기반 라이브러리들에 포함되었을 뿐만 아니라 [[awk]]{Awk}나 Perl 같은 스크립팅 언어의 문법 자체에도 포함이 되었다. 2

이스케이프

Predefined Term Matches
\t 탭 (좌우 수평 방향)
\b 백 스페이스
\v 탭 (위아래 수직 방향)
\f 폼피드 (Form feed ; 다음 페이지)
\r 개행 문자 (Carriage return)
\n 줄바꿈 문자 (Newline)
\cA : \cZ 제어문자 (Control characters)
\x0000 : \xFFFF 유니코드 16진수 (Unicode hexadecimal)
\x00 : \xFF 아스키 16진수 (ASCII hexadecimal)
\d 0-9 사이의 10진수 숫자. [0-9] 와 같다.
\D \d 의 여집합. [^0-9] 와 같다.
\w _ 를 포함한 모든 영어 알파벳과 숫자. [A-Za-z0-9_] 와 같다.
\W \w 의 여집합. [^A-Za-z0-9_] 와 같다.
\s 공백문자(스페이스, 탭, 폼피드 등). [\t\n\f\r]과 같다.
\S \s 의 여집합. 공백문제를 제외한 문자. [^\t\n\f\r]과 같다.
\b 단어의 경계를 나타내는 문자. (A word boundary)
\B 단어 내에서 문자의 경계가 아닌 문자. (Not a word boundary (inside a word))

앵커(anchor)

^ 행 시작 지점
$ 행 끝나는 지점
\A 문자열 시작 지점
\Z 문자열 끝나는 지점
\b 단어의 경계 지점
\B \b가 아닌 지점

다음과 같은 내용을 가진 hello.c 라는 파일이 있다고 하자.

#include <stdio.h>
main()
{
    printf("hello, world\n");
}

\A로 검색해보면 1번 라인이 출력된다. 즉, \A는 파일의(문자열의) 시작 지점에 매치되는 앵커이다.

$ ag '\A' hello.c
1:#include <stdio.h>

\Z는 파일의(문자열의) 끝 지점에 매치되는 앵커이다.

$ ag '\Z' hello.c
5:}

\b는 단어의 경계 지점에 매치된다.

반대로 \B는 단어의 경계가 아닌 곳에 매치된다.

수량자

  • * - 패턴을 0회 이상 반복
  • + - 패턴을 1회 이상 반복
  • ? - 0 ~ 1회

범위 수량자

  • {n}: n 회 반복
  • {n,m}: n ~ m 회 반복
  • {n,}: n 회 이상 반복

범위 수량자로 *, ?, +를 모두 대체할 수 있다.

욕심 수량자와 겸허 수량자

욕심(Greedy) 겸허(Lazy, Non-greedy)
* *?
+ +?
? ??
{n,m} {n,m}?

캡처 그룹

역참조

Backreferences

전방 탐색

  • lookahead, positive lookahead 라고 부른다.
\[(?= \text{pattern} )\]

전방 탐색은 주어진 패턴보다 뒤에(왼쪽에) 있는 문자의 일치를 판별한다.

예를 들어 \(b(?=c)\) 와 같은 정규식이 있다면 c 왼쪽에 있는 b에 매치된다.

  • b(?=c)
    • \(a \color{red}b cdbe\)  
      • echo abcdbe | ag 'b(?=c)' (잘 모르겠다면 이 명령을 복사해서 터미널에서 실행해보자.)
    • \(bb \color{red}b c\)  
      • echo bbbc | ag 'b(?=c)'
  • if(?=\()
    • \(\color{red}{if} (\)  
      • echo 'if(' | ag 'if(?=\()'

부정 전방 탐색

\[(?! \text{pattern})\]
  • negative lookahead 라고 부른다.

부정 전방 탐색은 전방 탐색의 반대이다.

  • 2(?!3) - 오른쪽에 3이 오지 않는 2.
    • \(123 \color{red}2 4\)  
      • echo 12324 | ag '2(?!3)'
    • \(\color{red}{222} 23\)  
      • echo 22223 | ag '2(?!3)'
  • (?!1234)\d{4} - 1234가 아닌 숫자 4개.
    • \(1234\)  
      • echo 1234 | ag '(?!1234)\d{4}'
    • \(\color{red}{1235}\)  
      • echo 1235 | ag '(?!1234)\d{4}'

숫자 3자리마다 콤마를 삽입하기

  • (?<=\d)(?=(\d{3})+($|\D))
    • 왼쪽의 숫자와 오른쪽의 3개 숫자 사이의 공간.
    • 이 정규식을 사용하면 3개 숫자마다 콤마를 삽입하는 코드를 쉽게 만들 수 있다.
var regex = /(?<=\d)(?=(\d{3})+($|\D))/g

'1234567'.replace(regex, ',');      // '1,234,567'
'12345678'.replace(regex, ',');     // '12,345,678'
'123456789'.replace(regex, ',');    // '123,456,789'
'1234567890'.replace(regex, ',');   // '1,234,567,890'
  • \B(?=(\d{3})+(?!\d))
var regex = /\B(?=(\d{3})+(?!\d))/g

'1234567'.replace(regex, ',');      // '1,234,567'
'12345678'.replace(regex, ',');     // '12,345,678'
'123456789'.replace(regex, ',');    // '123,456,789'
'1234567890'.replace(regex, ',');   // '1,234,567,890'

첫 한 글자만 남겨놓고 * 로 치환하기

개인정보 보호를 위해 사람 이름에 첫 한 글자만 남겨놓고 * 로 치환하는 코드가 필요할 수 있다.

var regex = /(?<=.)./g;

"홍길동".replace(regex, "*");
// '홍**'

"김수한무거북이와두루미삼천갑자동방삭".replace(regex, "*");
// '김*****************'

Clojure를 사용한다면 다음과 같이 할 수 있겠다.

(defn mask-name
  "첫 글자를 제외한 나머지 글자를 모두 * 로 교체한 문자열을 리턴합니다."
  [name-string]
  (string/replace name-string #"(?<=.)." "*"))

(mask-name "홍길동") ;; => "홍**"
(mask-name "김수한무거북이와두루미삼천갑자동방삭") ;; => "김*****************"

완전 일치의 반대

다음 패턴의 부정 전방 탐색을 사용하면 완전 일치의 반대 패턴이 된다. 필요에 따라 pattern 부분만 바꿔주면 된다.

^(?!pattern$j).*$

후방 탐색

\[(?<= pattern)\]

후방 탐색은 주어진 패턴보다 앞에(오른쪽에) 있는 패턴의 일치를 판별한다.

  • (?<=3)abc
    • \(123 \color{red}{abc}\)  
      • echo 123abc | ag '(?<=3)abc'
    • \(1abc3 \color{red}{abc} 5abc\)  
      • echo 1abc3abc5abc | ag '(?<=3)abc'

부정 후방 탐색

\[(?<! pattern)\]
  • (?<!2)abc
    • \(1 \color{red}{abc} 2abc 3 \color{red}{abc}\)  
      • echo 1abc2abc3abc | ag '(?<!2)abc'

참고 사항

  • Javascript는 부정 후방 탐색은 지원하지 않는다.

참고 예제

URI

  • [[URI]]

한글

\[[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]\]

이 정규식은 모든 한글에 매치된다.

유용한 도구 모음

  • REGEXPER: 입력한 정규식을 기차 레일 다이어그램으로 표현해준다. 정규식을 처음 공부하는 사람에게 추천한다.
  • regex2nfa: 입력한 정규식을 NFA 그래프로 표현해준다.

함께 읽기

  • [[rans-cmd]]

참고문헌

  • Beautiful Code / 찰스 페졸드 외 37 저 / 류광 옮김 / 한빛미디어 / 초판발행 2007년 12월 17일

주석

  1. US-3568156-A - Text Matching Algorithm. Google Patents. US-3568156-A - Text Matching Algorithm 

  2. Beautiful Code. 1장. 28쪽.