regexes — 정규식으로 텍스트 패턴 매칭하기

regexes — 정규식으로 텍스트 패턴 매칭하기

정규식(regular expression)은 특정 텍스트 패턴을 정의하는 문자들의 나열이에요. 보통 어떤 큰 텍스트 덩어리 안에서 찾고 싶은 패턴을 표현할 때 쓰죠. 이론 컴퓨터 과학과 형식 언어 이론에서는 이 정규식을 정규 언어(regular language)를 서술하는 데 씁니다. 1950년대에 태어난 이후 실용적인 구현, 예컨대 텍스트 편집기의 검색·치환 기능 같은 곳에서는 엄격한 과학적 정의를 훌쩍 넘어서 성장했어요. 이 점을 인정하고 혼동을 피하고자, Raku에서는 정규식을 보통 Regex(reg-ular ex-pression에서)라고 부르는데, 이 용어는 다른 프로그래밍 언어에서도 흔히 쓰여요.

Raku에서 정규식은 도메인 특화 언어, 즉 하위 언어(sublanguage) 또는 슬랭(slang)으로 작성돼요. 이 페이지는 그 언어를 설명하고, 문자열 안에서 텍스트 패턴을 찾는 패턴 매칭(pattern matching)이라는 과정에 정규식을 어떻게 쓰는지 풀어내요. Raku 정규식은 변수나 코드 블록을 여러 방식으로 인터폴레이션(보간)할 수 있고, 다양한 부사(adverb)로 수정할 수 있어요.

출처: Regexes - Raku Documentation

어휘 규약

근본적으로 Raku 정규식은 서브루틴과 아주 비슷해요. 둘 다 코드 객체이고, 익명 서브와 이름 있는 서브가 있듯이 익명 정규식과 이름 있는 정규식이 있어요.

정규식은 익명이든 이름이 있든 Regex 객체로 표현돼요. 다만 익명 Regex 객체와 이름 있는 Regex 객체를 만드는 문법은 달라요. 그래서 하나씩 살펴볼게요.

익명 정규식 정의 문법

익명 정규식은 다음 방식 중 하나로 만들 수 있어요:

rx/pattern/;          # 익명 Regex 객체; 'rx'는 'regex'의 약자
/pattern/;            # 익명 Regex 객체; 'rx/.../'의 축약형
regex { pattern };    # 키워드로 선언한 익명 정규식; 이 형태는 이름 있는 정규식을
                      # 정의하기 위한 것으로 다음 절에서 다룸

rx/ / 형태는 축약 형태 / /에 비해 두 가지 이점이 있어요.

첫째, 슬래시가 아닌 다른 구분자(delimiter)를 쓸 수 있다는 점이에요. 정규식 정의의 가독성을 높이는 데 쓰면 좋아요:

rx{ '/tmp/'.* };      # 중괄호를 구분자로 쓰면 첫 번째 정의가 두 번째보다
rx/ '/tmp/'.* /;      # 눈에 조금 더 잘 들어옴

선택지가 많긴 하지만, 모든 문자를 구분자로 고를 수는 없어요:

  • 공백이나 영숫자 문자는 구분자로 쓸 수 없어요. 정규식 정의 문법에서 공백은 일반적으로 선택 사항이지만, 함수 호출 문법과 구분해야 하는 곳에서는 필수예요.
  • 괄호는 대체 구분자로 쓸 수 있어요. 다만 rx와 여는 괄호 사이에 공백이 있어야 해요. 괄호가 바로 뒤따르는 식별자는 항상 서브루틴 호출로 파싱되기 때문이에요. 예를 들어 rx()에서 호출 연산자 ()는 서브루틴 rx를 호출해요. 하지만 rx ( abc ) 형태는 정말로 Regex 객체를 정의해요.
  • 콜론을 구분자로 쓰면 :adverb 형태를 취하는 부사와 충돌해요. 그래서 콜론을 그런 용도로 쓰는 건 금지돼요.
  • 해시 문자 #은 줄 끝까지 이어지는 주석의 시작으로 파싱되므로 구분자로 쓸 수 없어요.

둘째, rx 형태는 rx와 여는 구분자 사이에 정규식 부사를 넣어 정규식 전체의 정의를 수정할 수 있게 해 줘요. 이는 정규식 시작에 부사를 넣는 것과 같지만 더 명확할 수 있어요:

