shellopts 2.0.0.pre.13 → 2.0.1

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 (46) 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 +46 -134
  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 +58 -5
  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 +29 -21
  16. data/lib/shellopts/argument_type.rb +139 -0
  17. data/lib/shellopts/dump.rb +158 -0
  18. data/lib/shellopts/formatter.rb +325 -0
  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 +269 -82
  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 +2 -2
  28. data/lib/shellopts.rb +439 -220
  29. data/main +1180 -0
  30. data/shellopts.gemspec +9 -15
  31. metadata +85 -42
  32. data/lib/main.rb +0 -1
  33. data/lib/shellopts/ast/command.rb +0 -41
  34. data/lib/shellopts/ast/node.rb +0 -37
  35. data/lib/shellopts/ast/option.rb +0 -21
  36. data/lib/shellopts/ast/program.rb +0 -14
  37. data/lib/shellopts/compiler.rb +0 -128
  38. data/lib/shellopts/generator.rb +0 -15
  39. data/lib/shellopts/grammar/command.rb +0 -80
  40. data/lib/shellopts/grammar/node.rb +0 -33
  41. data/lib/shellopts/grammar/option.rb +0 -66
  42. data/lib/shellopts/grammar/program.rb +0 -65
  43. data/lib/shellopts/idr.rb +0 -236
  44. data/lib/shellopts/main.rb +0 -10
  45. data/lib/shellopts/option_struct.rb +0 -148
  46. data/lib/shellopts/shellopts.rb +0 -123
