command_mapper-gen 0.1.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +27 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +1 -0
  5. data/.yardopts +1 -0
  6. data/ChangeLog.md +20 -0
  7. data/Gemfile +17 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.md +145 -0
  10. data/Rakefile +15 -0
  11. data/bin/command_mapper-gen +7 -0
  12. data/commnad_mapper-gen.gemspec +61 -0
  13. data/examples/grep.rb +63 -0
  14. data/gemspec.yml +26 -0
  15. data/lib/command_mapper/gen/arg.rb +43 -0
  16. data/lib/command_mapper/gen/argument.rb +53 -0
  17. data/lib/command_mapper/gen/cli.rb +233 -0
  18. data/lib/command_mapper/gen/command.rb +202 -0
  19. data/lib/command_mapper/gen/exceptions.rb +9 -0
  20. data/lib/command_mapper/gen/option.rb +66 -0
  21. data/lib/command_mapper/gen/option_value.rb +23 -0
  22. data/lib/command_mapper/gen/parsers/common.rb +49 -0
  23. data/lib/command_mapper/gen/parsers/help.rb +351 -0
  24. data/lib/command_mapper/gen/parsers/man.rb +80 -0
  25. data/lib/command_mapper/gen/parsers/options.rb +127 -0
  26. data/lib/command_mapper/gen/parsers/usage.rb +141 -0
  27. data/lib/command_mapper/gen/parsers.rb +2 -0
  28. data/lib/command_mapper/gen/task.rb +90 -0
  29. data/lib/command_mapper/gen/types/enum.rb +30 -0
  30. data/lib/command_mapper/gen/types/key_value.rb +34 -0
  31. data/lib/command_mapper/gen/types/list.rb +34 -0
  32. data/lib/command_mapper/gen/types/map.rb +36 -0
  33. data/lib/command_mapper/gen/types/num.rb +18 -0
  34. data/lib/command_mapper/gen/types/str.rb +48 -0
  35. data/lib/command_mapper/gen/types.rb +6 -0
  36. data/lib/command_mapper/gen/version.rb +6 -0
  37. data/lib/command_mapper/gen.rb +2 -0
  38. data/spec/argument_spec.rb +92 -0
  39. data/spec/cli_spec.rb +269 -0
  40. data/spec/command_spec.rb +316 -0
  41. data/spec/option_spec.rb +85 -0
  42. data/spec/option_value_spec.rb +20 -0
  43. data/spec/parsers/common_spec.rb +616 -0
  44. data/spec/parsers/help_spec.rb +612 -0
  45. data/spec/parsers/man_spec.rb +158 -0
  46. data/spec/parsers/options_spec.rb +802 -0
  47. data/spec/parsers/usage_spec.rb +1175 -0
  48. data/spec/spec_helper.rb +6 -0
  49. data/spec/task_spec.rb +69 -0
  50. data/spec/types/enum_spec.rb +45 -0
  51. data/spec/types/key_value_spec.rb +36 -0
  52. data/spec/types/list_spec.rb +36 -0
  53. data/spec/types/map_spec.rb +48 -0
  54. data/spec/types/str_spec.rb +70 -0
  55. metadata +133 -0