rx:r:s/pattern/;            # :r (:ratchet)과 :s (:sigspace) 부사로,
                            # 공백이 의미를 갖는 래칫 정규식을 정의
rx/:r:s pattern/;           # 같은 뜻이지만 가독성은 낮을 수 있음

익명 정규식은 그 자체로 이름을 갖진 않지만, 이름 있는 변수 안에 넣으면 사실상 이름을 부여할 수 있어요. 그러면 포함 정규식의 밖이나 안(인터폴레이션으로)에서 참조할 수 있어요:

my $regex = / R \w+ /;
say "Zen Buddhists like Raku too" ~~ $regex; # OUTPUT: «「Raku」␤»

my $regex = /pottery/;
"Japanese pottery rocks!" ~~ / <$regex> /;   # $regex를 /.../ 안으로 보간
say $/;                                      # OUTPUT: «「pottery」␤»

이름 있는 정규식 정의 문법

이름 있는 정규식은 regex 선언자로 이렇게 만들어요:

regex R { pattern };        # 'R'이라는 이름을 가진 Regex 객체

rx 형태와 달리 구분자를 마음대로 고를 수 없어요. 중괄호가 필수예요. 이 점에서 regex 형태로 이름 있는 정규식을 정의하는 건 서브루틴 정의와 문법적으로 비슷하다고 볼 수 있어요:

my sub   S { /pattern/ };   # Sub 객체의 정의 (Regex를 반환)
my regex R {  pattern  };   # Regex 객체의 정의

이는 Regex 객체가 데이터가 아니라 코드를 나타낸다는 사실을 강조해요:

&S ~~ Code;                 # OUTPUT: «True␤»
&R ~~ Code;                 # OUTPUT: «True␤»
&R ~~ Method;               # OUTPUT: «True␤»   (Regex는 사실상 Method!)

또한 rx 형태와 달리 regex 키워드로 이름 있는 정규식을 정의할 땐 여는 구분자 앞에 부사를 넣을 수 없어요. 대신 정규식 전체를 수정할 부사는 중괄호 안 맨 앞에 넣어요:

regex R { :i pattern };     # :i (:ignorecase), 패턴을 대소문자 무시로

또는 축약으로, :ratchet:sigspace 부사가 필요할 땐 regex 선언자의 rule·token 변형을 쓰는 게 가능하고 권장돼요:

regex R { :r pattern };     # :r (:ratchet)을 패턴 전체에 적용
token R { pattern };        # 같은 뜻: 'token'은 ':r'을 내포
regex R { :r :s pattern };  # :r (:ratchet)과 :s (:sigspace)를 패턴에 적용
rule  R { pattern };        # 같은 뜻: 'rule'은 ':r:s'를 내포

이름 있는 정규식은 다른 정규식의 빌딩 블록으로 쓸 수 있어요. 다른 정규식 안에서 <regex-name> 문법으로 호출할 수 있는 메서드이기 때문이에요. 이렇게 쓰면 자주 서브룰(subrule)이라고 부릅니다. Grammar가 서브룰의 자연 서식지지만, 흔히 쓰이는 미리 정의된 문자 클래스 중 상당수도 이름 있는 정규식으로 구현돼 있어요.

정규식 가독성: 공백과 주석

:sigspace 부사를 써서 공백을 문법적으로 의미 있게 만들지 않는 한, 정규식의 공백은 무시돼요.

공백 외에도 일반 코드처럼 정규식 안에서 주석을 써서 이해를 높일 수 있어요. 한 줄 주석과 여러 줄/내장 주석 모두 말이죠:

my $regex =  rx/ \d ** 4            #`(연도를 YYYY로 매칭)
                 '-'
                 \d ** 2            # ...월을 MM으로
                 '-'
                 \d ** 2 /;         # ...그리고 일을 DD로

say '2015-12-25'.match($regex);     # OUTPUT: «「2015-12-25」␘»

매칭 문법

문자열을 정규식에 매칭하는 방법은 여러 가지예요. 어떤 문법을 쓰든 성공적인 매칭은 Match 객체로 이어져요. 매칭이 실패하면 결과는 Nil이에요. 어느 쪽이든 매칭 연산의 결과는 특별 매칭 변수 $/로 접근할 수 있어요.