@@ -0,0 +1,375 @@
1
+ module ShellOpts
2
+ module Grammar
3
+ # Except for #parent, #children, and #token, all members are initialized by calling
4
+ # #parse on the object
5
+ #
6
+ class Node
7
+ attr_reader :parent
8
+ attr_reader :children
9
+ attr_reader :token
10
+
11
+ # Note that in derived classes 'super' should be called after member
12
+ # initialization because Node#initialize calls #attach on the parent that
13
+ # may need to access the members
14
+ def initialize(parent, token)
15
+ constrain parent, Node, nil
16
+ constrain parent, nil, lambda { |node| ALLOWED_PARENTS[self.class].any? { |klass| node.is_a?(klass) } }
17
+ constrain token, Token
18
+
19
+ @parent = parent
20
+ @children = []
21
+ @token = token
22
+ @parent.send(:attach, self) if @parent
23
+ end
24
+
25
+ def traverse(*klasses, &block)
26
+ do_traverse(Array(klasses).flatten, &block)
27
+ end
28
+
29
+ def parents() parent ? [parent] + parent.parents : [] end
30
+ def ancestors() parents.reverse end
31
+
32
+ def inspect
33
+ "#{self.class}"
34
+ end
35
+
36
+ protected
37
+ def attach(child)
38
+ @children << child
39
+ end
40
+
41
+ def do_traverse(klasses, &block)
42
+ yield(self) if klasses.empty? || klasses.any? { |klass| self.is_a?(klass) }
43
+ children.each { |node| node.traverse(klasses, &block) }
44
+ end
45
+ end
46
+
47
+ class IdrNode < Node
48
+ # Command of this object. This is different from #parent when a
49
+ # subcommand is nested on a higher level than its supercommand.
50
+ # Initialized by the analyzer
51
+ attr_reader :command
52
+
53
+ # Unique identifier of node (String) within the context of a program. nil
54
+ # for the Program object. It is the list of path elements concatenated
55
+ # with '.' and with internal '!' removed (eg. "cmd.opt" or "cmd.cmd!").
56
+ # Initialized by the parser
57
+ attr_reader :uid
58
+
59
+ # Path from Program object and down to this node. Array of identifiers.
60
+ # Empty for the Program object. Initialized by the parser
61
+ attr_reader :path
62
+
63
+ # Canonical identifier (Symbol) of the object
64
+ #
65
+ # For options, this is the canonical name of the objekt without the
66
+ # initial '-' or '--'. For commands it is the command name including the
67
+ # suffixed exclamation mark. Both options and commands have internal dashes
68
+ # replaced with underscores. Initialized by the parser
69
+ #
70
+ # Note that the analyzer fails with an an error if both --with-separator
71
+ # and --with_separator are used because they would both map to
72
+ # :with_separator
73
+ attr_reader :ident
74
+
75
+ # Canonical name (String) of the object
76
+ #
77
+ # This is the name of the object as the user sees it. For options it is
78
+ # the name of the first long option or the name of the first short option
79
+ # if there is no long option name. For commands it is the name without
80
+ # the exclamation mark. Initialized by the parser
81
+ attr_reader :name
82
+
83
+ # The associated attribute (Symbol) in the parent command object. nil if
84
+ # #ident is a reserved word. Initialized by the parser
85
+ attr_reader :attr
86
+
87
+ protected
88
+ def lookup(path)
89
+ path.empty? or raise ArgumentError, "Argument should be empty"
90
+ self
91
+ end
92
+ end
93
+
94
+ # Note that options are children of Command object but are attached to
95
+ # OptionGroup objects that in turn are attached to the command. This is
96
+ # done to be able to handle multiple options with common brief or
97
+ # descriptions
98
+ #
99
+ class Option < IdrNode
100
+ # Redefine command of this object
101
+ def command() parent.parent end # double-parent because options live in option groups
102
+
103
+ # Option group of this object
104
+ def group() parent end
105
+
106
+ # Short option identfiers
107
+ attr_reader :short_idents
108
+
109
+ # Long option identifiers
110
+ attr_reader :long_idents
111
+
112
+ # Short option names (including initial '-')
113
+ attr_reader :short_names
114
+
115
+ # Long option names (including initial '--')
116
+ attr_reader :long_names
117
+
118
+ # Identifiers of option. Include both short and long identifiers
119
+ def idents() short_idents + long_idents end
120
+
121
+ # Names of option. Includes both short and long option names
122
+ def names() short_names + long_names end # TODO: Should be in declaration order
123
+
124
+ # Name of argument or nil if not present
125
+ attr_reader :argument_name
126
+
127
+ # Type of argument (ArgType)
128
+ attr_reader :argument_type
129
+
130
+ # Enum values if argument type is an enumerator
131
+ def argument_enum() @argument_type.values end
132
+
133
+ def repeatable?() @repeatable end
134
+ def argument?() @argument end
135
+ def optional?() @optional end
136
+
137
+ def integer?() @argument_type.is_a? IntegerArgument end
138
+ def float?() @argument_type.is_a? FloatArgument end
139
+ def file?() @argument_type.is_a? FileArgument end
140
+ def enum?() @argument_type.is_a? EnumArgument end
141
+ def string?() argument? && !integer? && !float? && !file? && !enum? end
142
+
143
+ def match?(literal) argument_type.match?(literal) end
144
+
145
+ # Return true if the option can be assigned the given value
146
+ # def value?(value) ... end
147
+ end
148
+
149
+ # Note that all public attributes are assigned by #attach
150
+ #
151
+ class OptionGroup < Node
152
+ alias_method :command, :parent
153
+
154
+ # Array of options in declaration order
155
+ attr_reader :options
156
+
157
+ # Brief description of option(s)
158
+ attr_reader :brief
159
+
160
+ # Description of option(s)
161
+ attr_reader :description
162
+
163
+ def initialize(parent, token)
164
+ @options = []
165
+ @brief = nil
166
+ @default_brief = nil
167
+ @description = []
168
+ super(parent, token)
169
+ end
170
+
171
+ protected
172
+ def attach(child)
173
+ super
174
+ case child
175
+ when Option; @options << child
176
+ when Brief; @brief = child
177
+ when Paragraph; @description << child
178
+ when Code; @description << child
179
+ end
180
+ end
181
+ end
182
+
183
+ # Note that except for :options, all public attributes are assigned by
184
+ # #attach. :options and the member variables supporting the #[] and #[]=
185
+ # methods are initialized by the analyzer
186
+ #
187
+ class Command < IdrNode
188
+ # Supercommand or nil if this is the top-level Program object.
189
+ # Initialized by the analyzer
190
+ attr_reader :supercommand
191
+
192
+ # Brief description of command
193
+ attr_accessor :brief
194
+
195
+ # Description of command. Array of Paragraph or Code objects. Initialized
196
+ # by the parser
197
+ attr_reader :description
198
+
199
+ # Array of option groups in declaration order. Initialized by the parser
200
+ # TODO: Rename 'groups'
201
+ attr_reader :option_groups
202
+
203
+ # Array of options in declaration order. Initialized by the analyzer
204
+ attr_reader :options
205
+
206
+ # Array of sub-commands. Initialized by the parser but edited by the analyzer
207
+ attr_reader :commands
208
+
209
+ # Array of Arg objects. Initialized by the parser
210
+ attr_reader :specs
211
+
212
+ # Array of ArgDescr objects. Initialized by the parser
213
+ attr_reader :descrs
214
+
215
+ def initialize(parent, token)
216
+ @brief = nil
217
+ @default_brief = nil
218
+ @description = []
219
+ @option_groups = []
220
+ @options = []
221
+ @options_hash = {} # Initialized by the analyzer
222
+ @commands = []
223
+ @commands_hash = {} # Initialized by the analyzer
224
+ @specs = []
225
+ @descrs = []
226
+ super
227
+ end
228
+
229
+ # Maps from any (sub-)path, name or identifier of an option or command (including the
230
+ # suffixed '!') to the associated option. #[] and #key? can't be used
231
+ # until after the analyze phase
232
+ def [](key)
233
+ case key
234
+ when String; lookup(key.split("."))
235
+ when Symbol; lookup(key.to_s.sub(".", "!.").split(".").map(&:to_sym))
236
+ when Array; lookup(key)
237
+ else
238
+ nil
239
+ end
240
+ end
241
+
242
+ def key?(key) !self.[](key).nil? end
243
+
244
+ # Mostly for debug. Has questional semantics because it only lists local keys
245
+ def keys() @options_hash.keys + @commands_hash.keys end
246
+
247
+ # Shorthand to get the associated Grammar::Command object from a Program
248
+ # or a Grammar::Command object
249
+ def self.command(obj)
250
+ constrain obj, Command, ::ShellOpts::Program
251
+ obj.is_a?(Command) ? obj : obj.__grammar__
252
+ end
253
+
254
+ protected
255
+ def attach(child)
256
+ super
257
+ case child
258
+ when OptionGroup; @option_groups << child
259
+ when Command; @commands << child
260
+ when ArgSpec; @specs << child
261
+ when ArgDescr; @descrs << child
262
+ when Brief; @brief = child
263
+ when Paragraph; @description << child
264
+ when Code; @description << child
265
+ end
266
+ end
267
+
268
+ def lookup(path)
269
+ key = path[0]
270
+ if path.size > 1
271
+ @commands_hash[key]&.lookup(path[1..-1])
272
+ elsif path.size == 1
273
+ @commands_hash[key] || @options_hash[key]
274
+ else
275
+ self
276
+ end
277
+ end
278
+ end
279
+
280
+ class Program < Command
281
+ # Lifted from .gemspec. TODO
282
+ attr_reader :info
283
+ end
284
+
285
+ class ArgSpec < Node
286
+ # List of Arg objects (initialized by the analyzer)
287
+ alias_method :command, :parent
288
+
289
+ attr_reader :arguments
290
+
291
+ def initialize(parent, token)
292
+ @arguments = []
293
+ super
294
+ end
295
+
296
+ protected
297
+ def attach(child)
298
+ arguments << child if child.is_a?(Arg)
299
+ end
300
+ end
301
+
302
+ class Arg < Node
303
+ alias_method :spec, :parent
304
+ end
305
+
306
+ # DocNode object has no children but lines.
307
+ #
308
+ class DocNode < Node
309
+ # Array of :text tokens. Assigned by the parser
310
+ attr_reader :tokens
311
+
312
+ # The text of the node
313
+ def text() @text ||= tokens.map(&:source).join(" ") end
314
+
315
+ def lines() [text] end
316
+
317
+ def to_s() text end # FIXME
318
+
319
+ def initialize(parent, token, text = nil)
320
+ @tokens = [token]
321
+ @text = text
322
+ super(parent, token)
323
+ end
324
+ end
325
+
326
+ class ArgDescr < DocNode
327
+ alias_method :command, :parent
328
+ end
329
+
330
+ class Usage < ArgDescr
331
+ end
332
+
333
+ module WrappedNode
334
+ using Ext::Array::Wrap
335
+ def words() @words ||= text.split(" ") end
336
+ def lines(width, initial = 0) @lines ||= words.wrap(width, initial) end
337
+ end
338
+
339
+ class Brief < DocNode
340
+ include WrappedNode
341
+ alias_method :subject, :parent # Either a command or an option
342
+ end
343
+
344
+ class Paragraph < DocNode
345
+ include WrappedNode
346
+ alias_method :subject, :parent # Either a command or an option
347
+ end
348
+
349
+ class Code < DocNode
350
+ def text() @text ||= lines.join("\n") end
351
+ def lines() @lines ||= tokens.map { |t| " " * [t.charno - token.charno, 0].max + t.source } end
352
+ end
353
+
354
+ class Section < Node
355
+ def name() token.source end
356
+ end
357
+
358
+ class Node
359
+ ALLOWED_PARENTS = {
360
+ Program => [NilClass],
361
+ Command => [Command],
362
+ OptionGroup => [Command],
363
+ Option => [OptionGroup],
364
+ ArgSpec => [Command],
365
+ Arg => [ArgSpec],
366
+ ArgDescr => [Command],
367
+ Brief => [Command, OptionGroup, ArgSpec, ArgDescr],
368
+ Paragraph => [Command, OptionGroup],
369
+ Code => [Command, OptionGroup],
370
+ Section => [Program]
371
+ }
372
+ end
373
+ end
374
+ end
375
+
@@ -0,0 +1,103 @@
1
+
2
+ module ShellOpts
3
+ class Interpreter
4
+ attr_reader :expr
5
+ attr_reader :args
6
+
7
+ def initialize(grammar, argv, float: true, exception: false)
8
+ constrain grammar, Grammar::Program
9
+ constrain argv, [String]
10
+ @grammar, @argv = grammar, argv.dup
11
+ @float, @exception = float, exception
12
+ end
13
+
14
+ def interpret
15
+ @expr = command = Program.new(@grammar)
16
+ @seen = {} # Set of seen options by UID (using UID is needed when float is true)
17
+ @args = []
18
+
19
+ while arg = @argv.shift
20
+ if arg == "--"
21
+ break
22
+ elsif arg.start_with?("-")
23
+ interpret_option(command, arg)
24
+ elsif @args.empty? && subcommand_grammar = command.__grammar__[:"#{arg}!"]
25
+ command = Command.add_command(command, Command.new(subcommand_grammar))
26
+ else
27
+ if @float
28
+ @args << arg # This also signals that no more commands are accepted
29
+ else
30
+ @argv.unshift arg
31
+ break
32
+ end
33
+ end
34
+ end
35
+ [@expr, Args.new(@args + @argv)]
36
+ end
37
+
38
+ def self.interpret(grammar, argv, **opts)
39
+ self.new(grammar, argv, **opts).interpret
40
+ end
41
+
42
+ protected
43
+ # Lookup option in the command hierarchy and return pair of command and
44
+ # option associated command. Raise if not found
45
+ #
46
+ def find_option(command, name)
47
+ while command && (option = command.__grammar__[name]).nil?
48
+ command = command.__supercommand__
49
+ end
50
+ option or error "Unknown option '#{name}'"
51
+ [command, option]
52
+ end
53
+
54
+ def interpret_option(command, option)
55
+ # Split into name and argument
56
+ case option
57
+ when /^(--.+?)(?:=(.*))?$/
58
+ name, value, short = $1, $2, false
59
+ when /^(-.)(.+)?$/
60
+ name, value, short = $1, $2, true
61
+ end
62
+
63
+ option_command, option = find_option(command, name)
64
+ !@seen.key?(option.uid) || option.repeatable? or error "Duplicate option '#{name}'"
65
+ @seen[option.uid] = true
66
+
67
+ # Process argument
68
+ if option.argument?
69
+ if value.nil? && !option.optional?
70
+ if !@argv.empty?
71
+ value = @argv.shift
72
+ else
73
+ error "Missing argument for option '#{name}'"
74
+ end
75
+ end
76
+ value &&= interpret_option_value(option, name, value)
77
+ elsif value && short
78
+ @argv.unshift("-#{value}")
79
+ value = nil
80
+ elsif !value.nil?
81
+ error "No argument allowed for option '#{opt_name}'"
82
+ end
83
+
84
+ Command.add_option(option_command, Option.new(option, name, value))
85
+ end
86
+
87
+ def interpret_option_value(option, name, value)
88
+ type = option.argument_type
89
+ if type.match?(name, value)
90
+ type.convert(value)
91
+ elsif value == ""
92
+ nil
93
+ else
94
+ error type.message
95
+ end
96
+ end
97
+
98
+ def error(msg)
99
+ raise Error, msg
100
+ end
101
+ end
102
+ end
103
+
@@ -0,0 +1,175 @@
1
+
2
+ module ShellOpts
3
+ class Line
4
+ attr_reader :source
5
+ attr_reader :lineno
6
+ attr_reader :charno
7
+ attr_reader :text
8
+
9
+ def initialize(lineno, charno, source)
10
+ @lineno, @source = lineno, source
11
+ @charno = charno + ((@source =~ /(\S.*?)\s*$/) || 0)
12
+ @text = $1 || ""
13
+ end
14
+
15
+ def blank?() @text == "" end
16
+
17
+ forward_to :@text, :=~, :!~
18
+
19
+ # Split on whitespace while keeping track of character position. Returns
20
+ # array of char, word tuples
21
+ def words
22
+ return @words if @words
23
+ @words = []
24
+ charno = self.charno
25
+ text.scan(/(\s*)(\S*)/)[0..-2].each { |spaces, word|
26
+ charno += spaces.size
27
+ @words << [charno, word] if word != ""
28
+ charno += word.size
29
+ }
30
+ @words
31
+ end
32
+
33
+ def to_s() text end
34
+ def dump() puts "#{lineno}:#{charno} #{text.inspect}" end
35
+ end
36
+
37
+ class Lexer
38
+ COMMAND_RE = /[a-z][a-z._-]*!/
39
+
40
+ DECL_RE = /^(?:-|--|\+|\+\+|(?:@(?:\s|$))|(?:[^\\!]\S*!(?:\s|$)))/
41
+
42
+ # Match ArgSpec argument words. TODO
43
+ SPEC_RE = /^[^a-z]{2,}$/
44
+
45
+ # Match ArgDescr words (should be at least two characters long)
46
+ DESCR_RE = /^[^a-z]{2,}$/
47
+
48
+ SECTIONS = %w(DESCRIPTION OPTIONS COMMANDS)
49
+
50
+ using Ext::Array::ShiftWhile
51
+
52
+ attr_reader :name # Name of program
53
+ attr_reader :source
54
+ attr_reader :tokens
55
+
56
+ def oneline?() @oneline end
57
+
58
+ def initialize(name, source, oneline)
59
+ @name = name
60
+ @source = source
61
+ @oneline = oneline
62
+ @source += "\n" if @source[-1] != "\n" # Always terminate source with a newline
63
+ end
64
+
65
+ def lex(lineno = 1, charno = 1)
66
+ # Split source into lines and tag them with lineno and charno. Only the
67
+ # first line can have charno != 1
68
+ lines = source[0..-2].split("\n").map.with_index { |line,i|
69
+ l = Line.new(i + lineno, charno, line)
70
+ charno = 1
71
+ l
72
+ }
73
+
74
+ # Skip initial comments and blank lines and compute indent level
75
+ lines.shift_while { |line| line.text == "" || line.text.start_with?("#") && line.charno == 1 }
76
+ initial_indent = lines.first&.charno
77
+
78
+ # Create program token. The source is the program name
79
+ @tokens = [Token.new(:program, 0, 0, name)]
80
+
81
+ # Used to detect code blocks
82
+ last_nonblank = @tokens.first
83
+
84
+ # Process lines
85
+ while line = lines.shift
86
+ # Pass-trough blank lines
87
+ if line.to_s == ""
88
+ @tokens << Token.new(:blank, line.lineno, line.charno, "")
89
+ next
90
+ end
91
+
92
+ # Ignore meta comments
93
+ if line.charno < initial_indent
94
+ next if line =~ /^#/
95
+ error_token = Token.new(:text, line.lineno, 0, "")
96
+ lexer_error error_token, "Illegal indentation"
97
+ end
98
+
99
+ # Line without escape sequences
100
+ source = line.text[(line.text =~ /^\\/ ? 1 : 0)..-1]
101
+
102
+ # Code lines
103
+ if last_nonblank.kind == :text && line.charno > last_nonblank.charno && line !~ DECL_RE
104
+ @tokens << Token.new(:text, line.lineno, line.charno, source)
105
+ lines.shift_while { |line| line.blank? || line.charno > last_nonblank.charno }.each { |line|
106
+ kind = (line.blank? ? :blank : :text)
107
+ @tokens << Token.new(kind, line.lineno, line.charno, line.text)
108
+ }
109
+
110
+ # Sections
111
+ elsif SECTIONS.include?(line.text)
112
+ @tokens << Token.new(:section, line.lineno, line.charno, line.text)
113
+
114
+ # Options, commands, usage, arguments, and briefs
115
+ elsif line =~ DECL_RE
116
+ words = line.words
117
+ while (charno, word = words.shift)
118
+ case word
119
+ when "@"
120
+ if words.empty?
121
+ error_token = Token.new(:text, line.lineno, charno, "@")
122
+ lexer_error error_token, "Empty '@' declaration"
123
+ end
124
+ source = words.shift_while { true }.map(&:last).join(" ")
125
+ @tokens << Token.new(:brief, line.lineno, charno, source)
126
+ when "--" # FIXME Rename argdescr
127
+ @tokens << Token.new(:usage, line.lineno, charno, "--")
128
+ source = words.shift_while { |_,w| w =~ DESCR_RE }.map(&:last).join(" ")
129
+ @tokens << Token.new(:usage_string, line.lineno, charno, source)
130
+ when "++" # FIXME Rename argspec
131
+ @tokens << Token.new(:spec, line.lineno, charno, "++")
132
+ words.shift_while { |c,w|
133
+ w =~ SPEC_RE and @tokens << Token.new(:argument, line.lineno, c, w)
134
+ }
135
+ when /^-|\+/
136
+ @tokens << Token.new(:option, line.lineno, charno, word)
137
+ when /!$/
138
+ @tokens << Token.new(:command, line.lineno, charno, word)
139
+ else
140
+ source = [word, words.shift_while { |_,w| w !~ DECL_RE }.map(&:last)].flatten.join(" ")
141
+ @tokens << Token.new(:brief, line.lineno, charno, source)
142
+ end
143
+ end
144
+
145
+ # TODO: Move to parser and remove @oneline from Lexer
146
+ (token = @tokens.last).kind != :brief || !oneline? or
147
+ lexer_error token, "Briefs are only allowed in multi-line specifications"
148
+
149
+ # Paragraph lines
150
+ else
151
+ @tokens << Token.new(:text, line.lineno, line.charno, source)
152
+ end
153
+ # FIXME Not sure about this
154
+ # last_nonblank = @tokens.last
155
+ last_nonblank = @tokens.last if ![:blank, :usage_string, :argument].include? @tokens.last.kind
156
+ end
157
+
158
+ # Move arguments and briefs before first command if one-line source
159
+ # if oneline? && cmd_index = @tokens.index { |token| token.kind == :command }
160
+ # @tokens =
161
+ # @tokens[0...cmd_index] +
162
+ # @tokens[cmd_index..-1].partition { |token| ![:command, :option].include?(token.kind) }.flatten
163
+ # end
164
+
165
+ @tokens
166
+ end
167
+
168
+ def self.lex(name, source, oneline, lineno = 1, charno = 1)
169
+ Lexer.new(name, source, oneline).lex(lineno, charno)
170
+ end
171
+
172
+ def lexer_error(token, message) raise LexerError.new(token), message end
173
+ end
174
+ end
175
+