shellopts 2.0.0.pre.14 → 2.0.0
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 +4 -4
- data/.gitignore +2 -1
- data/.ruby-version +1 -1
- data/README.md +201 -267
- data/TODO +37 -5
- data/doc/format.rb +95 -0
- data/doc/grammar.txt +27 -0
- data/doc/syntax.rb +110 -0
- data/doc/syntax.txt +10 -0
- data/lib/ext/array.rb +62 -0
- data/lib/ext/forward_to.rb +15 -0
- data/lib/ext/lcs.rb +34 -0
- data/lib/shellopts/analyzer.rb +130 -0
- data/lib/shellopts/ansi.rb +8 -0
- data/lib/shellopts/args.rb +25 -15
- data/lib/shellopts/argument_type.rb +139 -0
- data/lib/shellopts/dump.rb +158 -0
- data/lib/shellopts/formatter.rb +292 -92
- data/lib/shellopts/grammar.rb +375 -0
- data/lib/shellopts/interpreter.rb +103 -0
- data/lib/shellopts/lexer.rb +175 -0
- data/lib/shellopts/parser.rb +293 -0
- data/lib/shellopts/program.rb +279 -0
- data/lib/shellopts/renderer.rb +227 -0
- data/lib/shellopts/stack.rb +7 -0
- data/lib/shellopts/token.rb +44 -0
- data/lib/shellopts/version.rb +1 -1
- data/lib/shellopts.rb +359 -3
- data/main +1180 -0
- data/shellopts.gemspec +8 -14
- metadata +86 -41
- data/lib/ext/algorithm.rb +0 -14
- data/lib/ext/ruby_env.rb +0 -8
- data/lib/shellopts/ast/command.rb +0 -112
- data/lib/shellopts/ast/dump.rb +0 -28
- data/lib/shellopts/ast/option.rb +0 -15
- data/lib/shellopts/ast/parser.rb +0 -106
- data/lib/shellopts/constants.rb +0 -88
- data/lib/shellopts/exceptions.rb +0 -21
- data/lib/shellopts/grammar/analyzer.rb +0 -76
- data/lib/shellopts/grammar/command.rb +0 -87
- data/lib/shellopts/grammar/dump.rb +0 -56
- data/lib/shellopts/grammar/lexer.rb +0 -56
- data/lib/shellopts/grammar/option.rb +0 -55
- data/lib/shellopts/grammar/parser.rb +0 -78
@@ -0,0 +1,293 @@
|
|
1
|
+
|
2
|
+
module ShellOpts
|
3
|
+
module Grammar
|
4
|
+
class Node
|
5
|
+
def parse() end
|
6
|
+
|
7
|
+
def self.parse(parent, token)
|
8
|
+
this = self.new(parent, token)
|
9
|
+
this.parse
|
10
|
+
this
|
11
|
+
end
|
12
|
+
|
13
|
+
def parser_error(token, message) raise ParserError, "#{token.pos} #{message}" end
|
14
|
+
end
|
15
|
+
|
16
|
+
class IdrNode
|
17
|
+
# Assumes that @name and @path has been defined
|
18
|
+
def parse
|
19
|
+
@ident = @path.last || :!
|
20
|
+
@attr = ::ShellOpts::Command::RESERVED_OPTION_NAMES.include?(ident.to_s) ? nil : ident
|
21
|
+
@uid = parent && @path.join(".").sub(/!\./, ".") # uid is nil for the Program object
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Option
|
26
|
+
SHORT_NAME_RE = /[a-zA-Z0-9]/
|
27
|
+
LONG_NAME_RE = /[a-zA-Z0-9][a-zA-Z0-9_-]*/
|
28
|
+
NAME_RE = /(?:#{SHORT_NAME_RE}|#{LONG_NAME_RE})(?:,#{LONG_NAME_RE})*/
|
29
|
+
|
30
|
+
def parse
|
31
|
+
token.source =~ /^(-|--|\+|\+\+)(#{NAME_RE})(?:=(.+?)(\?)?)?$/ or
|
32
|
+
parser_error token, "Illegal option: #{token.source.inspect}"
|
33
|
+
initial = $1
|
34
|
+
name_list = $2
|
35
|
+
arg = $3
|
36
|
+
optional = $4
|
37
|
+
|
38
|
+
@repeatable = %w(+ ++).include?(initial)
|
39
|
+
|
40
|
+
@short_idents = []
|
41
|
+
@short_names = []
|
42
|
+
names = name_list.split(",")
|
43
|
+
if %w(+ -).include?(initial)
|
44
|
+
while names.first&.size == 1
|
45
|
+
name = names.shift
|
46
|
+
@short_names << "-#{name}"
|
47
|
+
@short_idents << name.to_sym
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
names.each { |name|
|
52
|
+
name.size > 1 or
|
53
|
+
parser_error token, "Long names should be at least two characters long: '#{name}'"
|
54
|
+
}
|
55
|
+
|
56
|
+
@long_names = names.map { |name| "--#{name}" }
|
57
|
+
@long_idents = names.map { |name| name.tr("-", "_").to_sym }
|
58
|
+
|
59
|
+
@name = @long_names.first || @short_names.first
|
60
|
+
@path = command.path + [@long_idents.first || @short_idents.first]
|
61
|
+
|
62
|
+
@argument = !arg.nil?
|
63
|
+
|
64
|
+
named = true
|
65
|
+
if @argument
|
66
|
+
if arg =~ /^([^:]+)(?::(.*))/
|
67
|
+
@argument_name = $1
|
68
|
+
named = true
|
69
|
+
arg = $2
|
70
|
+
elsif arg =~ /^:(.*)/
|
71
|
+
arg = $1
|
72
|
+
named = false
|
73
|
+
end
|
74
|
+
|
75
|
+
case arg
|
76
|
+
when "", nil
|
77
|
+
@argument_name ||= "VAL"
|
78
|
+
@argument_type = StringType.new
|
79
|
+
when "#"
|
80
|
+
@argument_name ||= "INT"
|
81
|
+
@argument_type = IntegerArgument.new
|
82
|
+
when "$"
|
83
|
+
@argument_name ||= "NUM"
|
84
|
+
@argument_type = FloatArgument.new
|
85
|
+
when "FILE", "DIR", "PATH", "EFILE", "EDIR", "EPATH", "NFILE", "NDIR", "NPATH"
|
86
|
+
@argument_name ||= arg.sub(/^(?:E|N)/, "")
|
87
|
+
@argument_type = FileArgument.new(arg.downcase.to_sym)
|
88
|
+
when /,/
|
89
|
+
@argument_name ||= arg
|
90
|
+
@argument_type = EnumArgument.new(arg.split(","))
|
91
|
+
else
|
92
|
+
named && @argument_name.nil? or parser_error token, "Illegal type expression: #{arg.inspect}"
|
93
|
+
@argument_name = arg
|
94
|
+
@argument_type = StringType.new
|
95
|
+
end
|
96
|
+
@optional = !optional.nil?
|
97
|
+
else
|
98
|
+
@argument_type = StringType.new
|
99
|
+
end
|
100
|
+
super
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
def basename2ident(s) s.tr("-", "_").to_sym end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Command
|
108
|
+
def parse
|
109
|
+
if parent
|
110
|
+
path_names = token.source.sub("!", "").split(".")
|
111
|
+
@name = path_names.last
|
112
|
+
@path = path_names.map { |cmd| "#{cmd}!".to_sym }
|
113
|
+
else
|
114
|
+
@path = []
|
115
|
+
@name = token.source
|
116
|
+
end
|
117
|
+
super
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Program
|
122
|
+
def self.parse(token)
|
123
|
+
super(nil, token)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class ArgSpec
|
128
|
+
def parse # TODO
|
129
|
+
super
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class Parser
|
135
|
+
using Stack
|
136
|
+
using Ext::Array::ShiftWhile
|
137
|
+
|
138
|
+
# AST root node
|
139
|
+
attr_reader :program
|
140
|
+
|
141
|
+
# Commands by UID
|
142
|
+
attr_reader :commands
|
143
|
+
|
144
|
+
def initialize(tokens)
|
145
|
+
@tokens = tokens.dup # Array of token. Consumed by #parse
|
146
|
+
@nodes = {}
|
147
|
+
end
|
148
|
+
|
149
|
+
def parse()
|
150
|
+
@program = Grammar::Program.parse(@tokens.shift)
|
151
|
+
oneline = @tokens.first.lineno == @tokens.last.lineno
|
152
|
+
nodes = [@program] # Stack of Nodes. Follows the indentation of the source
|
153
|
+
cmds = [@program] # Stack of cmds. Used to keep track of the current command
|
154
|
+
|
155
|
+
while token = @tokens.shift
|
156
|
+
# Unwind stack according to indentation
|
157
|
+
while token.charno <= nodes.top.token.charno
|
158
|
+
node = nodes.pop
|
159
|
+
cmds.pop if cmds.top == node
|
160
|
+
!nodes.empty? or parse_error(token, "Illegal indent")
|
161
|
+
end
|
162
|
+
|
163
|
+
case token.kind
|
164
|
+
when :section
|
165
|
+
Grammar::Section.parse(nodes.top, token)
|
166
|
+
|
167
|
+
when :option
|
168
|
+
# Collect options into option groups if on the same line and not in
|
169
|
+
# oneline mode
|
170
|
+
options = [token] + @tokens.shift_while { |follow|
|
171
|
+
!oneline && follow.kind == :option && follow.lineno == token.lineno
|
172
|
+
}
|
173
|
+
group = Grammar::OptionGroup.new(cmds.top, token)
|
174
|
+
options.each { |option| Grammar::Option.parse(group, option) }
|
175
|
+
nodes.push group
|
176
|
+
|
177
|
+
when :command
|
178
|
+
parent = nil # Required by #indent
|
179
|
+
token.source =~ /^(?:(.*)\.)?([^.]+)$/
|
180
|
+
parent_id = $1
|
181
|
+
ident = $2.to_sym
|
182
|
+
parent_uid = parent_id && parent_id.sub(".", "!.") + "!"
|
183
|
+
|
184
|
+
# Handle dotted command
|
185
|
+
if parent_uid
|
186
|
+
# Clear stack except for the top-level Program object and then
|
187
|
+
# push command objects in the path
|
188
|
+
#
|
189
|
+
# FIXME: Move to analyzer
|
190
|
+
# cmds = cmds[0..0]
|
191
|
+
# for ident in parent_uid.split(".").map(&:to_sym)
|
192
|
+
# cmds.push cmds.top.commands.find { |c| c.ident == ident } or
|
193
|
+
# parse_error token, "Unknown command: #{ident.sub(/!/, "")}"
|
194
|
+
# end
|
195
|
+
# parent = cmds.top
|
196
|
+
parent = cmds.top
|
197
|
+
if !cmds.top.is_a?(Grammar::Program) && token.lineno == cmds.top.token.lineno
|
198
|
+
parent = cmds.pop.parent
|
199
|
+
end
|
200
|
+
|
201
|
+
# Regular command
|
202
|
+
else
|
203
|
+
# Don't nest cmds if they are declared on the same line (as it
|
204
|
+
# often happens with one-line declarations). Program is special
|
205
|
+
# cased as its virtual token is on line 0
|
206
|
+
parent = cmds.top
|
207
|
+
if !cmds.top.is_a?(Grammar::Program) && token.lineno == cmds.top.token.lineno
|
208
|
+
parent = cmds.pop.parent
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
command = Grammar::Command.parse(parent, token)
|
213
|
+
nodes.push command
|
214
|
+
cmds.push command
|
215
|
+
|
216
|
+
when :spec
|
217
|
+
spec = Grammar::ArgSpec.parse(cmds.top, token)
|
218
|
+
@tokens.shift_while { |token| token.kind == :argument }.each { |token|
|
219
|
+
Grammar::Arg.parse(spec, token)
|
220
|
+
}
|
221
|
+
|
222
|
+
when :argument
|
223
|
+
; raise # Should never happen
|
224
|
+
|
225
|
+
when :usage
|
226
|
+
; # Do nothing
|
227
|
+
|
228
|
+
when :usage_string
|
229
|
+
Grammar::ArgDescr.parse(cmds.top, token)
|
230
|
+
|
231
|
+
when :text
|
232
|
+
# Text is only allowed on new lines
|
233
|
+
token.lineno > nodes.top.token.lineno
|
234
|
+
|
235
|
+
# Detect indented comment groups (code)
|
236
|
+
if nodes.top.is_a?(Grammar::Paragraph)
|
237
|
+
code = Grammar::Code.parse(nodes.top.parent, token) # Using parent of paragraph
|
238
|
+
@tokens.shift_while { |t|
|
239
|
+
if t.kind == :text && t.charno >= token.charno
|
240
|
+
code.tokens << t
|
241
|
+
elsif t.kind == :blank && @tokens.first&.kind != :blank # Emit last blank line
|
242
|
+
if @tokens.first&.charno >= token.charno # But only if it is not the last blank line
|
243
|
+
code.tokens << t
|
244
|
+
end
|
245
|
+
else
|
246
|
+
break
|
247
|
+
end
|
248
|
+
}
|
249
|
+
|
250
|
+
# Detect comment groups (paragraphs)
|
251
|
+
else
|
252
|
+
if nodes.top.is_a?(Grammar::Command) || nodes.top.is_a?(Grammar::OptionGroup)
|
253
|
+
Grammar::Brief.new(nodes.top, token, token.source.sub(/\..*/, "")) if !nodes.top.brief
|
254
|
+
parent = nodes.top
|
255
|
+
else
|
256
|
+
parent = nodes.top.parent
|
257
|
+
end
|
258
|
+
|
259
|
+
paragraph = Grammar::Paragraph.parse(parent, token)
|
260
|
+
while @tokens.first&.kind == :text && @tokens.first.charno == token.charno
|
261
|
+
paragraph.tokens << @tokens.shift
|
262
|
+
end
|
263
|
+
nodes.push paragraph # Leave paragraph on stack so we can detect code blocks
|
264
|
+
end
|
265
|
+
|
266
|
+
when :brief
|
267
|
+
parent = nodes.top.is_a?(Grammar::Paragraph) ? nodes.top.parent : nodes.top
|
268
|
+
parent.brief.nil? or parse_error token, "Duplicate brief"
|
269
|
+
Grammar::Brief.parse(parent, token)
|
270
|
+
|
271
|
+
when :blank
|
272
|
+
; # do nothing
|
273
|
+
|
274
|
+
else
|
275
|
+
raise InternalError, "Unexpected token kind: #{token.kind.inspect}"
|
276
|
+
end
|
277
|
+
|
278
|
+
# Skip blank lines
|
279
|
+
@tokens.shift_while { |token| token.kind == :blank }
|
280
|
+
end
|
281
|
+
|
282
|
+
@program
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.parse(tokens)
|
286
|
+
self.new(tokens).parse
|
287
|
+
end
|
288
|
+
|
289
|
+
protected
|
290
|
+
def parse_error(token, message) raise ParserError, token, message end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
@@ -0,0 +1,279 @@
|
|
1
|
+
|
2
|
+
# TODO: Create a BasicShellOptsObject with is_a? and operators defined
|
3
|
+
#
|
4
|
+
module ShellOpts
|
5
|
+
# Command represents a program or a subcommand. It is derived from
|
6
|
+
# BasicObject to have only a minimum of inherited member methods.
|
7
|
+
# Additional methods defined in Command use the '__<identifier>__' naming
|
8
|
+
# convention that doesn't collide with option or subcommand names but
|
9
|
+
# they're rarely used in application code
|
10
|
+
#
|
11
|
+
# The names of the inherited methods can't be used as options or
|
12
|
+
# command namess. They are: instance_eval, instance_exec method_missing,
|
13
|
+
# singleton_method_added, singleton_method_removed, and
|
14
|
+
# singleton_method_undefined
|
15
|
+
#
|
16
|
+
# Command also defines #subcommand and #subcommand! but they can be
|
17
|
+
# overshadowed by an option or command declaration. Their values can
|
18
|
+
# still be accessed using the dashed name, though
|
19
|
+
#
|
20
|
+
# Options and subcommands can be accessed using #[]
|
21
|
+
#
|
22
|
+
# The following methods are created dynamically for each declared option
|
23
|
+
# with an attribute name
|
24
|
+
#
|
25
|
+
# def <identifier>(default = nil) self["<identifier>"] || default end
|
26
|
+
# def <identifier>=(value) self["<identifier>"] = value end
|
27
|
+
# def <identifier>?() self.key?("<identifier>") end
|
28
|
+
#
|
29
|
+
# Options without an an attribute can still be accessed using #[] or trough
|
30
|
+
# #__options__ or #__options_list__):
|
31
|
+
#
|
32
|
+
# Each subcommand has a single method:
|
33
|
+
#
|
34
|
+
# # Return the subcommand object or nil if not present
|
35
|
+
# def <identifier>!() subcommand == :<identifier> ? @__subcommand__ : nil end
|
36
|
+
#
|
37
|
+
# The general #subcommand method can be used to find out which subcommand is
|
38
|
+
# used
|
39
|
+
#
|
40
|
+
class Command < BasicObject
|
41
|
+
define_method(:is_a?, ::Kernel.method(:is_a?))
|
42
|
+
|
43
|
+
# These names can't be used as option or command names
|
44
|
+
RESERVED_OPTION_NAMES = %w(
|
45
|
+
is_a
|
46
|
+
instance_eval instance_exec method_missing singleton_method_added
|
47
|
+
singleton_method_removed singleton_method_undefined)
|
48
|
+
|
49
|
+
# These methods can be overridden by an option (the value is not used -
|
50
|
+
# this is just for informational purposes)
|
51
|
+
OVERRIDEABLE_METHODS = %w(
|
52
|
+
subcommand
|
53
|
+
)
|
54
|
+
|
55
|
+
# Redefine ::new to call #__initialize__
|
56
|
+
def self.new(grammar)
|
57
|
+
object = super()
|
58
|
+
object.__send__(:__initialize__, grammar)
|
59
|
+
object
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return command object or option argument value if present, otherwise nil
|
63
|
+
#
|
64
|
+
# The key is the name or identifier of the object or any any option
|
65
|
+
# alias. Eg. :f, '-f', :file, or '--file' are all usable as option keys
|
66
|
+
# and :cmd! or 'cmd' as command keys
|
67
|
+
#
|
68
|
+
# For options, the returned value is the argument given by the user
|
69
|
+
# optionally converted to Integer or Float or nil if the option doesn't
|
70
|
+
# take arguments. If the option takes an argument and it is repeatable
|
71
|
+
# the value is an array of the arguments. Repeatable options without
|
72
|
+
# arguments have the number of occurences as the value
|
73
|
+
#
|
74
|
+
def [](key)
|
75
|
+
case object = __grammar__[key]
|
76
|
+
when ::ShellOpts::Grammar::Command
|
77
|
+
object.ident == __subcommand__!.__ident__ ? __subcommand__! : nil
|
78
|
+
when ::ShellOpts::Grammar::Option
|
79
|
+
__options__[object.ident]
|
80
|
+
else
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Assign a value to an existing option. This can be used to implement
|
86
|
+
# default values. #[]= doesn't currently check the type of the given
|
87
|
+
# value so take care. Note that the corresponding option(s) in
|
88
|
+
# #__option_list__ is not updated
|
89
|
+
def []=(key, value)
|
90
|
+
case object = __grammar__[key]
|
91
|
+
when ::ShellOpts::Grammar::Command
|
92
|
+
::Kernel.raise ArgumentError, "#{key.inspect} is not an option"
|
93
|
+
when ::ShellOpts::Grammar::Option
|
94
|
+
object.argument? || object.repeatable? or
|
95
|
+
::Kernel.raise ArgumentError, "#{key.inspect} is not assignable"
|
96
|
+
__options__[object.ident] = value
|
97
|
+
else
|
98
|
+
::Kernel.raise ArgumentError, "Unknown option or command: #{key.inspect}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return true if the given command or option is present
|
103
|
+
def key?(key)
|
104
|
+
case object = __grammar__[key]
|
105
|
+
when ::ShellOpts::Grammar::Command
|
106
|
+
object.ident == __subcommand__!.ident ? __subcommand__! : nil
|
107
|
+
when ::ShellOpts::Grammar::Option
|
108
|
+
__options__.key?(object.ident)
|
109
|
+
else
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Subcommand identifier or nil if not present. #subcommand is often used in
|
115
|
+
# case statement to branch out to code that handles the given subcommand:
|
116
|
+
#
|
117
|
+
# prog, args = ShellOpts.parse("do_this! do_that!", ARGV)
|
118
|
+
# case prog.subcommand
|
119
|
+
# when :do_this!; prog.do_this.operation # or prog[:subcommand!] or prog.subcommand!
|
120
|
+
# when :do_that!; prog.do_that.operation
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# Note: Can be overridden by option, in that case use #__subcommand__ or
|
124
|
+
# ShellOpts.subcommand(object) instead
|
125
|
+
def subcommand() __subcommand__ end
|
126
|
+
|
127
|
+
# The subcommand object or nil if not present. Per-subcommand methods
|
128
|
+
# (#<identifier>!) are often used instead of #subcommand! to get the
|
129
|
+
# subcommand
|
130
|
+
#
|
131
|
+
# Note: Can be overridden by a subcommand declaration (but not an
|
132
|
+
# option), in that case use #__subcommand__! or
|
133
|
+
# ShellOpts.subcommand!(object) instead
|
134
|
+
#
|
135
|
+
def subcommand!() __subcommand__! end
|
136
|
+
|
137
|
+
# The parent command or nil. Initialized by #add_command
|
138
|
+
attr_accessor :__supercommand__
|
139
|
+
|
140
|
+
# UID of command/program
|
141
|
+
def __uid__() @__grammar__.uid end
|
142
|
+
|
143
|
+
# Identfier including the exclamation mark (Symbol)
|
144
|
+
def __ident__() @__grammar__.ident end
|
145
|
+
|
146
|
+
# Name of command/program without the exclamation mark (String)
|
147
|
+
def __name__() @__grammar__.name end
|
148
|
+
|
149
|
+
# Grammar object
|
150
|
+
attr_reader :__grammar__
|
151
|
+
|
152
|
+
# Hash from identifier to value. Can be Integer, Float, or String
|
153
|
+
# depending on the option's type. Repeated options options without
|
154
|
+
# arguments have the number of occurences as the value, with arguments
|
155
|
+
# the value is an array of the given values
|
156
|
+
attr_reader :__options__
|
157
|
+
|
158
|
+
# List of Option objects for the subcommand in the same order as
|
159
|
+
# given by the user but note that options are reordered to come after
|
160
|
+
# their associated subcommand if float is true. Repeated options are not
|
161
|
+
# collapsed
|
162
|
+
attr_reader :__option_list__
|
163
|
+
|
164
|
+
# The subcommand identifier (a Symbol incl. the exclamation mark) or nil
|
165
|
+
# if not present. Use #subcommand!, or the dynamically generated
|
166
|
+
# '#<identifier>!' method to get the actual subcommand object
|
167
|
+
def __subcommand__() @__subcommand__&.__ident__ end
|
168
|
+
|
169
|
+
# The actual subcommand object or nil if not present
|
170
|
+
def __subcommand__!() @__subcommand__ end
|
171
|
+
|
172
|
+
private
|
173
|
+
def __initialize__(grammar)
|
174
|
+
@__grammar__ = grammar
|
175
|
+
@__options__ = {}
|
176
|
+
@__option_list__ = []
|
177
|
+
@__options__ = {}
|
178
|
+
@__subcommand__ = nil
|
179
|
+
|
180
|
+
__define_option_methods__
|
181
|
+
end
|
182
|
+
|
183
|
+
def __define_option_methods__
|
184
|
+
@__grammar__.options.each { |opt|
|
185
|
+
next if opt.attr.nil?
|
186
|
+
if opt.argument? || opt.repeatable?
|
187
|
+
if opt.optional?
|
188
|
+
self.instance_eval %(
|
189
|
+
def #{opt.attr}(default = nil)
|
190
|
+
if @__options__.key?(:#{opt.attr})
|
191
|
+
@__options__[:#{opt.attr}] || default
|
192
|
+
else
|
193
|
+
nil
|
194
|
+
end
|
195
|
+
end
|
196
|
+
)
|
197
|
+
elsif !opt.argument?
|
198
|
+
self.instance_eval %(
|
199
|
+
def #{opt.attr}(default = nil)
|
200
|
+
if @__options__.key?(:#{opt.attr})
|
201
|
+
value = @__options__[:#{opt.attr}]
|
202
|
+
value == 0 ? default : value
|
203
|
+
else
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
end
|
207
|
+
)
|
208
|
+
else
|
209
|
+
self.instance_eval("def #{opt.attr}() @__options__[:#{opt.attr}] end")
|
210
|
+
end
|
211
|
+
self.instance_eval("def #{opt.attr}=(value) @__options__[:#{opt.attr}] = value end")
|
212
|
+
@__options__[opt.attr] = 0 if !opt.argument?
|
213
|
+
end
|
214
|
+
self.instance_eval("def #{opt.attr}?() @__options__.key?(:#{opt.attr}) end")
|
215
|
+
}
|
216
|
+
|
217
|
+
@__grammar__.commands.each { |cmd|
|
218
|
+
next if cmd.attr.nil?
|
219
|
+
self.instance_eval %(
|
220
|
+
def #{cmd.attr}()
|
221
|
+
:#{cmd.attr} == __subcommand__ ? __subcommand__! : nil
|
222
|
+
end
|
223
|
+
)
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
def __add_option__(option)
|
228
|
+
ident = option.grammar.ident
|
229
|
+
@__option_list__ << option
|
230
|
+
if option.repeatable?
|
231
|
+
if option.argument?
|
232
|
+
(@__options__[ident] ||= []) << option.argument
|
233
|
+
else
|
234
|
+
@__options__[ident] ||= 0
|
235
|
+
@__options__[ident] += 1
|
236
|
+
end
|
237
|
+
else
|
238
|
+
@__options__[ident] = option.argument
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def __add_command__(subcommand)
|
243
|
+
subcommand.__supercommand__ = self
|
244
|
+
@__subcommand__ = subcommand
|
245
|
+
end
|
246
|
+
|
247
|
+
def self.add_option(subcommand, option) subcommand.__send__(:__add_option__, option) end
|
248
|
+
def self.add_command(subcommand, cmd) subcommand.__send__(:__add_command__, cmd) end
|
249
|
+
end
|
250
|
+
|
251
|
+
# The top-level command
|
252
|
+
class Program < Command
|
253
|
+
end
|
254
|
+
|
255
|
+
# Option models an option as given by the user on the subcommand line.
|
256
|
+
# Compiled options (and possibly aggregated) options are stored in the
|
257
|
+
# Command#__options__ array
|
258
|
+
class Option
|
259
|
+
# Associated Grammar::Option object
|
260
|
+
attr_reader :grammar
|
261
|
+
|
262
|
+
# The actual name used on the shell command-line (String)
|
263
|
+
attr_reader :name
|
264
|
+
|
265
|
+
# Argument value or nil if not present. The value is a String, Integer,
|
266
|
+
# or Float depending the on the type of the option
|
267
|
+
attr_accessor :argument
|
268
|
+
|
269
|
+
forward_to :grammar,
|
270
|
+
:uid, :ident,
|
271
|
+
:repeatable?, :argument?, :integer?, :float?,
|
272
|
+
:file?, :enum?, :string?, :optional?,
|
273
|
+
:argument_name, :argument_type, :argument_enum
|
274
|
+
|
275
|
+
def initialize(grammar, name, argument)
|
276
|
+
@grammar, @name, @argument = grammar, name, argument
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|