익명 정규식 /pattern/이나 이름 있는 정규식 R에 문자열을 매칭하는 가장 흔한 방법들:

  • Smartmatch: "string" ~~ /pattern/, 또는 "string" ~~ /<R>/ 문자열을 Regex에 smartmatch하면 문자열을 Regex에 대해 정규식 매칭해요:

    say "Go ahead, make my day." ~~ / \w+ /;   # OUTPUT: «「Go」␘»
    my regex R { me|you };
    say "You talkin' to me?" ~~ / <R> /;       # OUTPUT: «「me」␘ R => 「me」␘»
    say "May the force be with you." ~~ &R ;   # OUTPUT: «「you」␘»
    

    마지막 두 문장의 출력이 다른 걸 보면, 이름 있는 정규식에 smartmatch하는 이 두 방식이 동일하지 않다는 걸 알 수 있어요. 그 차이는 익명 정규식 / / 안에서 메서드 호출 <R>Match 객체에 소위 '이름 있는 캡처'를 설치하기 때문이고, 이름 있는 Regex에 그 자체로 smartmatch할 때는 그런 설치가 일어나지 않아요.

  • 명시적 토픽 매치: m/pattern/, 또는 m/<R>/ 매치 연산자 m/ /는 토픽 변수 $_m 뒤의 정규식에 즉시 매칭해요. rx/ / 문법처럼 매치 연산자도 m과 여는 정규식 구분자 사이에 부사를 쓸 수 있고, 슬래시가 아닌 구분자를 쓸 수 있어요. 다만 rx/ / 문법이 정규식 컴파일에 영향을 주는 정규식 부사만 쓸 수 있는 반면, m/ / 문법은 정규식 엔진이 패턴 매칭을 어떻게 수행할지 정하는 매칭 부사도 추가로 쓸 수 있어요. m/ // / 문법의 핵심 차이를 보여주는 예시:

    my $match;
    $_ = "abc";
    $match = m/.+/; say $match; say $match.^name; # OUTPUT: «「abc」␘Match␘»
    $match =  /.+/; say $match; say $match.^name; # OUTPUT: «/.+/␘Regex␘»
    
  • 싱크·불리언 컨텍스트에서의 암시적 토픽 매치 Regex 객체를 싱크 컨텍스트나 Bool로 강제되는 컨텍스트에서 쓰면 토픽 변수 $_가 자동으로 그 정규식에 매칭돼요:

    $_ = "dummy string";        # 토픽을 명시적으로 설정
    rx/ s.* /;                  # 싱크 컨텍스트의 Regex 객체는 자동 매칭
    say $/;                     # OUTPUT: «「string」␘»
    say $/ if rx/ d.* /;        # 불리언 컨텍스트의 Regex 객체는 자동 매칭
                                # OUTPUT: «「dummy string」␘»
    
  • match 메서드: "string".match: /pattern/, 또는 "string".match: /<R>/ match 메서드는 위에서 다룬 m/ / 연산자와 유사해요. 문자열에서 인자로 Regex를 받아 호출하면 그 문자열을 Regex에 매칭해요.

  • 그래머 파싱: grammar-name.parse($string) 그래머를 파싱하는 건 정규식에 문자열을 매칭하는 것보다 더 많은 걸 포함하지만, 이 강력한 정규식 기반 텍스트 구조 분해 도구를 흔한 패턴 매칭 방법 개요에서 빼놓을 수 없어요. 단순 정규식이 제공하는 것 이상이 필요하다고 느낀다면, 그래머 튜토리얼을 봐서 정규식을 한 단계 더 끌어올려 보세요.

리터럴과 메타문자

정규식은 리터럴과 메타문자의 관점에서 매칭할 패턴을 서술해요. 영숫자 문자와 밑줄 _이 리터럴을 구성해요. 이 문자들은 자기 자신과만 매칭되죠. 다른 문자들은 메타문자로 동작하는데, 그 자체로 특별한 의미(와일드카드 역할을 하는 점 . 같은)를 갖거나, 더 큰 메타문법 구성(선행 탐색 단언을 정의하는 <?before ...> 같은)에서 다른 문자들과 함께 쓰여요.

가장 단순한 형태의 정규식은 리터럴만으로 이뤄져요:

/Cześć/;           # 폴란드어로 "Hello"
/こんばんは/;        # 일본어로 "안녕하세요"
/Καλησπέρα/;       # 그리스어로 "안녕하세요"

정규식이 평소 메타문자로 동작하는 문자 하나 이상을 리터럴로 매칭하고 싶으면, 그 문자들을 백슬래시로 이스케이프하거나 단일·이중 따옴표로 따옴표 처리해야 해요.

