Search code examples
pythonparsingcommand-linepyparsingcommand-line-arguments

network device command parsing with pyparsing


I`m developing network device command parser using pyparsing.

I analysed and define the command format as below:

cli ::= string + (next)*
next ::= string|range|group|simple_recursive|selective_recursive|infinite_recursive|keywords
keywords ::= "WORD"
             | "LINE" 
             | "A.B.C.D" 
             | "A.B.C.D/M" 
             | "X:X::X:X" 
             | "X:X::X:X/M" 
             | "HH:MM:SS" 
             | "AA:NN" 
             | "XX:XX:XX:XX:XX:XX" 
             | "MULTILINE"
inner_recur ::= next + (next)* + ("|")* | ("|" + next + (next)*)*
string ::= alphanums + "_" + "-"
range ::= "<" + nums + "-" nums + ">"
group ::= "(" + inner_recur + ")"
simple_recursive ::= "." + range
selective_recursive ::= "{" + inner_recur + "}"
infinite_recursive ::= "[" + inner_recur + "]"

and implemented written:

# string ::= alphanums + "_" + "-"
string_ = Word(alphanums + "_" + "-").setResultsName("string")
#print(string_.parseString("option82"))

# range ::= "<" + nums + "-" nums + ">"
range_ = Combine(Literal("<") + Word(nums) + Literal("-") + Word(nums) + Literal(">")).setResultsName("range")
#print(range_.parseString("<24-1004>"))

# simple_recursive ::= "." + range
simple_recursive_ = Combine(Literal(".") + range_).setResultsName("simple_recursive")
#print(simple_recursive_.parseString(".<1-60045>"))

# keywords ::= "WORD" | "LINE" | "A.B.C.D" | "A.B.C.D/M" | "X:X::X:X" | "X:X::X:X/M" | "HH:MM:SS" | "AA:NN" | "XX:XX:XX:XX:XX:XX" | "MULTILINE"
keywords_ = Keyword("X:X::X:X/M").setResultsName("X:X::X:/M") | Keyword("A.B.C.D/M").setResultsName("A.B.C.D/M") | Keyword("A.B.C.D").setResultsName("A.B.C.D") | Keyword("X:X::X:X").setResultsName("X:X::X:X") | Keyword("HH:MM:SS").setResultsName("HH:MM:SS") | Keyword("AA:NN").setResultsName("AA:NN") | Keyword("XX:XX:XX:XX:XX:XX").setResultsName("XX:XX:XX:XX:XX:XX") | Keyword("MULTILINE").setResultsName("MULTILINE") | Keyword("WORD").setResultsName("WORD") | Keyword("LINE").setResultsName("LINE")
#print(keywords_.parseString("A.B.C.D").asXML())


#next_ = Forward()
inner_recur = Forward()

# group ::= "(" + inner_recur + ")"
group_ = Combine(Literal("(") + inner_recur + Literal(")"))

# selective_recursive ::= "{" + inner_recur + "}"
selective_recursive_ = Combine(Literal("{") + inner_recur + Literal("}"))

# infinite_recursive ::= "[" + inner_recur + "]"
infinite_recursive_ = Combine(Literal("[") + inner_recur + Literal("]"))

# next ::= string|range|group|simple_recursive|selective_recursive|infinite_recursive|keywords
next_ = keywords_ | string_ | simple_recursive_ | range_ | group_ | selective_recursive_ | infinite_recursive_

# inner_recur ::= next + (next)* + ("|")* | ("|" + next + (next)*)*
inner_recur << next_ + ZeroOrMore(next_) + ZeroOrMore(Literal("|") | ZeroOrMore(Literal("|") + next_ + OneOrMore(next_)))

# cli ::= string + (next)*
cli_ = string_ + ZeroOrMore(next_)

To test my parser, I tried to input datas

>>> test = cli_.parseString("bgp as .<1-200>")
>>> print(test)
>>> ['bgp', 'as', ['.<1-200>']]
test = cli_.parseString("bgp as <1-200> <1-255> <1-255> WORD A.B.C.D A.B.C.D/M (A|(B|C))")
print(test)
>>> 
test = cli_.parseString("test (A|<1-200>|(B|{a|b|c} aaa)")
test = cli_.parseString("test (A|<1-200>|(B|{a|b|c|})|)")

when parsed second data, infinite recursion raised. I don't understand this situation and have any solution...

I expect the result:

['bgp', 'as', ['<1-200>'], ['<1-255>'], ['<1-255>'], 'WORD', 'A.B.C.D', 'A.B.C.D/M', ['A', ['B', 'C']]]

what is my problem in format or code? and point be modified?


Solution

  • While you have made a good first step in defining your grammar in conceptual BNF terms before writing code, I'm struggling a bit with making sense of your grammar. The culprit to me seems to be this part:

    inner_recur ::= next + (next)* + ("|")* | ("|" + next + (next)*)*
    

    From your posted examples, this looks like you are trying to define some sort of infix notation, using '|' as an operator.

    From your tests, it also looks like you need to support multiple inner_recur terms within any grouping ()'s, []'s, or {}'s.

    Also, please read the docs (https://pyparsing-docs.readthedocs.io/en/latest/pyparsing.html) to get a clearer picture of the difference between setResultsName and setName. I'm pretty sure in your parser throughout, you are using setResultsName but really want setName. Similarly with using Combine when you really want Group.

    Lastly, I rewrote your test code using runTests, and saw that you had mismatched ()'s on the third test.

    Here is your parser with these changes:

    # string ::= alphanums + "_" + "-"
    string_ = Word(alphanums + "_" + "-").setResultsName("string")
    #print(string_.parseString("option82"))
    
    # range ::= "<" + nums + "-" nums + ">"
    range_ = Group(Literal("<") + Word(nums) + Literal("-") + Word(nums) + Literal(">")).setResultsName("range")
    #print(range_.parseString("<24-1004>"))
    
    # simple_recursive ::= "." + range
    simple_recursive_ = Group(Literal(".") + range_).setResultsName("simple_recursive")
    #print(simple_recursive_.parseString(".<1-60045>"))
    
    # keywords ::= "WORD" | "LINE" | "A.B.C.D" | "A.B.C.D/M" | "X:X::X:X" | "X:X::X:X/M" | "HH:MM:SS" | "AA:NN" | "XX:XX:XX:XX:XX:XX" | "MULTILINE"
    keywords_ = Keyword("X:X::X:X/M").setResultsName("X:X::X:/M") | Keyword("A.B.C.D/M").setResultsName("A.B.C.D/M") | Keyword("A.B.C.D").setResultsName("A.B.C.D") | Keyword("X:X::X:X").setResultsName("X:X::X:X") | Keyword("HH:MM:SS").setResultsName("HH:MM:SS") | Keyword("AA:NN").setResultsName("AA:NN") | Keyword("XX:XX:XX:XX:XX:XX").setResultsName("XX:XX:XX:XX:XX:XX") | Keyword("MULTILINE").setResultsName("MULTILINE") | Keyword("WORD").setResultsName("WORD") | Keyword("LINE").setResultsName("LINE")
    #print(keywords_.parseString("A.B.C.D").asXML())
    
    #next_ = Forward()
    inner_recur = Forward()
    
    # group ::= "(" + inner_recur + ")"
    group_ = Group(Literal("(") + OneOrMore(inner_recur) + Literal(")"))
    
    # selective_recursive ::= "{" + inner_recur + "}"
    selective_recursive_ = Group(Literal("{") + OneOrMore(inner_recur) + Literal("}"))
    
    # infinite_recursive ::= "[" + inner_recur + "]"
    infinite_recursive_ = Group(Literal("[") + OneOrMore(inner_recur) + Literal("]"))
    
    # next ::= string|range|group|simple_recursive|selective_recursive|infinite_recursive|keywords
    next_ = keywords_ | string_ | simple_recursive_ | range_ | group_ | selective_recursive_ | infinite_recursive_
    #~ next_.setName("next_").setDebug()
    
    # inner_recur ::= next + (next)* + ("|")* | ("|" + next + (next)*)*
    #~ inner_recur <<= OneOrMore(next_) + ZeroOrMore(Literal("|")) | ZeroOrMore(Literal("|") + OneOrMore(next_))
    inner_recur <<= Group(infixNotation(next_,
        [
            (None, 2, opAssoc.LEFT),
            ('|', 2, opAssoc.LEFT),
        ]) + Optional('|'))
    # cli ::= string + (next)*
    cli_ = string_ + ZeroOrMore(next_)
    
    
    tests = """\
    bgp as .<1-200>
    bgp as <1-200> <1-255> <1-255> WORD A.B.C.D A.B.C.D/M (A|(B|C))
    test (A|<1-200>|(B|{a|b|c} aaa))
    test (A|<1-200>|(B|{a|b|c|})|)
    """
    cli_.runTests(tests)
    

    Which gives:

    bgp as .<1-200>
    ['bgp', 'as', ['.', ['<', '1', '-', '200', '>']]]
    - simple_recursive: ['.', ['<', '1', '-', '200', '>']]
      - range: ['<', '1', '-', '200', '>']
    - string: 'as'
    
    
    bgp as <1-200> <1-255> <1-255> WORD A.B.C.D A.B.C.D/M (A|(B|C))
    ['bgp', 'as', ['<', '1', '-', '200', '>'], ['<', '1', '-', '255', '>'], ['<', '1', '-', '255', '>'], 'WORD', 'A.B.C.D', 'A.B.C.D/M', ['(', [['A', '|', ['(', [['B', '|', 'C']], ')']]], ')']]
    - A.B.C.D: 'A.B.C.D'
    - A.B.C.D/M: 'A.B.C.D/M'
    - WORD: 'WORD'
    - range: ['<', '1', '-', '255', '>']
    - string: 'as'
    
    
    test (A|<1-200>|(B|{a|b|c} aaa))
    ['test', ['(', [['A', '|', ['<', '1', '-', '200', '>'], '|', ['(', [['B', '|', [['{', [['a', '|', 'b', '|', 'c']], '}'], 'aaa']]], ')']]], ')']]
    - string: 'test'
    
    
    test (A|<1-200>|(B|{a|b|c|})|)
    ['test', ['(', [['A', '|', ['<', '1', '-', '200', '>'], '|', ['(', [['B', '|', ['{', [['a', '|', 'b', '|', 'c'], '|'], '}']]], ')']], '|'], ')']]
    - string: 'test'
    

    This may be off the mark in some places, but I hope it gives you some ideas to move forward with your project.