shellopts 2.0.0.pre.14 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|