백슬래시는 스위치 역할을 해요. 단일 메타문자를 리터럴로 바꾸고, 그 반대도 해요:

/ \# /;             # 해시 메타문자를 리터럴로 매칭
/ \w /;             # 리터럴 'w'를 문자 클래스로 전환 (아래 참조)
/Hallelujah\!/;     # 느낌표 포함한 문자열 'Hallelujah!' 매칭

메타문자가 (아직) Raku에서 특별한 의미를 갖지 않더라도, 정규식이 컴파일되고 문자를 리터럴로 매칭하게 하려면 이스케이프(또는 따옴표)가 필요해요. 이래야 리터럴과 메타문자를 명확히 구분할 수 있어요. 예를 들어 쉼표를 매칭하려면 이건 동작하고:

/ \, /;             # 리터럴 쉼표 ',' 매칭

이건 실패해요:

/ ,  /;             # !! 오류: 아직 의미가 없/인식되지 않는 메타문자는
                    # 자동으로 리터럴 매칭되지 않음

이스케이프 백슬래시가 다음 단일 문자에만 영향을 주는 반면, 단일 메타문자나 메타문자 나열은 단일·이중 따옴표로 따옴표 처리해서 리터럴 매칭 문자열로 바꿀 수 있어요:

/ "abc" /;          # 리터럴을 따옴표 처리해도 더 리터럴해지진 않음
/ "Hallelujah!" /;  # 하지만 일반적으로 /Hallelujah\!/보다 이 형태를 선호
/ "two words" /;    # 공백을 따옴표 처리하면 의미를 갖게 되어
                    # 가운데 공백을 포함한 'two words' 문자열과 매칭
/ '#!:@' /;         # 메타문자 나열 '#!:@'과 매칭

다만 따옴표 처리한다고 모든 메타문자가 리터럴이 되는 건 아니에요. 따옴표가 Raku의 평범한 인터폴레이션 규칙을 따르기 때문이에요. 특히 「…」 따옴표는 인터폴레이션을 전혀 허용하지 않고, 단일 따옴표('…' 또는 ‘…’)는 백슬래시가 단일 따옴표와 백슬래시 자신을 이스케이프하게 하며, 이중 따옴표("…" 또는 “…”)는 변수와 {…} 형태의 코드 블록 인터폴레이션을 가능하게 해요. 그래서 이 모두가 동작해요:

/ '\'' /;           # 백슬래시 다음 단일 따옴표 2개: \'
/ 「\'」 /;          # 백슬래시 다음 단일 따옴표 하나도 매칭
my $x = 'Hi';
/ "$x there!" /;    # 문자열 'Hi there!' 매칭
/ "1 + 1 = {1+1}" /; # 문자열 '1 + 1 = 2' 매칭

반면 이 예시들은 피해야 할 실수를 보여줘요:

/ '\' /;             # !! 오류: 백슬래시를 이렇게 매칭하는 게 아님.
                     # 지금은 두 번째 따옴표를 이스케이프함
/ "Price tag $0.50"/; # !! 오류: "$0"은 첫 번째 위치 캡처(Nil)로 해석
                     # 되지 '$0' 그 자체로 해석되지 않음

문자열은 왼쪽에서 오른쪽으로 검색되므로 문자열의 일부만 정규식과 매칭돼도 충분해요:

if 'Life, the Universe and Everything' ~~ / and / {
    say ~$/;            # OUTPUT: «and␘»
    say $/.prematch;    # OUTPUT: «Life, the Universe ␘»
    say $/.postmatch;   # OUTPUT: « Everything␘»
    say $/.from;        # OUTPUT: «19␘»
    say $/.to;          # OUTPUT: «22␘»
};

매칭 결과는 항상 $/ 변수에 저장되고 매칭에서도 반환돼요. 매칭이 성공하면 둘 다 Match 타입이고, 실패하면 둘 다 Nil 타입이에요.

와일드카드

정규식에서 이스케이프되지 않은 점 .은 임의의 단일 문자 하나와 매칭돼요.

그래서 이 모두가 매칭돼요:

'raku' ~~ /rak./;       # 문자열 전체 매칭
'raku' ~~ / rak . /;    # 같음; 공백은 무시됨
'raku' ~~ / ra.u /;     # .이 k에 매칭
'raker' ~~ / rak. /;    # .이 e에 매칭

