Skip to content

Instantly share code, notes, and snippets.

@alexspeller
Created September 1, 2012 23:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexspeller/3590638 to your computer and use it in GitHub Desktop.
Save alexspeller/3590638 to your computer and use it in GitHub Desktop.
Docopt Parselet proof of concept
require 'parslet'
class Docopt < Parslet::Parser
# ==========
# = Basics =
# ==========
# Simple Tokens
rule(:equals) { str('=') }
rule(:newline) { str("\n") }
rule(:lt) { str('<') }
rule(:gt) { str('>') }
rule(:lbracket) { str('(') }
rule(:rbracket) { str(')') }
rule(:lsquare) { str('[') }
rule(:rsquare) { str(']') }
rule(:tab) { str("\t") }
rule(:dash) { str('-') }
rule(:pipe) { str('|') }
rule(:dots) { str('...').as(:dots) }
rule(:options_shortcut) { str("[options]").as(:options) } # just matches "[options]" as its own shortcut
# some more complex tokens
rule(:identifier) { match['a-z'].repeat(1) } # matches any word a-z
rule(:exclusive) { whitespace.maybe >> pipe >> whitespace.maybe } # matches "|" or " | "
rule(:option_start) { dash >> dash.maybe } # matches "-" or "--"
# types of whitespace
rule(:whitespace) { (spaces | newline | tab).repeat(1) } # any whitespace
rule(:indentation) { (spaces | tab).repeat(1) } # space or tab only
rule(:spaces) { str(" ").repeat(1) } # space only
# ===================
# = Root level rule =
# ===================
# This is the main rule for the parser, describing the root level
rule(:docstring) { title_and_description >> usage >> whitespace >> options >> whitespace.maybe }
root :docstring
# Matches anything up to "Usage:"
rule(:title_and_description) { (usage.absent? >> any).repeat(1).as(:title_and_description) }
# ==================
# = Usage examples =
# ==================
# this is the root level rule matching the whole usage examples section
rule(:usage) { str('Usage:') >> newline.maybe >> usage_examples.as(:usage_examples) }
rule(:usage_examples) { usage_example.repeat(1) }
rule(:usage_example) { (indentation >> program_name >> usage_content >> newline) }
rule(:program_name) { identifier }
rule(:usage_content) { (newline.absent? >> usage_body.as(:example)) >> dots.maybe }
# match an actual usage specification after stripping off program name and end of line
rule(:usage_body) { (options_shortcut | command_spec | argument_spec | option_spec | spaces).repeat(1) }
# commands in the usage line
rule(:command_spec) { optional(command | command_set, :command_set) } # match commands in or out of []
rule(:command) { identifier.as(:command) }
rule(:command_set) { lbracket >> command_list >> rbracket } # match commands inside brackets e.g. "(set|delete)"
rule(:command_list) { command >> (exclusive >> command).repeat(0) } # matches "set", "set|delete", "set|delete|vaporize"
# arguments in usage line
rule(:argument_spec) { optional(argument, :argument) } # match arguments in or out of []
rule(:argument) { lt >> identifier >> gt } # match "<foo>"
# options in usage line
rule(:option_spec) { optional(option_set, :option_set) } # match options in or out of []
rule(:option_set) { option_alternative >> other_alternatives.repeat(0) } # matches "--left", "--left|--right"
rule(:other_alternatives) { exclusive >> option_alternative } # matches "|--right"
rule(:option_alternative) { option_start >> (option_with_argument | boolean_option) } # matches "--foo", "-f", "--foo=<bar>", "-f <bar>"
rule(:boolean_option) { identifier.as(:option) } # matches "foo"
rule(:option_with_argument) { identifier.as(:option) >> (spaces | equals) >> argument.as(:argument) } # matches "--foo=<bar>", "-f <bar>"
# matches "[thing]" or "thing". if it matches "[thing]", tag it as "optional_name", otherwise
# just tag it as "name"
def optional thing, name
(lsquare >> thing.as(:"optional_#{name}") >> rsquare) | thing.as(name)
end
# ================
# = Option lines =
# ================
# the root level rule for the whole options section
rule(:options) { str('Options:') >> newline >> option_lines.as(:option_lines) }
rule(:option_lines) { option_line.repeat(1) } # one or more lines
rule(:option_line) { indentation >> option_names >> indentation.maybe >> option_description.maybe } # description of a line
rule(:option_names) { (option_alternative | whitespace).repeat(1) } # one or more alternative names e.g. "-h --help"
rule(:option_description) { (newline.absent? >> any).repeat(1).as(:description) } # match the description to the end of the line
end
DOC = "Naval Fate.
Usage:
naval ship new <name>
naval ship new <name>...
naval ship new [options]
\tnaval ship <name> move <x> <y> --fast
naval ship <name> [move] <x> <y>
naval ship [<name>] [move] <x> <y>
naval ship <name> move <x> <y> [--fast]
naval ship <name> move <x> <y> --speed=<kn>
naval ship <name> move <x> <y> -s <kn>
naval ship <name> move <x> <y> [--fast|--slow]
naval ship <name> move <x> <y> [--fast | --slow]
naval mine (set|remove) <x> <y> [--moored|--drifting]
naval --version
Options:
-h --help
-h --help Show this screen.
"
require 'pp'
pp Docopt.new.parse(DOC)
{:title_and_description=>"Naval Fate.\n\n"@0,
:usage_examples=>
[{:example=>
[{:command_set=>{:command=>"ship"@28}},
{:command_set=>{:command=>"new"@33}},
{:argument=>"<name>"@37}]},
{:example=>
[{:command_set=>{:command=>"ship"@52}},
{:command_set=>{:command=>"new"@57}},
{:argument=>"<name>"@61}],
:dots=>"..."@67},
{:example=>
[{:command_set=>{:command=>"ship"@79}},
{:command_set=>{:command=>"new"@84}},
{:options=>"[options]"@88}]},
{:example=>
[{:command_set=>{:command=>"ship"@105}},
{:argument=>"<name>"@110},
{:command_set=>{:command=>"move"@117}},
{:argument=>"<x>"@122},
{:argument=>"<y>"@126},
{:option_set=>{:option=>"fast"@132}}]},
{:example=>
[{:command_set=>{:command=>"ship"@145}},
{:argument=>"<name>"@150},
{:optional_command_set=>{:command=>"move"@158}},
{:argument=>"<x>"@164},
{:argument=>"<y>"@168}]},
{:example=>
[{:command_set=>{:command=>"ship"@180}},
{:optional_argument=>"<name>"@186},
{:optional_command_set=>{:command=>"move"@195}},
{:argument=>"<x>"@201},
{:argument=>"<y>"@205}]},
{:example=>
[{:command_set=>{:command=>"ship"@217}},
{:argument=>"<name>"@222},
{:command_set=>{:command=>"move"@229}},
{:argument=>"<x>"@234},
{:argument=>"<y>"@238},
{:optional_option_set=>{:option=>"fast"@245}}]},
{:example=>
[{:command_set=>{:command=>"ship"@259}},
{:argument=>"<name>"@264},
{:command_set=>{:command=>"move"@271}},
{:argument=>"<x>"@276},
{:argument=>"<y>"@280},
{:option_set=>{:option=>"speed"@286, :argument=>"<kn>"@292}}]},
{:example=>
[{:command_set=>{:command=>"ship"@305}},
{:argument=>"<name>"@310},
{:command_set=>{:command=>"move"@317}},
{:argument=>"<x>"@322},
{:argument=>"<y>"@326},
{:option_set=>{:option=>"s"@331, :argument=>"<kn>"@333}}]},
{:example=>
[{:command_set=>{:command=>"ship"@346}},
{:argument=>"<name>"@351},
{:command_set=>{:command=>"move"@358}},
{:argument=>"<x>"@363},
{:argument=>"<y>"@367},
{:optional_option_set=>[{:option=>"fast"@374}, {:option=>"slow"@381}]}]},
{:example=>
[{:command_set=>{:command=>"ship"@395}},
{:argument=>"<name>"@400},
{:command_set=>{:command=>"move"@407}},
{:argument=>"<x>"@412},
{:argument=>"<y>"@416},
{:optional_option_set=>[{:option=>"fast"@423}, {:option=>"slow"@432}]}]},
{:example=>
[{:command_set=>{:command=>"mine"@446}},
{:command_set=>[{:command=>"set"@452}, {:command=>"remove"@456}]},
{:argument=>"<x>"@464},
{:argument=>"<y>"@468},
{:optional_option_set=>
[{:option=>"moored"@475}, {:option=>"drifting"@484}]}]},
{:example=>[{:option_set=>{:option=>"version"@504}}]}],
:option_lines=>
[{:option=>"h"@525},
{:option=>"help"@529},
{:option=>"h"@537},
{:option=>"help"@541},
{:description=>"Show this screen."@550}]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment