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.
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,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 += @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
+