반면 이건 매칭되지 않아요:

'raku' ~~ / . rak /;

대상 문자열에서 rak 앞에 매칭할 문자가 없기 때문이에요.

특히 .은 논리 줄바꿈 \n에도 매칭된다는 점을 기억하세요:

my $text = qq:to/END/
  Although I am a
  multi-line text,
  I can be matched
  with /.*/.
  END
  ;

say $text ~~ / .* /;
# OUTPUT: «「Although I am a␘multi-line text,␘I can be matched␘with /.*/.␘」»

문자 클래스

백슬래시 문자 클래스

\w 형태의 미리 정의된 문자 클래스가 있어요. 그 부정은 대문자 \W로 써요.

\n\N

\n은 논리 줄바꿈과 매칭돼요. \N은 논리 줄바꿈이 아닌 단일 문자와 매칭돼요.

논리 줄바꿈을 구성하는 정의는 Unicode의 줄 경계(line boundary) 정의를 따르며, 특히 줄바꿈(LF) U+000A, 세로 탭(VT) U+000B, 폼 피드(FF) U+000C, 캐리지 리턴(CR) U+000D, 그리고 Windows 스타일 줄바꿈 시퀀스 CRLF를 모두 포함해요.

정규식에서 \n의 해석은 newline 프라그마가 제어하는 $?NL 변수의 값과 무관해요.

\t\T

\t는 탭/탭 문자 하나 U+0009와 매칭돼요. \T는 탭이 아닌 단일 문자와 매칭돼요. U+000B VERTICAL TABULATION 같은 이국적인 탭은 여기에 포함되지 않아요.

\h\H

\h는 단일 수평 공백 문자와 매칭돼요. \H는 수평 공백 문자가 아닌 단일 문자와 매칭돼요. 수평 공백 문자의 예로는 다음이 있어요:

U+0020 SPACE
U+00A0 NO-BREAK SPACE
U+0009 CHARACTER TABULATION
U+2001 EM QUAD

줄바꿈 문자 같은 세로 공백은 명시적으로 제외돼요. 그것들은 \v로 매칭할 수 있고, \s는 모든 종류의 공백과 매칭돼요.

\v\V

\v는 단일 세로 공백 문자와 매칭돼요. \V는 세로 공백 문자가 아닌 단일 문자와 매칭돼요. 세로 공백 문자의 예로는 다음이 있어요:

U+000A LINE FEED (LF)
U+000B VERTICAL TABULATION (VT)
U+000C FORM FEED (FF)
U+000D CARRIAGE RETURN (CR)
U+0085 NEXT LINE (NEL)
U+2028 LINE SEPARATOR
U+2029 PARAGRAPH SEPARATOR

수평 공백은 제외돼요. 예를 들어 공백 U+0020 SPACE를 세로 공백과 매칭하려 시도하는 건 실패해요. 그런 문자의 경우 \s를 써요.

\s, \S

\s는 단일 공백 문자(수평·세로 모두)와 매칭돼요. \S는 공백 문자가 아닌 단일 문자와 매칭돼요.

\d\D

\d는 단일 숫자 문자와 매칭돼요. \D는 숫자 문자가 아닌 단일 문자와 매칭돼요.

\d\D는 주어진 텍스트를 unicode의 Unicode::Number 프로퍼티로 분류해요. 그래서 다음은 매칭돼요:

FULL STOP '.'
COMMERCIAL AT '@'
...

사실 \d는 decimal digit, 그리고 숫자로 분류되는 다른 글리프들(분수 등)에도 매칭돼요. ASCII 숫자만 매칭하고 싶다면 <[0..9]> 또는 <:Hex + :Digit> 형태를 쓰는 편이 안전해요.

\d는 유니코드의 Numeric Type이 Numeric/Decimal인 문자와 매칭한다는 점을 기억하세요. ., @ 같은 건 매칭되지 않아요. 만약 \d가 매칭하지 않는 걸 매칭해야 한다면 \D를 쓰되, 그 부정이 정말 원하는 연산인지 확인하세요.

\w\W

\w는 단일 "워드" 문자와 매칭돼요. \W는 워드 문자가 아닌 단일 문자와 매칭돼요.

\w가 매칭하는 정확한 문자 집합은 유니코드 프로퍼티 \w 정의에 따라 달라져요. 기본적으로 \w는 유니코드 Alphabetic 프로퍼티를 가진 문자, 숫자, 그리고 밑줄 문자와 매칭돼요. 어떤 언어는 \w에 더 많은 문자를 포함하기도 해요.

