Grammar
Grammar
텍스트를 규칙에 따라 분석하고 싶을 때, Raku에서는 grammar 선언으로 문법을 정의할 수 있어요. 그렇게 선언된 모든 문법 타입의 기반이 되는 클래스가 바로 Grammar예요.
본문
Grammar는 Match를 상속하는 클래스예요. grammar로 선언하면서 슈퍼클래스를 따로 명시하지 않은 모든 타입은 Grammar의 서브클래스가 됩니다.
class Grammar is Match {}
예를 들어 식별자(identifier)를 파싱하는 문법을 하나 만들어 보면, 우리가 만든 Identifier 문법이 실제로 Grammar의 일종인지 확인할 수 있어요.
grammar Identifier {
token TOP { <initial> <rest>* }
token initial { <+myletter +[_]> }
token rest { <+myletter +mynumber +[_]> }
token myletter { <[A..Za..z]> }
token mynumber { <[0..9]> }
}
say Identifier.isa(Grammar); # OUTPUT: «True»
my $match = Identifier.parse('W4anD0eR96');
say ~$match; # OUTPUT: «W4anD0eR96»
문법에 대한 더 자세한 내용은 문법(grammars) 문서와 문법 튜토리얼에서 다뤄요.
메서드
method parse
method parse($target, :$rule = 'TOP', Capture() :$args = \(), Mu :$actions = Mu, *%opt)
$target을 파싱해요. $target이 Str이 아니면 Str로 강제 변환되고, 시작 규칙으로는 $rule을 써요. $args를 주면 시작 규칙에 추가 인자로 전달됩니다.
grammar RepeatChar {
token start($character) { $character+ }
}
say RepeatChar.parse('aaaaaa', :rule('start'), :args(\('a')));
say RepeatChar.parse('bbbbbb', :rule('start'), :args(\('b')));
# OUTPUT:
# 「aaaaaa」
# 「bbbbbb」
actions라는 이름 인자를 넘기면, 각 regex 매치가 성공할 때마다 같은 이름의 메서드가 actions 객체에서 호출돼요. 이때 매치 객체가 유일한 위치 인자로 전달되지요.
my $actions = class { method TOP($/) { say "7" } };
grammar { token TOP { a { say "42" } b } }.parse('ab', :$actions);
# OUTPUT: «427»
추가 이름 인자는 매칭 옵션으로 쓰여요. 예를 들어 :pos(4)를 주면 0부터 세는 기준으로 다섯 번째 문자부터 파싱을 시작해요. regex가 가질 수 있는 매칭 부사(adverb)는 여러 종류가 있는데, :s나 :i처럼 컴파일 타임에 적용되는 부사는 .parse에 넘길 수 없어요. regex는 이미 컴파일되어 있으니까요. 대신 :pos나 :continue처럼 런타임 동작에 영향을 주는 부사는 넘길 수 있습니다.
say RepeatChar.parse('bbbbbb', :rule('start'), :args(\('b')), :pos(4)).Str;
# OUTPUT: «bb»
parse 메서드는 매치가 끝났을 때 커서(cursor)가 대상 문자열의 끝에 도달해야만 성공해요. 중간에서 멈출 수 있게 하려면 subparse 메서드를 쓰면 됩니다.
문법의 최상위 regex는 백트래킹(backtrack)할 수 있게 허용돼요. 성공하면 Match를, 실패하면 Nil을 돌려줘요.
method subparse
method subparse($target, :$rule = 'TOP', Capture() :$args = \(), Mu :$actions = Mu, *%opt)
parse와 거의 똑같이 동작하지만, 한 가지가 달라요. 성공하기 위해 커서가 문자열 끝까지 도달할 필요가 없어요. 즉 전체 문자열을 맞출 필요가 없다는 뜻이죠.
parse와 달리 subparse는 항상 Match를 돌려줘요. 문법이 매치에 실패하면 실패한 매치(그래서 거짓인 값)를 돌려주는 식이에요.
grammar RepeatChar {
token start($character) { $character+ }
}
say RepeatChar.subparse('bbbabb', :rule('start'), :args(\('b')));
say RepeatChar.parse( 'bbbabb', :rule('start'), :args(\('b')));
say RepeatChar.subparse('bbbabb', :rule('start'), :args(\('a')));
say RepeatChar.subparse('bbbabb', :rule('start'), :args(\('a')), :pos(3));
# OUTPUT:
# 「bbb」
# Nil
# #<failed match>
# 「a」
method parsefile
method parsefile(Str(Cool) $filename, :$enc, *%opts)
$enc 인코딩으로 $filename 파일을 읽어서 파싱해요. 모든 이름 인자는 parse 메서드로 그대로 전달됩니다.
grammar Identifiers {
token TOP { [<identifier><.ws>]+ }
token identifier { <initial> <rest>* }
token initial { <+myletter +[_]> }
token rest { <+myletter +mynumber +[_]> }
token myletter { <[A..Za..z]> }
token mynumber { <[0..9]> }
}
say Identifiers.parsefile('users.txt', :enc('UTF-8'))
.Str.trim.subst(/\n/, ',', :g);
# users.txt :
# TimToady
# lizmat
# jnthn
# moritz
# zoffixznet
# MasterDuke17
# OUTPUT: «TimToady,lizmat,jnthn,moritz,zoffixznet,MasterDuke17»