regexes-best-practices — 정규식·그래머를 견고하게 쓰는 요령
regexes-best-practices — 정규식·그래머를 견고하게 쓰는 요령
정규식(regex)과 그래머(grammar)를 견고하게 만들려면, 코드 배치와 가독성, 무엇을 실제로 매칭할지, 그리고 흔한 함정들을 피하는 데 신경 써야 해요. 이 페이지는 그런 모범 사례들을 정리한 거예요.
출처: Regexes: Best practices and gotchas - Raku Documentation
코드 배치
:sigspace 어드버브를 쓰지 않는 한 Raku 정규식에서는 공백이 의미를 갖지 않아요. 이 점을 활용해 가독성이 좋아지는 곳에 공백을 넣고, 필요하면 주석도 넣어요.
매우 압축된 버전을 비교해 볼게요:
my regex float { <[+-]>?\d*'.'\d+[e<[+-]>?\d+]? }
이렇게 가독성 좋게 쓴 버전이랑요:
my regex float {
<[+-]>? # optional sign
\d* # leading digits, optional
'.'
\d+
[ # optional exponent
e <[+-]>? \d+
]?
}
일반적인 규칙으로는 원자(atom) 주변과 그룹 안쪽에 공백을 쓰고, 수량자(quantifier)는 원자 바로 뒤에 붙이며, 여는/닫는 대괄호와 괄호를 세로로 정렬해요.
괄호나 대괄호 안에 대안(alternation) 목록을 쓸 때는 세로 막대를 정렬해요:
my regex example {
<preamble>
[
|| <choice_1>
|| <choice_2>
|| <choice_3>
]+
<postamble>
}
작게 유지하기
정규식은 보통 일반 코드보다 압축적이에요. 적은 코드로 많은 걸 하기 때문에, 정규식을 짧게 유지하는 게 좋아요.
정규식의 일부에 이름을 붙일 수 있다면, 그 부분을 별도의 이름 있는 정규식으로 빼는 게 보통 좋아요.
예를 들어 앞의 float 정규식을 이렇게 분해할 수 있어요:
my token sign { <[+-]> }
my token decimal { \d+ }
my token exponent { 'e' <sign>? <decimal> }
my regex float {
<sign>?
<decimal>?
'.'
<decimal>
<exponent>?
}
이렇게 하면 정규식이 더 복잡해질 때 특히 도움이 돼요. 예를 들어 지수가 있으면 소수점을 선택 사항으로 만들고 싶을 때도 있고요.
my regex float {
<sign>?
[
|| <decimal>? '.' <decimal> <exponent>?
|| <decimal> <exponent>
]
}
무엇을 매칭할 것인가
입력 데이터 형식에 명확한 명세가 없거나, 프로그래머가 명세를 모를 때가 많아요. 그럴 땐 기대하는 바를 너그럽게(liberal) 두는 게 좋은데, 단 모호함이 생길 가능성이 없을 때만 그래요.
예를 들어 ini 파일을 봐요:
[section]
key=value
섹션 헤더 안에는 뭐가 들어갈 수 있을까요? 단어 하나만 허용하는 건 너무 제한적일 수 있어요. 누군가는 [two words]처럼 쓰거나 대시를 쓸 수도 있으니까요. "안에 뭐가 허용되나"를 묻는 대신, **"뭐가 허용되지 않나?"**를 물어보는 편이 나을 때가 있어요.
분명히 닫는 대괄호는 허용되면 안 돼요. [a]b]가 모호해지거든요. 같은 논리로 여는 대괄호도 금지해야 해요. 그러면 이렇게 남아요:
token header { '[' <-[ \[\]] >+ ']' }
이건 한 줄만 처리한다면 괜찮아요. 하지만 파일 전체를 처리한다면 이 정규식이 이런 것까지 파싱해 버려요:
[with a
newline in between]
바람직하지 않을 수 있죠. 절충안은 이렇게 하는 거예요:
token header { '[' <-[ \[\]] \n >+ ']' }
그리고 후처리에서 섹션 헤더의 앞뒤 공백·탭을 떼어내면 돼요.
공백 매칭
:sigspace 어드버브(또는 token/regex 대신 rule 선언자)는 여러 곳에 나타날 수 있는 공백을 암시적으로 파싱할 때 아주 유용해요.
ini 파일 파싱 예제로 돌아가 볼게요:
my regex kvpair { \s* <key=identifier> '=' <value=identifier> \n+ }
사용자가 등호 주변에 공백을 넣을 수 있으니 이건 우리가 원하는 만큼 너그럽지 않아요. 그래서 이렇게 해볼 수 있는데:
my regex kvpair { \s* <key=identifier> \s* '=' \s* <value=identifier> \n+ }
이건 다루기 어려워 보이죠. 그래서 다른 방법을 써 볼게요:
my rule kvpair { <key=identifier> '=' <value=identifier> \n+ }
잠깐! 값 뒤의 암시적 공백 매칭이 줄바꿈 문자를 포함한 모든 공백을 다 써버려서, \n+가 매칭할 게 남지 않아요 (rule은 백트래킹도 비활성화하니까 방법도 없고요).
따라서 암시적 공백의 정의를 "입력 형식에서 의미 없는 공백"으로 다시 정의하는 게 중요해요.
이건 ws 토큰을 재정의해서 하며, 그래머 안에서만 동작해요:
grammar IniFormat {
token ws { <!ww> \h* }
rule header { \s* '[' (\w+) ']' \n+ }
token identifier { \w+ }
rule kvpair { \s* <key=identifier> '=' <value=identifier> \n+ }
token section {
<header>
<kvpair>*
}
token TOP {
<section>*
}
}
my $contents = q:to/EOI/;
[passwords]
jack = password1
joy = muchmoresecure123
[quotas]
jack = 123
joy = 42
EOI
say so IniFormat.parse($contents);
모든 정규식을 그래머로 모으고 토큰으로 바꾼 것(어차피 백트래킹할 필요가 없으니) 외에, 흥미로운 새 부분은 이거예요:
token ws { <!ww> \h* }
이건 암시적 공백 파싱을 위해 호출돼요. 두 단어 문자 사이가 아닐 때(<!ww>, 부정된 "단어 안" 단언) 매칭되고, 가로 공백 문자 0개 이상에서도 매칭돼요. 가로 공백으로 제한하는 게 중요한데, 줄바꿈(세로 공백)은 레코드를 구분하는 문자라 암시적으로 매칭되면 안 되기 때문이에요.
그래도 공백 관련 함정이 하나 더 있어요. 정규식 \n+는 "\n \n" 같은 문자열과 매칭되지 않아요. 두 줄바꿈 사이에 빈칸이 있으니까요. 그런 입력을 허용하려면 \n+를 \n\s*로 바꾸면 돼요.