\w\W는 unicode의 Alphabetic 프로퍼티를 기반으로 해요. Alphabetic에는 Letter(문자), Number(숫자), 그리고 다른 여러 문자 카테고리가 포함돼요 (참고: _Alphabetic이 아니지만 \w에는 포함돼요).

$word =~ /\w+/ 같은 기존 문법과의 호환을 위해, \wAlphabetic과 underscore 문자를 매칭해요. 그래서 한글/중국어/일본어 텍스트 같은 것들과도 잘 작동해요. \w가 매칭하는 것과 매칭하지 않는 것을 정확히 이해하려면 유니코드 프로퍼티 문서를 참조하세요.

[a-zA-Z0-9_]만 매칭하고 싶으면 다른 클래스를 써야 해요 (예: <[a..zA..Z0..9_]>).

\c 캐릭터 이름

백슬래시 문자 클래스 \c[LATIN CAPITAL LETTER A]A와 매칭돼요. 이름이 너무 길어서 \c[A]처럼 쓸 수도 있는데, 이름은 전체를 매칭해요. \c[...]의 축약형도 있어서 \c[F], \c[L]을 대문자 이름과 매칭할 수 있어요. \C는 어떤 유니코드 프로퍼티를 가진 문자 하나와 매칭돼요.

\x\X

\x는 유니코드 코드 포인트 하나와 매칭돼요.

\X[....]는 조합 문자를 포함한 연속된 문자들과 매칭돼요.

\o\O

\o는 옥탈(8진수), \O는 hex(16진수) 문자와 매칭돼요.

. 및 문자 클래스와 매칭하기

정규식에 .(와일드카드)처럼 임의의 문자와 매칭하고 싶다면, 정확한 문자를 매칭해야 할 필요가 자주 있어요. .은 줄바꿈을 포함해 모든 문자와 매칭되지만, 명시적으로 빈 문자(빈 매치)와도 매칭할 수 있어요. 그래서:

say "abc\ndef" ~~ / .* .? . /;

이 정규식은 점 .이 빈 문자열과도 매칭할 수 있어서 공백을 포함한 임의의 문자를 매칭해요.

앵커와 단언

문자 클래스와 메타문자를 결합해 패턴을 만들 때, 매칭이 일어나는 위치를 제어하고 싶을 때가 많아요. Raku는 위치 앵커를 위해 ^(문자열의 시작)와 $(문자열의 끝)를 제공해요. 줄 경계는 ^^(줄 시작)와 $$(줄 끝)로 앵커할 수 있어요.

참고: ^$는 각각 문자열의 시작과 끝에 매칭하지만, ^^$$는 각 줄의 시작과 끝에 매칭해요.

my $str = "Greetings\nfrom\nRaku";
say "문자열 시작" if $str ~~ /^Greetings/;   # True (첫 줄 시작)
say "문자열 끝"   if $str ~~ /Raku$/;        # True (마지막 줄 끝)
say "줄 시작"     if $str ~~ /^^from/;       # True (두 번째 줄 시작)

워드 경계

\b는 워드 문자와 비워드 문자 사이의 위치, 즉 워드 경계와 매칭돼요. \b는 "워드 경계"를 단언해요. 그 반대인 \B는 "비-워드 경계"를 단언해요. \b의 진짜 의미와 제약을 이해하는 게 중요해요.

이와 같은 단언은 어떤 문자도 소비하지 않아요. \b(워드 경계)는 현재 위치가 단어 중간이 아닌 단어 경계일 때만 매칭 성공해요.

say "최종 매치" if "hello world" ~~ / world\b/;   # 'world'가 워드 경계에서 끝남
say "매치 없음" if "hello world" ~~ / world\b/ && "hello world" ~~ /o\b/;
# 두 번째 조건은 실패 → 첫 문장만 출력

\b는 문자 클래스 안에서는 쓸 수 없어요. <-[…]><[…]> 안에서 \b를 쓰면 컴파일 오류가 나요. 대신 \n이나 다른 명시적 문자를 써야 해요.

lookahead/lookbehind 단언

Raku는 lookahead/lookbehind 단언에 대해 <before ...><after ...>을 제공해요. <!before ...>는 부정 lookahead, <!after ...>는 부정 lookbehind예요. 이들은 0-폭으로, 문자를 소비하지 않고 위치만 검사해요.

