具有子表达式语法的简单Instaparse解析器

时间:2013-08-16 21:41:13

标签: parsing clojure

我正在使用Instaparse来解析像:

这样的表达式

$(foo bar baz $(frob))

成像:

[:expr "foo" "bar" "baz" [:expr "frob"]]

我差不多了,但是模糊不清。这是我的语法的简化版本,即repros,试图依赖负向前瞻。

(def simple
  (insta/parser
    "expr = <dollar> <lparen> word (<space> word)* <rparen>
     <word> = !(dollar lparen) #'.+' !(rparen)
     <space> = #'\\s+'
     <dollar> = <'$'>
     <lparen> = <'('>
     <rparen> = <')'>"))

(simple "$(foo bar)")

哪些错误:

Parse error at line 1, column 11:
$(foo bar)
          ^
Expected one of:
")"
#"\s+"

这里我说过一个单词可以是任何字符,以便支持如下表达式:

$(foo () `bar` b-a-z)

等。请注意,单词可以包含(),但不能包含$()。不知道如何在语法中表达这一点。似乎问题是<word>过于贪婪,消耗了最后)而不是让expr拥有它。


更新从word中删除了空格:

(def simple2
  (insta/parser
    "expr = <dollar> <lparen> word (<space> word)* <rparen>
     <word> = !(dollar lparen) #'[^ ]+' !(rparen)
     <space> = #'\\s+'
     <dollar> = <'$'>
     <lparen> = <'('>
     <rparen> = <')'>"))


(simple2 "$(foo bar)")
; Parse error at line 1, column 11:
; $(foo bar)
;           ^
; Expected one of:
; ")"
; #"\s+"

(simple2 "$(foo () bar)")
; Parse error at line 1, column 14:
; $(foo () bar)
;              ^
; Expected one of:
; ")"
; #"\s+"

更新2 更多测试用例

(simple2 "$(foo bar ())")
(simple2 "$((foo bar baz))")

更新3 完整的解析器

对于任何好奇的人来说,完全正常工作的解析器超出了这个问题的范围:

(def parse
  "expr     - the top-level expression made up of cmds and sub-exprs. When multiple
              cmds are present, it implies they should be sucessively piped.
   cmd      - a single command consisting of words.
   sub-expr - a backticked or $(..)-style sub-expression to be evaluated inline.
   parened  - a grouping of words wrapped in parenthesis, explicitly tokenized to 
              allow parenthesis in cmds and disambiguate between sub-expression 
              syntax."
  (insta/parser
    "expr = cmd (<space> <pipe> <space> cmd)*
     cmd = words
     <sub-expr> = <backtick> expr <backtick> | nestable-sub-expr
     <nestable-sub-expr> = <dollar> <lparen> expr <rparen>
     words = word (<space>* word)*
     <word> = sub-expr | parened | word-chars
     <word-chars> = #'[^ `$()|]+'
     parened = lparen words rparen
     <space> = #'[ ]+'
     <pipe> = #'[|]'
     <dollar> = <'$'>
     <lparen> = '('
     <rparen> = ')'
     <backtick> = <'`'>"))

使用示例:

(parse "foo bar (qux) $(clj (map (partial * $(js 45 * 2)) (range 10))) `frob`")

解析:

[:expr [:cmd [:words "foo" "bar" [:parened "(" [:words "qux"] ")"] [:expr [:cmd [:words "clj" [:parened "(" [:words "map" [:parened "(" [:words "partial" "*" [:expr [:cmd [:words "js" "45" "*" "2"]]]] ")"] [:parened "(" [:words "range" "10"] ")"]] ")"]]]] [:expr [:cmd [:words "frob"]]]]]]

这是我写的聊天机器人yetibot的解析器。它取代了之前基于正则表达式的混乱解析。

2 个答案:

答案 0 :(得分:2)

我真的不知道instaparser,所以我只是阅读了足够的文档来给我一种虚假的安全感。我也没有测试,我真的不知道你的要求是什么。

特别是,我不知道:

1)$()是否可以嵌套(我认为你的语法不可能,但我觉得这很奇怪)

2)()是否可以包含空格而不被解析为多个单词

3)()是否可以包含$()

你需要清楚这样的事情才能写出语法(或者,当它发生时,要求建议)。

更新根据评论修改语法。我删除了$ ()的作品,因为它们似乎没必要,这样角括号也更容易处理。

以下是基于回答上述问题“是,不,是”以及关于正则表达式格式的一些随机假设。 (我不完全清楚角括号是如何工作的,但我认为用括号输出你想要的方式并不容易;我决定将它们作为单个元素输出。如果我弄清楚了什么,我我会编辑它。)

<sequence> = element (<space> element)*
<element> = expr | paren_sequence | word
expr = <'$'> <'('> sequence <')'>
<word> = !('$'? '(') #'([^ $()]|\$[^(])+'
<paren_sequence> = '(' sequence ')' 
<space> = #'\\s+'

希望有所帮助。

答案 1 :(得分:2)

您必须进行两项更改才能使两个示例都正常工作。

1)添加负面背后隐藏

首先,您需要<word>的正则表达式中的负 lookbehind 。这样,它会将)的所有出现次数作为最后一个字符:

 <word> = !(dollar lparen) #'[^ ]+(?<!\\))' 

所以这将修复你的第一个测试用例:

(simple2 "$(foo bar)")
=> [:expr "foo" "bar"]

2)为最后一个单词添加语法

现在,如果您运行第二个测试用例,它将失败:

(simple2 "$(foo () bar)")
=> Parse error at line 1, column 8:
   $(foo () bar)
          ^ 
   Expected one of: 
   ")" (followed by end-of-string)
   #"\s+"

这失败了,因为我们已经告诉我们的语法删除)的所有实例中的最后<word>。我们现在必须告诉我们的语法如何区分<word>的最后一个实例和其他实例。我们将通过添加特定的<lastword>语法来完成此操作,并使<word>的所有其他实例都可选。完整的语法看起来像这样:

(def simple2
  (insta/parser
    "expr = <dollar> <lparen> word* lastword <rparen>
     <word>  = !(dollar lparen) #'[^ ]+' <space>+
     <lastword> = !(dollar lparen) #'[^ ]+(?<!\\))' 
     <space> = #'\\s+'
     <dollar> = <'$'>
     <lparen> = <'('>
     <rparen> = <')'>")) 

你的两个测试用例应该可以正常工作:

(simple2 "$(foo bar)")
=> [:expr "foo" "bar"]

(simple2 "$(foo () bar)")
=> [:expr "foo" "()" "bar"]

希望这有帮助。