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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.ruby-version +1 -1
  4. data/README.md +201 -267
  5. data/TODO +37 -5
  6. data/doc/format.rb +95 -0
  7. data/doc/grammar.txt +27 -0
  8. data/doc/syntax.rb +110 -0
  9. data/doc/syntax.txt +10 -0
  10. data/lib/ext/array.rb +62 -0
  11. data/lib/ext/forward_to.rb +15 -0
  12. data/lib/ext/lcs.rb +34 -0
  13. data/lib/shellopts/analyzer.rb +130 -0
  14. data/lib/shellopts/ansi.rb +8 -0
  15. data/lib/shellopts/args.rb +25 -15
  16. data/lib/shellopts/argument_type.rb +139 -0
  17. data/lib/shellopts/dump.rb +158 -0
  18. data/lib/shellopts/formatter.rb +292 -92
  19. data/lib/shellopts/grammar.rb +375 -0
  20. data/lib/shellopts/interpreter.rb +103 -0
  21. data/lib/shellopts/lexer.rb +175 -0
  22. data/lib/shellopts/parser.rb +293 -0
  23. data/lib/shellopts/program.rb +279 -0
  24. data/lib/shellopts/renderer.rb +227 -0
  25. data/lib/shellopts/stack.rb +7 -0
  26. data/lib/shellopts/token.rb +44 -0
  27. data/lib/shellopts/version.rb +1 -1
  28. data/lib/shellopts.rb +359 -3
  29. data/main +1180 -0
  30. data/shellopts.gemspec +8 -14
  31. metadata +86 -41
  32. data/lib/ext/algorithm.rb +0 -14
  33. data/lib/ext/ruby_env.rb +0 -8
  34. data/lib/shellopts/ast/command.rb +0 -112
  35. data/lib/shellopts/ast/dump.rb +0 -28
  36. data/lib/shellopts/ast/option.rb +0 -15
  37. data/lib/shellopts/ast/parser.rb +0 -106
  38. data/lib/shellopts/constants.rb +0 -88
  39. data/lib/shellopts/exceptions.rb +0 -21
  40. data/lib/shellopts/grammar/analyzer.rb +0 -76
  41. data/lib/shellopts/grammar/command.rb +0 -87
  42. data/lib/shellopts/grammar/dump.rb +0 -56
  43. data/lib/shellopts/grammar/lexer.rb +0 -56
  44. data/lib/shellopts/grammar/option.rb +0 -55
  45. 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