say "매치" if "foobar" ~~ / foo <before bar> /;
say "매치" if "foobar" ~~ / <after foo> bar /;
say "매치" if "foobar" ~~ / <!before baz> foo /;
say "매치" if "foobar" ~~ / foo <!before baz> /;

부정 lookahead를 쓸 때 위치가 정확한지 확인하는 게 중요해요.

정량자

+* 그리고 ?는 정량자로 알려져 있어요. 어떤 것의 "n개 또는 그 이상" 또는 "0개 또는 그 이상"을 매칭할 때 쓸 수 있어요.

say "매치" if "abc" ~~ / a b c+ /;   # 'c' 1개 이상
say "매치" if "abc" ~~ / a b c* /;   # 'c' 0개 이상
say "매치" if "abc" ~~ / a b c? /;   # 'c' 0개 또는 1개

정량자가 붙은 항목 뒤에 오는 것은 항상 문자열의 끝에 있어야 해요. 왜냐하면 정량자는 그 항목을 최대한 많이 매칭하기(greedy) 때문이에요. 정량자를 "게으르게"(lazy) 만들려면 ?를 정량자 뒤에 추가해요.

수량 정량자

정확한 수량을 매칭하려면 **를 쓸 수 있어요. <> 사이에 숫자 또는 범위를 넣어요.

say "매치" if "aaaa" ~~ / a ** 3 /;   # 정확히 3개의 'a'와 매칭 (하지만 'aaaa' 한 개도 매칭할 수 있음)
say "매치" if "aaa"  ~~ / a ** 3 /;   # 정확히 3개의 'a'

a ** 3은 세 개의 'a'와 매칭하지만, 뒤에 'a'가 더 있어도 매칭은 'aaa'에서 성공해요. 범위를 지정할 수도 있어요:

say "매치" if "aaa" ~~ / a ** 2..3 /;  # 2~3개의 'a'
say "매치" if "aaaaa" ~~ / a ** 2..* /; # 2개 이상의 'a'

^ 접두사로 가장 짧은 매칭을 요청할 수도 있어요 (** ^2..3).

캡처

정규식은 그룹을 캡처하는 데 쓰일 수 있어요. Raku에서 캡처는 ( ... )로 표기하고, 캡처된 값은 특별 변수 $/(또는 $0, $1 등)으로 접근해요.

'raku' ~~ / ( ... ) /;

캡처 번호는 왼쪽 괄호의 위치로 매겨져요. $0, $1, $2 등이 돼요. 이름 있는 캡처는 (... ) 대신 $<name> = ... 형태로 지정할 수 있고, $<name>으로 접근해요.

"abc" ~~ / (?<name> .+ ) /;

캡처 그룹의 결과는 리스트여야 하는 경우가 많아요. 배열 캡처 $0는 괄호 안의 매칭 결과를 반환해요. 예를 들어 반복적으로 매칭할 때 유용해요.

캡처 안에 캡처가 중첩된 경우

캡처에 다른 캡처가 중첩된 경우를 흔히 볼 수 있는데, 특히 임의 코드가 관련될 때 그래요:

"one two" ~~ / ( (\w+) \s+ (\w+) ) /;
say $0;        # "one two"
say $0[0];     # "one"
say $0[1];     # "two"

$0[0]은 첫 번째 캡처 안의 첫 번째 하위 캡처를 나타내요.

백트래킹

정규식 중간에 실패하면 엔진이 백트래킹해요. Raku는 기본적으로 재귀적 백트래킹 엔진을 써요.

"abc" ~~ / a.* c /;   # 성공: .*가 bc를 소비한 뒤 c로 백트래킹

백트래킹을 끄려면 :ratchet 부사(또는 token)를 쓰면 실패가 즉시 그 항목을 실패시켜요. 이는 왼쪽 대안(LTM)과 함께 쓸 때 강력해요.

대안

두 개 이상의 대안을 매칭하려면 |(LTM) 또는 ||(순서)를 써요.

say "매치" if "foo" ~~ / foo | bar /;   # foo 또는 bar

||는 순서대로 시도해서 첫 번째 성공을 취하고, |는 Longest Token Match (LTM)로 가장 긴 매칭을 취해요.

토큰과 규칙

tokenrule은 정규식의 특수한 형태로, 래칫(ratchet)을 기본으로 해요. 즉 백트래킹이 없어요.

