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.
- checksums.yaml +7 -0
- data/.github/workflows/ruby.yml +27 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +20 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +20 -0
- data/README.md +145 -0
- data/Rakefile +15 -0
- data/bin/command_mapper-gen +7 -0
- data/commnad_mapper-gen.gemspec +61 -0
- data/examples/grep.rb +63 -0
- data/gemspec.yml +26 -0
- data/lib/command_mapper/gen/arg.rb +43 -0
- data/lib/command_mapper/gen/argument.rb +53 -0
- data/lib/command_mapper/gen/cli.rb +233 -0
- data/lib/command_mapper/gen/command.rb +202 -0
- data/lib/command_mapper/gen/exceptions.rb +9 -0
- data/lib/command_mapper/gen/option.rb +66 -0
- data/lib/command_mapper/gen/option_value.rb +23 -0
- data/lib/command_mapper/gen/parsers/common.rb +49 -0
- data/lib/command_mapper/gen/parsers/help.rb +351 -0
- data/lib/command_mapper/gen/parsers/man.rb +80 -0
- data/lib/command_mapper/gen/parsers/options.rb +127 -0
- data/lib/command_mapper/gen/parsers/usage.rb +141 -0
- data/lib/command_mapper/gen/parsers.rb +2 -0
- data/lib/command_mapper/gen/task.rb +90 -0
- data/lib/command_mapper/gen/types/enum.rb +30 -0
- data/lib/command_mapper/gen/types/key_value.rb +34 -0
- data/lib/command_mapper/gen/types/list.rb +34 -0
- data/lib/command_mapper/gen/types/map.rb +36 -0
- data/lib/command_mapper/gen/types/num.rb +18 -0
- data/lib/command_mapper/gen/types/str.rb +48 -0
- data/lib/command_mapper/gen/types.rb +6 -0
- data/lib/command_mapper/gen/version.rb +6 -0
- data/lib/command_mapper/gen.rb +2 -0
- data/spec/argument_spec.rb +92 -0
- data/spec/cli_spec.rb +269 -0
- data/spec/command_spec.rb +316 -0
- data/spec/option_spec.rb +85 -0
- data/spec/option_value_spec.rb +20 -0
- data/spec/parsers/common_spec.rb +616 -0
- data/spec/parsers/help_spec.rb +612 -0
- data/spec/parsers/man_spec.rb +158 -0
- data/spec/parsers/options_spec.rb +802 -0
- data/spec/parsers/usage_spec.rb +1175 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/task_spec.rb +69 -0
- data/spec/types/enum_spec.rb +45 -0
- data/spec/types/key_value_spec.rb +36 -0
- data/spec/types/list_spec.rb +36 -0
- data/spec/types/map_spec.rb +48 -0
- data/spec/types/str_spec.rb +70 -0
- 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
|