@@ -0,0 +1,351 @@
1
+ require 'command_mapper/gen/parsers/options'
2
+ require 'command_mapper/gen/parsers/usage'
3
+ require 'command_mapper/gen/command'
4
+ require 'command_mapper/gen/exceptions'
5
+
6
+ module CommandMapper
7
+ module Gen
8
+ module Parsers
9
+ class Help
10
+
11
+ # @return [Command]
12
+ attr_reader :command
13
+
14
+ # The callback to pass any parser errors.
15
+ #
16
+ # @return [Proc(String, Parslet::ParserFailed), nil]
17
+ attr_reader :parser_error_callback
18
+
19
+ #
20
+ # Initializes the `--help` output parser.
21
+ #
22
+ # @param [Command] command
23
+ #
24
+ # @yield [line, parser_error]
25
+ # If a block is given, it will be used as a callback for any parser
26
+ # errors.
27
+ #
28
+ # @yieldparam [String] line
29
+ # The line that triggered the parser error.
30
+ #
31
+ # @yieldparam [Parslet::ParserFailed] parser_error
32
+ # The parser error.
33
+ #
34
+ def initialize(command,&block)
35
+ @command = command
36
+
37
+ @parser_error_callback = block
38
+ end
39
+
40
+ #
41
+ # Parses the `--help` output for the given command.
42
+ #
43
+ # @param [Command] command
44
+ # The command object to parse data into.
45
+ #
46
+ # @param [String] output
47
+ # The `--help` output to parse.
48
+ #
49
+ # @return [Command]
50
+ # The parsed command.
51
+ #
52
+ def self.parse(output,command,&block)
53
+ parser = new(command,&block)
54
+ parser.parse(output)
55
+
56
+ return command
57
+ end
58
+
59
+ #
60
+ # Runs the parser on the command's `--help` output.
61
+ #
62
+ # @param [Command] command
63
+ # The command object to parse data into.
64
+ #
65
+ # @return [Command, nil]
66
+ # Returns `nil` if the command could not be found.
67
+ #
68
+ # @raise [CommandNotInstalled]
69
+ # The command could not be found on the system.
70
+ #
71
+ def self.run(command,&block)
72
+ output = nil
73
+
74
+ begin
75
+ output = `#{command.command_string} --help 2>&1`
76
+ rescue Errno::ENOENT
77
+ # command not found
78
+ raise(CommandNotInstalled,"command #{command.command_name.inspect} is not installed")
79
+ end
80
+
81
+ if output.empty?
82
+ # --help not supported, fallback to trying -h
83
+ output = `#{command.command_string} -h 2>&1`
84
+ end
85
+
86
+ parse(output,command,&block) unless output.empty?
87
+ end
88
+
89
+ # List of argument names to ignore
90
+ IGNORED_ARGUMENT_NAMES = %w[option options opts]
91
+
92
+ #
93
+ # Determines whether to skip an argument based on it's name.
94
+ #
95
+ # @param [String] name
96
+ # The argument name.
97
+ #
98
+ # @return [Boolean]
99
+ # Indicates whether to skip the argument or not.
100
+ #
101
+ def ignore_argument?(name)
102
+ name == @command.command_name ||
103
+ IGNORED_ARGUMENT_NAMES.any? { |suffix|
104
+ name == suffix || name.end_with?(suffix)
105
+ }
106
+ end
107
+
108
+ #
109
+ # Parses an individual argument node.
110
+ #
111
+ # @param [Hash] node
112
+ # An argument node.
113
+ #
114
+ def parse_argument(argument,**kwargs)
115
+ name = argument[:name].to_s.downcase
116
+ keywords = kwargs
117
+
118
+ if argument[:repeats]
119
+ keywords[:repeats] = true
120
+ end
121
+
122
+ # ignore [OPTIONS] or [opts]
123
+ unless ignore_argument?(name)
124
+ @command.argument(name.to_sym,**keywords)
125
+ end
126
+ end
127
+
128
+ #
129
+ # Parses a node within the arguments node.
130
+ #
131
+ # @param [Hash] node
132
+ #
133
+ def parse_argument_node(node,**kwargs)
134
+ keywords = kwargs
135
+
136
+ if node[:repeats]
137
+ keywords[:repeats] = true
138
+ end
139
+
140
+ if node[:optional]
141
+ keywords[:required] = false
142
+
143
+ parse_arguments(node[:optional], **keywords)
144
+ else
145
+ parse_argument(node[:argument], **keywords)
146
+ end
147
+ end
148
+
149
+ #
150
+ # Parses a collection of arguments.
151
+ #
152
+ # @param [Array<Hash>, Hash] arguments
153
+ #
154
+ def parse_arguments(arguments,**kwargs)
155
+ case arguments
156
+ when Array
157
+ keywords = kwargs
158
+
159
+ if arguments.delete({repeats: '...'})
160
+ keywords[:repeats] = true
161
+ end
162
+
163
+ arguments.each do |node|
164
+ parse_argument_node(node,**keywords)
165
+ end
166
+ when Hash
167
+ parse_argument_node(arguments,**kwargs)
168
+ end
169
+ end
170
+
171
+ #
172
+ # Parses a `usage: ...` string into {#command}.
173
+ #
174
+ # @param [String] usage
175
+ #
176
+ def parse_usage(usage)
177
+ parser = Usage.new
178
+
179
+ # remove the command name and any subcommands
180
+ args = usage.sub("#{@command.command_string} ",'')
181
+
182
+ tree = begin
183
+ parser.args.parse(args)
184
+ rescue Parslet::ParseFailed => error
185
+ if @parser_error_callback
186
+ @parser_error_callback.call(usage,error)
187
+ end
188
+
189
+ return
190
+ end
191
+
192
+ parse_arguments(tree)
193
+ end
194
+
195
+ #
196
+ # Parses an option line (ex: ` -o, --opt VALUE Blah blah blah`)
197
+ # into {#command}.
198
+ #
199
+ # @param [String] line
200
+ # The option line to parse.
201
+ #
202
+ def parse_option_line(line)
203
+ parser = Parsers::Options.new
204
+ tree = begin
205
+ parser.parse(line)
206
+ rescue Parslet::ParseFailed => error
207
+ if @parser_error_callback
208
+ @parser_error_callback.call(line,error)
209
+ end
210
+
211
+ return
212
+ end
213
+
214
+ flag = tree[:long_flag] || tree[:short_flag]
215
+ keywords = {}
216
+
217
+ if tree[:equals]
218
+ keywords[:equals] = true
219
+ end
220
+
221
+ if tree[:optional]
222
+ if tree[:optional][:equals]
223
+ keywords[:equals] = :optional
224
+ end
225
+
226
+ value_node = tree[:optional][:value]
227
+ keywords[:value] = {required: false}
228
+ elsif tree[:value]
229
+ value_node = tree[:value]
230
+ keywords[:value] = {required: true}
231
+ end
232
+
233
+ if value_node
234
+ if value_node[:list]
235
+ separator = value_node[:list][:separator]
236
+
237
+ keywords[:value][:type] = Types::List.new(
238
+ separator: separator.to_s
239
+ )
240
+ elsif value_node[:key_value]
241
+ separator = value_node[:key_value][:separator]
242
+
243
+ keywords[:value][:type] = Types::KeyValue.new(
244
+ separator: separator.to_s
245
+ )
246
+ elsif value_node[:literal_values]
247
+ literal_values = []
248
+
249
+ value_node[:literal_values].each do |node|
250
+ literal_values << node[:string].to_s
251
+ end
252
+
253
+ # perform some value coercion
254
+ type = case literal_values
255
+ when %w[YES NO]
256
+ Types::Map.new(true => 'YES', false => 'NO')
257
+ when %w[Yes No]
258
+ Types::Map.new(true => 'Yes', false => 'No')
259
+ when %w[yes no]
260
+ Types::Map.new(true => 'yes', false => 'no')
261
+ when %w[Y N]
262
+ Types::Map.new(true => 'Y', false => 'N')
263
+ when %w[y n]
264
+ Types::Map.new(true => 'y', false => 'n')
265
+ when %w[ENABLED DISABLED]
266
+ Types::Map.new(true => 'ENABLED', false => 'DISABLED')
267
+ when %w[Enabled Disabled]
268
+ Types::Map.new(true => 'Enabled', false => 'Disabled')
269
+ when %w[enabled disabled]
270
+ Types::Map.new(true => 'enabled', false => 'disabled')
271
+ else
272
+ Types::Enum.new(literal_values.map(&:to_sym))
273
+ end
274
+
275
+ keywords[:value][:type] = type
276
+ elsif value_node[:name]
277
+ case value_node[:name]
278
+ when 'NUM'
279
+ keywords[:value][:type] = Types::Num.new
280
+ end
281
+ end
282
+ end
283
+
284
+ if flag
285
+ @command.option(flag.to_s, **keywords)
286
+ else
287
+ warn "could not detect option flag: #{line}"
288
+ end
289
+ end
290
+
291
+ USAGE_PREFIX = /^usage:\s+/i
292
+
293
+ USAGE_LINE = /#{USAGE_PREFIX}[a-z][a-z0-9_-]*/i
294
+
295
+ USAGE_SECTION = /^usage:$/i
296
+
297
+ INDENT = /^\s{2,}/
298
+
299
+ OPTION_LINE = /#{INDENT}-(?:[A-Za-z0-9]|-[A-Za-z0-9])/
300
+
301
+ SUBCOMMAND = /[a-z][a-z0-9]*(?:[_-][a-z0-9]+)*/
302
+
303
+ SUBCOMMAND_LINE = /^\s{2,}(#{SUBCOMMAND})(?:,\s[a-z][a-z0-9_-]*)?(?:\t|\s{2,}|$)/
304
+
305
+ def parse_subcommand_line(line)
306
+ if (match = line.match(SUBCOMMAND))
307
+ subcommand_name = match[0]
308
+
309
+ # filter out self-referetial subcommands
310
+ unless subcommand_name == @command.command_name
311
+ @command.subcommand(subcommand_name)
312
+ end
313
+ end
314
+ end
315
+
316
+ #
317
+ # Parses `--help` output into {#command}.
318
+ #
319
+ # @param [String] output
320
+ # The full `--help` output.
321
+ #
322
+ def parse(output)
323
+ usage_on_next_line = false
324
+
325
+ output.each_line do |line|
326
+ if line =~ USAGE_SECTION
327
+ usage_on_next_line = true
328
+ elsif usage_on_next_line
329
+ if line =~ INDENT
330
+ parse_usage(line.strip)
331
+ else
332
+ usage_on_next_line = false
333
+ end
334
+ else
335
+ if line =~ USAGE_LINE
336
+ usage = line.sub(USAGE_PREFIX,'').chomp
337
+
338
+ parse_usage(usage)
339
+ elsif line =~ OPTION_LINE
340
+ parse_option_line(line.chomp)
341
+ elsif line =~ SUBCOMMAND_LINE
342
+ parse_subcommand_line(line.chomp)
343
+ end
344
+ end
345
+ end
346
+ end
347
+
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,80 @@
1
+ require 'command_mapper/gen/parsers/help'
2
+ require 'command_mapper/gen/exceptions'
3
+
4
+ module CommandMapper
5
+ module Gen
6
+ module Parsers
7
+ class Man < Help
8
+
9
+ #
10
+ # Parses the command's man page.
11
+ #
12
+ # @param [Command] command
13
+ # The command object to parse data into.
14
+ #
15
+ # @return [Command, nil]
16
+ # Returns `nil` if the command could not be found.
17
+ #
18
+ # @raise [CommandNotInstalled]
19
+ # The `man` command was not installed.
20
+ #
21
+ def self.run(command)
22
+ output = begin
23
+ `man #{command.man_page} 2>/dev/null`
24
+ rescue Errno::ENOENT
25
+ raise(CommandNotInstalled,"the 'man' command is not installed")
26
+ end
27
+
28
+ parse(output,command) unless (output.nil? || output.empty?)
29
+ end
30
+
31
+ #
32
+ # Parses a command synopsis line.
33
+ #
34
+ # @param [String] line
35
+ # The command string.
36
+ #
37
+ def parse_synopsis(line)
38
+ parse_usage(line.strip)
39
+ end
40
+
41
+ SECTION_REGEXP = /^[A-Z ]+$/
42
+
43
+ INDENT = ' '
44
+
45
+ OPTION_LINE = /^#{INDENT}-(?:[A-Za-z0-9]|-[A-Za-z0-9])/
46
+
47
+ #
48
+ # Parses the man page output into {#command}.
49
+ #
50
+ # @param [String] output
51
+ # The plain-text man page output to parse.
52
+ #
53
+ def parse(output)
54
+ section = nil
55
+
56
+ output.each_line do |line|
57
+ line.chomp!
58
+
59
+ if line =~ SECTION_REGEXP
60
+ section = line
61
+ else
62
+ case section
63
+ when 'SYNOPSIS'
64
+ # SYNPSIS lines are indented
65
+ if line.start_with?(INDENT)
66
+ parse_synopsis(line.chomp)
67
+ end
68
+ when 'DESCRIPTION', 'OPTIONS'
69
+ if line =~ OPTION_LINE
70
+ parse_option_line(line.chomp)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,127 @@
1
+ require 'command_mapper/gen/parsers/common'
2
+
3
+ module CommandMapper
4
+ module Gen
5
+ module Parsers
6
+ class Options < Common
7
+
8
+ rule(:name) do
9
+ camelcase_name | lowercase_name | capitalized_name | uppercase_name
10
+ end
11
+
12
+ rule(:literal_values) do
13
+ (
14
+ name.as(:string) >> (
15
+ (str('|') >> name.as(:string)).repeat(1) |
16
+ (str(',') >> name.as(:string)).repeat(1)
17
+ )
18
+ ).as(:literal_values)
19
+ end
20
+
21
+ rule(:list) do
22
+ (
23
+ name.as(:name) >> (
24
+ (str(',').as(:separator) >> ellipsis) |
25
+ (str('[') >> str(',').as(:separator) >> ellipsis >> str(']'))
26
+ )
27
+ ).as(:list)
28
+ end
29
+
30
+ rule(:key_value) do
31
+ (
32
+ name >>
33
+ match[':='].as(:separator) >>
34
+ (name | ellipsis)
35
+ ).as(:key_value)
36
+ end
37
+
38
+ rule(:value) do
39
+ (
40
+ # "FOO,..."
41
+ list |
42
+ # "KEY:VALUE" or "KEY=VALUE"
43
+ key_value |
44
+ # "FOO|..." or "foo|..."
45
+ literal_values |
46
+ # "FOO" or "foo"
47
+ name.as(:name)
48
+ ).as(:value)
49
+ end
50
+
51
+ rule(:angle_brackets) do
52
+ str('<') >> space? >> value >> space? >> str('>')
53
+ end
54
+
55
+ rule(:curly_braces) do
56
+ str('{') >> space? >> value >> space? >> str('}')
57
+ end
58
+
59
+ rule(:square_brackets) do
60
+ (
61
+ str('[') >> space? >>
62
+ value >>
63
+ space? >> str(']')
64
+ ).as(:optional)
65
+ end
66
+
67
+ rule(:value_container) do
68
+ # "{...}"
69
+ curly_braces |
70
+ # "<...>"
71
+ angle_brackets |
72
+ # "[...]"
73
+ square_brackets |
74
+ # "..."
75
+ value
76
+ end
77
+
78
+ rule(:option_value) do
79
+ # " VALUE"
80
+ (space >> value_container) |
81
+ # "=VALUE"
82
+ (str('=').as(:equals) >> value_container) |
83
+ (
84
+ str('[') >> (
85
+ # "[=VALUE]"
86
+ (str('=').as(:equals) >> value_container) |
87
+ # "[VALUE]"
88
+ value_container
89
+ ) >> str(']')
90
+ ).as(:optional)
91
+ end
92
+
93
+ rule(:long_option) do
94
+ # "--option" or "--option VALUE" or "--option=VALUE"
95
+ long_flag.as(:long_flag) >> option_value.maybe
96
+ end
97
+
98
+ rule(:option_separator) { str(', ') }
99
+
100
+ rule(:short_and_long_option) do
101
+ # "-o, --option" or "-o, --option VALUE" or "-o, --option=VALUE"
102
+ short_flag.as(:short_flag) >>
103
+ option_separator >> long_flag.as(:long_flag) >>
104
+ (option_separator >> long_flag).repeat(0) >>
105
+ option_value.maybe
106
+ end
107
+
108
+ rule(:short_option) do
109
+ # "-o" or "-o VALUE" or "-o=VALUE"
110
+ short_flag.as(:short_flag) >> option_value.maybe
111
+ end
112
+
113
+ rule(:option) { long_option | short_and_long_option | short_option }
114
+
115
+ rule(:option_summary) { any.repeat(1) }
116
+
117
+ rule(:option_line) do
118
+ (str("\t") | spaces) >> option >> str(',').maybe >>
119
+ (match[' \t'].repeat(1) >> option_summary).maybe >> any.absent?
120
+ end
121
+
122
+ root :option_line
123
+
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,141 @@
1
+ require 'command_mapper/gen/parsers/common'
2
+
3
+ module CommandMapper
4
+ module Gen
5
+ module Parsers
6
+ class Usage < Common
7
+
8
+ rule(:capitalized_word) { match['A-Z'] >> match['a-z'].repeat(1) }
9
+ rule(:lowercase_word) { match['a-z'].repeat(1) }
10
+ rule(:word) { capitalized_word | lowercase_word }
11
+ rule(:words) do
12
+ (word >> (space >> word).repeat(1)).as(:words) >> ellipsis?
13
+ end
14
+
15
+ rule(:ignored_argument_names) do
16
+ str("OPTIONS") | str('OPTS') | str("options") | str('opts')
17
+ end
18
+
19
+ rule(:argument_name) do
20
+ (
21
+ (
22
+ (uppercase_name | capitalized_name | lowercase_name) >>
23
+ (space >> ignored_argument_names).maybe
24
+ ).as(:name) >> ellipsis?
25
+ ).as(:argument)
26
+ end
27
+
28
+ rule(:short_flags) do
29
+ (str('-') >> match['a-zA-Z0-9#'].repeat(2)).as(:short_flags)
30
+ end
31
+
32
+ rule(:flag) do
33
+ (long_flag.as(:long_flag) | short_flags | short_flag.as(:short_flag))
34
+ end
35
+
36
+ rule(:option_value_string) do
37
+ match['a-z0-9_-'].repeat(1).as(:string)
38
+ end
39
+
40
+ rule(:option_value_strings) do
41
+ option_value_string >> (
42
+ (str(',') >> option_value_string).repeat(1) |
43
+ (str('|') >> option_value_string).repeat(1)
44
+ )
45
+ end
46
+
47
+ rule(:option_value_name) do
48
+ (camelcase_name | lowercase_name | capitalized_name).as(:name)
49
+ end
50
+
51
+ rule(:option_value) { option_value_strings | option_value_name }
52
+
53
+ rule(:optional_option_value) do
54
+ str('[') >> space? >>
55
+ option_value.as(:optional) >>
56
+ space? >> str(']')
57
+ end
58
+
59
+ rule(:option_value_container) do
60
+ (str('{') >> space? >> option_value >> space? >> str('}')) |
61
+ (str('<') >> space? >> option_value >> space? >> str('>')) |
62
+ optional_option_value |
63
+ option_value
64
+ end
65
+
66
+ rule(:option) do
67
+ (
68
+ flag >> (
69
+ (space >> option_value_container) |
70
+ (str('=').as(:equals) >> option_value_container) |
71
+ (
72
+ str('[') >> (
73
+ (str('=').as(:equals) >> option_value_container) |
74
+ option_value_container
75
+ ) >> str(']')
76
+ ).as(:optional)
77
+ ).as(:value).maybe
78
+ ).as(:option)
79
+ end
80
+
81
+ rule(:angle_brackets_group) do
82
+ str('<') >> space? >>
83
+ (words.as(:argument) | args) >>
84
+ space? >> str('>')
85
+ end
86
+
87
+ rule(:curly_braces_group) do
88
+ str('{') >> space? >>
89
+ (words.as(:argument) | args) >>
90
+ space? >> str('}')
91
+ end
92
+
93
+ rule(:optional_group) do
94
+ (
95
+ str('[') >> space? >>
96
+ args >>
97
+ space? >> str(']') >> ellipsis?
98
+ ).as(:optional)
99
+ end
100
+
101
+ rule(:dash) { match['-'].repeat(1,2) }
102
+
103
+ rule(:arg) do
104
+ (
105
+ # "-o" or "--opt"
106
+ option |
107
+ # "FOO" or "foo" or "foo-bar" or "foo_bar"
108
+ argument_name |
109
+ # "<...>"
110
+ angle_brackets_group |
111
+ # "{...}"
112
+ curly_braces_group |
113
+ # "[...]"
114
+ optional_group |
115
+ # "..."
116
+ ellipsis |
117
+ # "--" or "-"
118
+ dash
119
+ )
120
+ end
121
+
122
+ rule(:arg_separator) do
123
+ str('|') | (space >> (str('|') >> space).maybe)
124
+ end
125
+ rule(:args) { arg >> ( arg_separator >> arg).repeat(0) }
126
+
127
+ rule(:subcommand_name) { match['a-z'] >> match['a-z0-9_-'].repeat(0) }
128
+ rule(:command_name) { match['a-zA-Z'] >> match['a-z0-9_-'].repeat(0) }
129
+
130
+ rule(:usage) do
131
+ command_name.as(:command_name) >>
132
+ (space >> subcommand_name.as(:subcommand_name)).maybe >>
133
+ (space >> args.as(:arguments)).maybe
134
+ end
135
+
136
+ root :usage
137
+
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,2 @@
1
+ require 'command_mapper/gen/parsers/help'
2
+ require 'command_mapper/gen/parsers/man'