token identifier { <alpha> \w* }
rule  greeting    { "Hello" \s+ <name> }

rule:sigspace(공백이 의미 있음)를 내포하고, token:ratchet을 내포해요.

캡처와 재캡처

이름 있는 캡처를 사용해서 매칭 결과의 특정 부분을 추출할 수 있어요. Grammar에서 특히 유용한데, 서브룰이 구조적 캡처를 만들어요.

grammar DateFormat {
    token TOP { <year> '-' <month> '-' <day> }
    token year  { \d ** 4 }
    token month { \d ** 2 }
    token day   { \d ** 2 }
}
say DateFormat.parse("2025-01-01");
# OUTPUT: 「2025-01-01」 year => 「2025」 month => 「01」 day => 「01」

그래머

정규식을 그래머로 조직해 복잡한 텍스트 구조를 파싱할 수 있어요. 그래머는 클래스처럼 grammar 키워드로 정의하고, token, rule, regex 메서드를 포함해요. 파싱은 .parse 메서드로 시작돼요.

grammar JSON-Grammar { ... }
my $match = JSON-Grammar.parse($json-text);

비동기 정규식

정규식은 Promise를 사용하는 코드 블록을 포함할 수 있어요. 이 때 :v(void)나 :c와 함께 조심해서 써야 해요.

정규식과 예외

정규식 평가 중 발생하는 예외는 X::Regex 타입으로 던져질 수 있어요. 예를 들어 불량한 요일 이름 같은 게 있을 수 있어요.

정규식과 유니코드

Raku 정규식은 유니코드 프로퍼티와 문자 클래스로 작업하는 강력한 방법을 제공해요.

say "é" ~~ /\N{...}/;

유니코드 프로퍼티에 대한 자세한 내용은 Unicode 페이지를 보세요.

정규식과 다중 디스패치

정규식은 다중 디스패치(멀티) 서브루틴에서 시그니처로 쓸 수 있어요. 이는 타입 기반 선택과 함께 유용해요.

multi sub process ( $x where / \d+ / ) { say "숫자: $x" }
multi sub process ( $x ) { say "기타: $x" }

클러스터링

[...]로 정규식의 일부를 클러스터(그룹)로 묶어서, 캡처를 만들지 않고 구조를 만들 수 있어요. 캡처는 별도로 $<name>이나 (...)를 써요.

"abcabc" ~~ / [ ab ] ** 2 /;   # ab를 두 번 반복

클러스터는 0-폭과 순서 같은 걸 제어하는 데 유용해요.

캡처링

정규식에서 캡처는 ( ... )로 하고, 결과는 $/에 저장돼요. $0은 첫 번째 캡처 그룹이고, 이름 있는 캡처는 $<name>으로 접근해요.

  • $0 — 위치 캡처의 배열
  • $<name> — 이름 있는 캡처
  • $/ — 전체 매칭

이름 있는 캡처와 서브룰

그래머에서 각 token/rule 호출은 자동으로 이름 있는 캡처를 생성해요. 이는 파싱 트리를 구성하는 데 핵심이에요.

grammar Number {
    token TOP { <int> ['.' <frac>]? }
    token int  { \d+ }
    token frac { \d+ }
}
my $m = Number.parse("3.14159");
say $m<int>;      # 「3」
say $m<frac>;     # 「14159」

태그된 캡처

$<name>=... 형태로 태그된 캡처를 만들면 매칭 결과에 이름을 붙여 나중에 읽기 쉽게 해요.

"2025-01-01" ~~ / $<year>=(\d ** 4) '-' $<month>=(\d ** 2) '-' $<day>=(\d ** 2) /;
say $<year>;   # 「2025」
say $<month>;  # 「01」
say $<day>;    # 「01」

바인딩

-> 화살표를 써서 캡처 결과를 블록 파라미터에 바인딩할 수 있어요. 정규식 매칭과 함께 쓰면 결과를 구조화해 꺼낼 수 있어요.

"3 4" ~~ / (\d+) \s+ (\d+) / -> $a, $b { say $a + $b }   # 7

정량자와 캡처

정량자가 붙은 캡처는 반복적으로 매칭돼요:

"a b c" ~~ / ( \w ) [\s+ \w]* /;
say $0;   # 「a」 (첫 번째만)

여러 번 캡처하려면 캡처를 배열 캡처로 명시해야 해요.