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,233 @@
|
|
1
|
+
require 'command_mapper/gen/parsers'
|
2
|
+
require 'command_mapper/gen/command'
|
3
|
+
require 'command_mapper/gen/version'
|
4
|
+
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
module CommandMapper
|
8
|
+
module Gen
|
9
|
+
class CLI
|
10
|
+
|
11
|
+
PROGRAM_NAME = "command_mapper-gen"
|
12
|
+
|
13
|
+
PARSERS = {
|
14
|
+
'help' => Parsers::Help,
|
15
|
+
'man' => Parsers::Man
|
16
|
+
}
|
17
|
+
|
18
|
+
BUG_REPORT_URL = "https://github.com/postmodern/command_mapper-gen/issues/new"
|
19
|
+
|
20
|
+
# The output file or `nil` for stdout.
|
21
|
+
#
|
22
|
+
# @return [File, nil]
|
23
|
+
attr_reader :output
|
24
|
+
|
25
|
+
# The parsers to run.
|
26
|
+
#
|
27
|
+
# @return [Array<Parsers::Help, Parsers::Man>]
|
28
|
+
attr_reader :parsers
|
29
|
+
|
30
|
+
# Specifies whether debug output should be printed.
|
31
|
+
#
|
32
|
+
# @return [Boolean, nil]
|
33
|
+
attr_reader :debug
|
34
|
+
|
35
|
+
# The parsed command.
|
36
|
+
#
|
37
|
+
# @return [Command]
|
38
|
+
attr_reader :command
|
39
|
+
|
40
|
+
# The command's option parser.
|
41
|
+
#
|
42
|
+
# @return [OptionParser]
|
43
|
+
attr_reader :option_parser
|
44
|
+
|
45
|
+
#
|
46
|
+
# Initializes the command.
|
47
|
+
#
|
48
|
+
def initialize
|
49
|
+
@output = nil
|
50
|
+
@parsers = PARSERS.values
|
51
|
+
@debug = false
|
52
|
+
@command = nil
|
53
|
+
|
54
|
+
@option_parser = option_parser
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Initializes and runs the command.
|
59
|
+
#
|
60
|
+
# @param [Array<String>] argv
|
61
|
+
# Command-line arguments.
|
62
|
+
#
|
63
|
+
# @return [Integer]
|
64
|
+
# The exit status of the command.
|
65
|
+
#
|
66
|
+
def self.run(argv=ARGV)
|
67
|
+
new().run(argv)
|
68
|
+
rescue Interrupt
|
69
|
+
# https://tldp.org/LDP/abs/html/exitcodes.html
|
70
|
+
return 130
|
71
|
+
rescue Errno::EPIPE
|
72
|
+
# STDOUT pipe broken
|
73
|
+
return 0
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Runs the command.
|
78
|
+
#
|
79
|
+
# @param [Array<String>] argv
|
80
|
+
# Command-line arguments.
|
81
|
+
#
|
82
|
+
# @return [Integer]
|
83
|
+
# The exit status of the command.
|
84
|
+
#
|
85
|
+
def run(argv=ARGV)
|
86
|
+
argv = begin
|
87
|
+
@option_parser.parse(argv)
|
88
|
+
rescue OptionParser::ParseError => error
|
89
|
+
print_error(error.message)
|
90
|
+
return -1
|
91
|
+
end
|
92
|
+
|
93
|
+
if argv.empty?
|
94
|
+
print_error "expects a COMMAND_NAME"
|
95
|
+
return -1
|
96
|
+
end
|
97
|
+
|
98
|
+
begin
|
99
|
+
@command = Command.new(argv.first)
|
100
|
+
|
101
|
+
@parsers.each do |parser|
|
102
|
+
parse_command = ->(command) {
|
103
|
+
parser.run(command) do |line,parse_error|
|
104
|
+
print_parser_error(command,line,parse_error)
|
105
|
+
end
|
106
|
+
|
107
|
+
command.subcommands.each_value do |subcommand|
|
108
|
+
parse_command.call(subcommand)
|
109
|
+
end
|
110
|
+
}
|
111
|
+
|
112
|
+
parse_command.call(@command)
|
113
|
+
end
|
114
|
+
rescue Error => error
|
115
|
+
print_error(error.message)
|
116
|
+
return -1
|
117
|
+
end
|
118
|
+
|
119
|
+
if (@command.options.empty? &&
|
120
|
+
@command.arguments.empty? &&
|
121
|
+
@command.subcommands.empty?)
|
122
|
+
print_error "no options or arguments detected"
|
123
|
+
return -2
|
124
|
+
end
|
125
|
+
|
126
|
+
if @output then @command.save(@output)
|
127
|
+
else puts command.to_ruby
|
128
|
+
end
|
129
|
+
|
130
|
+
return 0
|
131
|
+
rescue => error
|
132
|
+
print_backtrace(error)
|
133
|
+
return -1
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# The option parser.
|
138
|
+
#
|
139
|
+
# @return [OptionParser]
|
140
|
+
#
|
141
|
+
def option_parser
|
142
|
+
OptionParser.new do |opts|
|
143
|
+
opts.banner = "usage: #{PROGRAM_NAME} [options] COMMAND_NAME"
|
144
|
+
|
145
|
+
opts.separator ""
|
146
|
+
opts.separator "Options:"
|
147
|
+
|
148
|
+
opts.on('-o','--output FILE','Saves the output to FILE') do |file|
|
149
|
+
@output = file
|
150
|
+
end
|
151
|
+
|
152
|
+
opts.on('-p','--parser=PARSER', PARSERS, 'Selects which parser to use (help or man)') do |parser|
|
153
|
+
@parsers = [parser]
|
154
|
+
end
|
155
|
+
|
156
|
+
opts.on('-d','--debug','Enables debugging output') do
|
157
|
+
@debug = true
|
158
|
+
end
|
159
|
+
|
160
|
+
opts.on('-V','--version','Print the version') do
|
161
|
+
puts "#{PROGRAM_NAME} #{VERSION}"
|
162
|
+
exit
|
163
|
+
end
|
164
|
+
|
165
|
+
opts.on('-h','--help','Print the help output') do
|
166
|
+
puts opts
|
167
|
+
exit
|
168
|
+
end
|
169
|
+
|
170
|
+
opts.separator ""
|
171
|
+
opts.separator "Examples:"
|
172
|
+
opts.separator " #{PROGRAM_NAME} grep"
|
173
|
+
opts.separator ""
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
#
|
178
|
+
# Prints an error message to stderr.
|
179
|
+
#
|
180
|
+
# @param [String] error
|
181
|
+
# The error message.
|
182
|
+
#
|
183
|
+
def print_error(error)
|
184
|
+
$stderr.puts "#{PROGRAM_NAME}: #{error}"
|
185
|
+
end
|
186
|
+
|
187
|
+
#
|
188
|
+
# Prints a parsing error to stderr.
|
189
|
+
#
|
190
|
+
# @param [Command] command
|
191
|
+
# The command that was being populated.
|
192
|
+
#
|
193
|
+
# @param [String] string
|
194
|
+
# The text that could not be parsed.
|
195
|
+
#
|
196
|
+
# @param [Parslet::ParseError] error
|
197
|
+
# The parsing error.
|
198
|
+
#
|
199
|
+
def print_parser_error(command,string,error)
|
200
|
+
$stderr.puts "Failed to parse line in `#{command.command_string} --help`:"
|
201
|
+
$stderr.puts ""
|
202
|
+
$stderr.puts " #{string}"
|
203
|
+
$stderr.puts
|
204
|
+
|
205
|
+
if @debug
|
206
|
+
error.parse_failure_cause.ascii_tree.each_line do |backtrace_line|
|
207
|
+
$stderr.puts " #{backtrace_line}"
|
208
|
+
end
|
209
|
+
else
|
210
|
+
$stderr.puts error.message
|
211
|
+
end
|
212
|
+
|
213
|
+
$stderr.puts ""
|
214
|
+
end
|
215
|
+
|
216
|
+
#
|
217
|
+
# Prints a backtrace to stderr.
|
218
|
+
#
|
219
|
+
# @param [Exception] exception
|
220
|
+
# The exception.
|
221
|
+
#
|
222
|
+
def print_backtrace(exception)
|
223
|
+
$stderr.puts "Oops! Looks like you've found a bug!"
|
224
|
+
$stderr.puts "Please report the following to: #{BUG_REPORT_URL}"
|
225
|
+
$stderr.puts
|
226
|
+
$stderr.puts "```"
|
227
|
+
$stderr.puts "#{exception.full_message}"
|
228
|
+
$stderr.puts "```"
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'command_mapper/gen/option'
|
2
|
+
require 'command_mapper/gen/argument'
|
3
|
+
require 'command_mapper/gen/types'
|
4
|
+
|
5
|
+
module CommandMapper
|
6
|
+
module Gen
|
7
|
+
#
|
8
|
+
# Represents a mock `CommandMapper::Command` class that will be populated
|
9
|
+
# by the {Parsers} and written out to a file.
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
#
|
13
|
+
class Command
|
14
|
+
|
15
|
+
# The command's name.
|
16
|
+
#
|
17
|
+
# @return [String]
|
18
|
+
attr_accessor :command_name
|
19
|
+
|
20
|
+
# The parent command of this sub-command.
|
21
|
+
#
|
22
|
+
# @return [Command, nil]
|
23
|
+
attr_reader :parent_command
|
24
|
+
|
25
|
+
# @return [Hash{String => Option}]
|
26
|
+
attr_reader :options
|
27
|
+
|
28
|
+
# @return [Hash{Symbol => Argument}]
|
29
|
+
attr_reader :arguments
|
30
|
+
|
31
|
+
# @return [Hash{String => Command}]
|
32
|
+
attr_reader :subcommands
|
33
|
+
|
34
|
+
#
|
35
|
+
# Initializes the parsed command.
|
36
|
+
#
|
37
|
+
# @param [String] command_name
|
38
|
+
# The command name or path to the command.
|
39
|
+
#
|
40
|
+
def initialize(command_name,parent_command=nil)
|
41
|
+
@command_name = command_name
|
42
|
+
@parent_command = parent_command
|
43
|
+
|
44
|
+
@options = {}
|
45
|
+
@arguments = {}
|
46
|
+
@subcommands = {}
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# The command string to run the command.
|
51
|
+
#
|
52
|
+
# @return [String]
|
53
|
+
#
|
54
|
+
def command_string
|
55
|
+
if @parent_command
|
56
|
+
"#{@parent_command.command_string} #{@command_name}"
|
57
|
+
else
|
58
|
+
@command_name
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# The man-page name for the command.
|
64
|
+
#
|
65
|
+
# @return [String]
|
66
|
+
#
|
67
|
+
def man_page
|
68
|
+
if @parent_command
|
69
|
+
"#{@parent_command.man_page}-#{@command_name}"
|
70
|
+
else
|
71
|
+
@command_name
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Defines an option for the command.
|
77
|
+
#
|
78
|
+
# @param [String] flag
|
79
|
+
#
|
80
|
+
# @param [Hash{Symbol => Object}] kwargs
|
81
|
+
#
|
82
|
+
def option(flag,**kwargs)
|
83
|
+
@options[flag] = Option.new(flag,**kwargs)
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Defines an argument for the command.
|
88
|
+
#
|
89
|
+
# @param [Symbol] name
|
90
|
+
#
|
91
|
+
# @param [Hash{Symbol => Object}] kwargs
|
92
|
+
#
|
93
|
+
def argument(name,**kwargs)
|
94
|
+
@arguments[name] = Argument.new(name,**kwargs)
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# Defines a new sub-command.
|
99
|
+
#
|
100
|
+
# @param [String] name
|
101
|
+
# The subcommand name.
|
102
|
+
#
|
103
|
+
# @return [Command]
|
104
|
+
# The newly defined subcommand.
|
105
|
+
#
|
106
|
+
def subcommand(name)
|
107
|
+
@subcommands[name] = Command.new(name,self)
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# The CamelCase class name derived from the {#command_name}.
|
112
|
+
#
|
113
|
+
# @return [String, nil]
|
114
|
+
# The class name or `nil` if {#command_name} is also `nil`.
|
115
|
+
#
|
116
|
+
def class_name
|
117
|
+
@command_name.split(/[_-]+/).map(&:capitalize).join
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Converts the parsed command to Ruby source code.
|
122
|
+
#
|
123
|
+
# @return [String]
|
124
|
+
# The generated ruby source code for the command.
|
125
|
+
#
|
126
|
+
def to_ruby
|
127
|
+
lines = []
|
128
|
+
|
129
|
+
if @parent_command.nil?
|
130
|
+
lines << "require 'command_mapper/command'"
|
131
|
+
lines << ""
|
132
|
+
lines << "#"
|
133
|
+
lines << "# Represents the `#{@command_name}` command"
|
134
|
+
lines << "#"
|
135
|
+
|
136
|
+
lines << "class #{class_name} < CommandMapper::Command"
|
137
|
+
lines << ""
|
138
|
+
lines << " command #{@command_name.inspect} do"
|
139
|
+
|
140
|
+
indent = " "
|
141
|
+
else
|
142
|
+
lines << "subcommand #{@command_name.inspect} do"
|
143
|
+
|
144
|
+
indent = " "
|
145
|
+
end
|
146
|
+
|
147
|
+
unless @options.empty?
|
148
|
+
@options.each_value do |option|
|
149
|
+
lines << "#{indent}#{option.to_ruby}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
if (!@options.empty? && !@arguments.empty?)
|
154
|
+
lines << ''
|
155
|
+
end
|
156
|
+
|
157
|
+
unless @arguments.empty?
|
158
|
+
@arguments.each_value do |argument|
|
159
|
+
lines << "#{indent}#{argument.to_ruby}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
unless @subcommands.empty?
|
164
|
+
if (!@options.empty? || !@arguments.empty?)
|
165
|
+
lines << ''
|
166
|
+
end
|
167
|
+
|
168
|
+
@subcommands.each_value.each_with_index do |subcommand,index|
|
169
|
+
lines << '' if index > 0
|
170
|
+
|
171
|
+
subcommand.to_ruby.each_line do |line|
|
172
|
+
if line == $/
|
173
|
+
lines << ''
|
174
|
+
else
|
175
|
+
lines << "#{indent}#{line.chomp}"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
if @parent_command.nil?
|
182
|
+
lines << " end"
|
183
|
+
lines << ''
|
184
|
+
end
|
185
|
+
|
186
|
+
lines << "end"
|
187
|
+
|
188
|
+
return lines.join($/) + $/
|
189
|
+
end
|
190
|
+
|
191
|
+
#
|
192
|
+
# Saves the parsed command to the given file path.
|
193
|
+
#
|
194
|
+
# @param [String] path
|
195
|
+
#
|
196
|
+
def save(path)
|
197
|
+
File.write(path,to_ruby)
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'command_mapper/gen/option_value'
|
2
|
+
|
3
|
+
module CommandMapper
|
4
|
+
module Gen
|
5
|
+
#
|
6
|
+
# Represents a mock `CommandMapper::Option` class.
|
7
|
+
#
|
8
|
+
class Option
|
9
|
+
|
10
|
+
# The option flag for the option.
|
11
|
+
#
|
12
|
+
# @return [String]
|
13
|
+
attr_reader :flag
|
14
|
+
|
15
|
+
# @return [Boolean, :equals, nil]
|
16
|
+
attr_reader :equals
|
17
|
+
|
18
|
+
# @return [Boolean, nil]
|
19
|
+
attr_reader :repeats
|
20
|
+
|
21
|
+
# @return [OptionValue, nil]
|
22
|
+
attr_reader :value
|
23
|
+
|
24
|
+
#
|
25
|
+
# Initializes the parsed argument.
|
26
|
+
#
|
27
|
+
# @param [String] flag
|
28
|
+
# The option flag.
|
29
|
+
#
|
30
|
+
# @param [Boolean, :optional, nil] equals
|
31
|
+
#
|
32
|
+
# @param [Boolean, nil] repeats
|
33
|
+
#
|
34
|
+
# @param [Hash{Symbol => Object}, nil] value
|
35
|
+
#
|
36
|
+
def initialize(flag, equals: nil, repeats: nil, value: nil)
|
37
|
+
@flag = flag
|
38
|
+
@equals = equals
|
39
|
+
@value = OptionValue.new(**value) if value
|
40
|
+
@repeats = repeats
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Converts the parsed option to Ruby source code.
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
#
|
48
|
+
def to_ruby
|
49
|
+
ruby = "option #{@flag.inspect}"
|
50
|
+
fixme = nil
|
51
|
+
|
52
|
+
if @flag =~ /^-[a-zA-Z0-9]/ && @flag.length <= 3
|
53
|
+
ruby << ", name: "
|
54
|
+
fixme = "name"
|
55
|
+
end
|
56
|
+
|
57
|
+
ruby << ", equals: #{@equals.inspect}" unless @equals.nil?
|
58
|
+
ruby << ", repeats: #{@repeats.inspect}" unless @repeats.nil?
|
59
|
+
ruby << ", value: #{@value.to_ruby}" if @value
|
60
|
+
ruby << "\t# FIXME: #{fixme}" if fixme
|
61
|
+
ruby
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'command_mapper/gen/arg'
|
2
|
+
require 'command_mapper/gen/types/str'
|
3
|
+
|
4
|
+
module CommandMapper
|
5
|
+
module Gen
|
6
|
+
class OptionValue < Arg
|
7
|
+
|
8
|
+
#
|
9
|
+
# Converts the parsed option to Ruby source code.
|
10
|
+
#
|
11
|
+
# @return [String]
|
12
|
+
#
|
13
|
+
def to_ruby
|
14
|
+
ruby = super()
|
15
|
+
|
16
|
+
if ruby.empty? then "true"
|
17
|
+
else "{#{ruby}}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module CommandMapper
|
4
|
+
module Gen
|
5
|
+
module Parsers
|
6
|
+
class Common < Parslet::Parser
|
7
|
+
|
8
|
+
rule(:space) { match[' '] }
|
9
|
+
rule(:spaces) { space.repeat(1) }
|
10
|
+
rule(:space?) { space.maybe }
|
11
|
+
|
12
|
+
rule(:ellipsis) { str('...') }
|
13
|
+
rule(:ellipsis?) { (space? >> ellipsis.as(:repeats)).maybe }
|
14
|
+
|
15
|
+
rule(:lowercase_name) do
|
16
|
+
match['a-z'] >> match['a-z0-9'].repeat(0) >> (
|
17
|
+
match['_-'] >> match['a-z0-9'].repeat(1)
|
18
|
+
).repeat(0)
|
19
|
+
end
|
20
|
+
|
21
|
+
rule(:uppercase_name) do
|
22
|
+
match['A-Z'] >> match['A-Z0-9'].repeat(0) >> (
|
23
|
+
match['_-'] >> match['A-Z0-9'].repeat(1)
|
24
|
+
).repeat(0)
|
25
|
+
end
|
26
|
+
|
27
|
+
rule(:camelcase_name) do
|
28
|
+
match['a-z'] >> match['a-z0-9'].repeat(0) >> (
|
29
|
+
match['A-Z'] >> match['a-z0-9'].repeat(1)
|
30
|
+
).repeat(1)
|
31
|
+
end
|
32
|
+
|
33
|
+
rule(:capitalized_name) do
|
34
|
+
match['A-Z'] >> match['a-z0-9'].repeat(1) >> (
|
35
|
+
match['_-'] >> match['a-z0-9'].repeat(1)
|
36
|
+
).repeat(0)
|
37
|
+
end
|
38
|
+
|
39
|
+
rule(:short_flag) { str('-') >> match['a-zA-Z0-9#'] }
|
40
|
+
rule(:long_flag) do
|
41
|
+
str('--') >> match['a-zA-Z'] >> match['a-zA-Z0-9'].repeat(1) >> (
|
42
|
+
match['_-'] >> match['a-zA-Z0-9'].repeat(1)
|
43
|
+
).repeat(0)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|