shellopts 2.0.0.pre.11 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 +26 -16
  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 -207
  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
data/lib/shellopts.rb CHANGED
@@ -1,243 +1,475 @@
1
- require "shellopts/version"
2
1
 
3
- require 'shellopts/compiler.rb'
2
+ $quiet = nil
3
+ $verb = nil
4
+ $debug = nil
5
+ $shellopts = nil
6
+
7
+ require 'indented_io'
8
+
9
+ #$LOAD_PATH.unshift "../constrain/lib"
10
+ require 'constrain'
11
+ include Constrain
12
+
13
+ require 'ext/array.rb'
14
+ require 'ext/forward_to.rb'
15
+ require 'ext/lcs.rb'
16
+ include ForwardTo
17
+
18
+ require 'shellopts/version.rb'
19
+
20
+ require 'shellopts/stack.rb'
21
+ require 'shellopts/token.rb'
22
+ require 'shellopts/grammar.rb'
23
+ require 'shellopts/program.rb'
24
+ require 'shellopts/lexer.rb'
25
+ require 'shellopts/argument_type.rb'
4
26
  require 'shellopts/parser.rb'
5
- require 'shellopts/generator.rb'
6
- require 'shellopts/option_struct.rb'
7
- require 'shellopts/main.rb'
8
-
9
- # Name of program. Defined as the basename of the program file
10
- #PROGRAM = File.basename($PROGRAM_NAME)
11
-
12
- # ShellOpts main Module
13
- #
14
- # This module contains methods to process command line options and arguments.
15
- # ShellOpts keeps a reference in ShellOpts.shellopts to the result of the last
16
- # command that was processed through its interface and use it as the implicit
17
- # object of many of its methods. This matches the typical use case where only
18
- # one command line is ever processed and makes it possible to create class
19
- # methods that knows about the command like #error and #fail
20
- #
21
- # For example; the following process and convert a command line into a struct
22
- # representation and also sets ShellOpts.shellopts object so that the #error
23
- # method can print a relevant spec string:
24
- #
25
- # USAGE = "a,all f,file=FILE -- ARG1 ARG2"
26
- # opts, args = ShellOpts.as_struct(USAGE, ARGV)
27
- # File.exist?(opts.file) or error "Can't find #{opts.file}"
28
- #
29
- # The command line is processed through one of the methods #process, #as_array,
30
- # #as_hash, or #as_struct that returns a [data, args] tuple. The data type
31
- # depends on the method: #process yields a Idr object that internally serves as
32
- # the base for the #as_array and #as_hash and #as_struct that converts it into
33
- # an Array, Hash, or ShellOpts::OptionStruct object. For example:
34
- #
35
- # USAGE = "..."
36
- # ShellOpts.process(USAGE, ARGV)
37
- # program, args = ShellOpts.as_program(USAGE, ARGV)
38
- # array, args = ShellOpts.as_array(USAGE, ARGV)
39
- # hash, args = ShellOpts.as_hash(USAGE, ARGV)
40
- # struct, args = ShellOpts.as_struct(USAGE, ARGV)
41
- #
42
- # +args+ is a ShellOpts::Argv object containing the the remaning command line
43
- # arguments. Argv is derived from Array
44
- #
45
- # ShellOpts can raise the exception CompilerError is there is an error in the
46
- # USAGE string. If there is an error in the user supplied command line, #error
47
- # is called instead and the program terminates with exit code 1. ShellOpts
48
- # raises ConversionError is there is a name collision when converting to the
49
- # hash or struct representations. Note that CompilerError and ConversionError
50
- # are caused by misuse of the library and the problem should be corrected by
51
- # the developer
52
- #
53
- # ShellOpts injects the constant PROGRAM into the global scope. It contains the
54
- # name of the program
55
- #
56
- # INCLUDING SHELLOPTS
57
- #
58
- # ShellOpts can optionally be included in your shell application main file but
59
- # it is not supposed to be included anywhere else
60
- #
61
- # Some behind the scenes magic happen if you include the ShellOpts module in your
62
- # main exe file
63
- #
27
+ require 'shellopts/analyzer.rb'
28
+ require 'shellopts/interpreter.rb'
29
+ require 'shellopts/ansi.rb'
30
+ require 'shellopts/renderer.rb'
31
+ require 'shellopts/formatter.rb'
32
+ require 'shellopts/dump.rb'
33
+
34
+
64
35
  module ShellOpts
65
- def self.default_name()
66
- @default_name || defined?(PROGRAM) ? PROGRAM : File.basename($0)
36
+ # Base error class
37
+ #
38
+ # Note that errors in the usage of the ShellOpts library are reported using
39
+ # standard exceptions
40
+ #
41
+ class ShellOptsError < StandardError
42
+ attr_reader :token
43
+ def initialize(token)
44
+ super
45
+ @token = token
46
+ end
67
47
  end
68
48
 
69
- def self.default_name=(name)
70
- @default_name = name
71
- end
49
+ # Raised on syntax errors on the command line (eg. unknown option). When
50
+ # ShellOpts handles the exception a message with the following format is
51
+ # printed on standard error:
52
+ #
53
+ # <program>: <message>
54
+ # Usage: <program> ...
55
+ #
56
+ class Error < ShellOptsError; end
57
+
58
+ # Default class for program failures. Failures are raised on missing files or
59
+ # illegal paths. When ShellOpts handles the exception a message with the
60
+ # following format is printed on standard error:
61
+ #
62
+ # <program>: <message>
63
+ #
64
+ class Failure < Error; end
65
+
66
+ # ShellOptsErrors during compilation. These errors are caused by syntax errors in the
67
+ # source. Messages are formatted as '<file> <lineno>:<charno> <message>' when
68
+ # handled by ShellOpts
69
+ class CompilerError < ShellOptsError; end
70
+ class LexerError < CompilerError; end
71
+ class ParserError < CompilerError; end
72
+ class AnalyzerError < CompilerError; end
73
+
74
+ # Internal errors. These are caused by bugs in the ShellOpts library
75
+ class InternalError < ShellOptsError; end
76
+
77
+ class ShellOpts
78
+ using Ext::Array::ShiftWhile
79
+ using Ext::Array::PopWhile
80
+
81
+ # Name of program. Defaults to the name of the executable
82
+ attr_reader :name
83
+
84
+ # Specification (String). Initialized by #compile
85
+ attr_reader :spec
86
+
87
+ # Array of arguments. Initialized by #interpret
88
+ attr_reader :argv
89
+
90
+ # Grammar. Grammar::Program object. Initialized by #compile
91
+ attr_reader :grammar
92
+
93
+ # Resulting ShellOpts::Program object containing options and optional
94
+ # subcommand. Initialized by #interpret
95
+ def program() @program end
96
+
97
+ # Array of remaining arguments. Initialized by #interpret
98
+ attr_reader :args
99
+
100
+ # Compiler flags
101
+ attr_accessor :stdopts
102
+ attr_accessor :msgopts
103
+
104
+ # Interpreter flags
105
+ attr_accessor :float
106
+
107
+ # True if ShellOpts lets exceptions through instead of writing an error
108
+ # message and exit
109
+ attr_accessor :exception
110
+
111
+ # File of source
112
+ attr_reader :file
113
+
114
+ # Debug: Internal variables made public
115
+ attr_reader :tokens
116
+ alias_method :ast, :grammar # Oops - defined earlier FIXME
117
+
118
+ def initialize(name: nil, stdopts: true, msgopts: false, float: true, exception: false)
119
+ @name = name || File.basename($PROGRAM_NAME)
120
+ @stdopts, @msgopts, @float, @exception = stdopts, msgopts, float, exception
121
+ end
72
122
 
73
- def self.default_usage()
74
- @default_usage || defined?(USAGE) ? USAGE : nil
75
- end
123
+ # Compile source and return grammar object. Also sets #spec and #grammar.
124
+ # Returns the grammar
125
+ def compile(spec)
126
+ handle_exceptions {
127
+ @oneline = spec.index("\n").nil?
128
+ @spec = spec.sub(/^\s*\n/, "")
129
+ @file = find_caller_file
130
+ @tokens = Lexer.lex(name, @spec, @oneline)
131
+ ast = Parser.parse(tokens)
132
+ # TODO: Add standard and message options and their handlers
133
+ @grammar = Analyzer.analyze(ast)
134
+ }
135
+ self
136
+ end
76
137
 
77
- def self.default_usage=(usage)
78
- @default_usage = usage
79
- end
138
+ # Use grammar to interpret arguments. Return a ShellOpts::Program and
139
+ # ShellOpts::Args tuple
140
+ #
141
+ def interpret(argv)
142
+ handle_exceptions {
143
+ @argv = argv.dup
144
+ @program, @args = Interpreter.interpret(grammar, argv, float: float, exception: exception)
145
+ }
146
+ self
147
+ end
80
148
 
81
- def self.default_key_type()
82
- @default_key_type || ::ShellOpts::DEFAULT_KEY_TYPE
83
- end
149
+ # Compile +spec+ and interpret +argv+. Returns a tuple of a
150
+ # ShellOpts::Program and ShellOpts::Args object
151
+ #
152
+ def process(spec, argv)
153
+ compile(spec)
154
+ interpret(argv)
155
+ self
156
+ end
84
157
 
85
- def self.default_key_type=(type)
86
- @default_key_type = type
87
- end
158
+ # Create a ShellOpts object and sets the global instance, then process the
159
+ # spec and arguments. Returns a tuple of a ShellOpts::Program with the
160
+ # options and subcommands and a ShellOpts::Args object with the remaining
161
+ # arguments
162
+ #
163
+ def self.process(spec, argv, **opts)
164
+ ::ShellOpts.instance = shellopts = ShellOpts.new(**opts)
165
+ shellopts.process(spec, argv)
166
+ [shellopts.program, shellopts.argv]
167
+ end
88
168
 
89
- # Base class for ShellOpts exceptions
90
- class Error < RuntimeError; end
169
+ # Write short usage and error message to standard error and terminate
170
+ # program with status 1
171
+ #
172
+ # #error is supposed to be used when the user made an error and the usage
173
+ # is written to help correcting the error
174
+ #
175
+ def error(subject = nil, message)
176
+ saved = $stdout
177
+ $stdout = $stderr
178
+ $stderr.puts "#{name}: #{message}"
179
+ Formatter.usage(program)
180
+ exit 1
181
+ ensure
182
+ $stdout = saved
183
+ end
91
184
 
92
- # Raised when a syntax error is detected in the spec string
93
- class CompilerError < Error
94
- def initialize(start, usage)
95
- super(usage)
96
- set_backtrace(caller(start))
185
+ # Write error message to standard error and terminate program with status 1
186
+ #
187
+ # #failure is supposed to be used the used specified the correct arguments
188
+ # but something went wrong during processing. Since the used didn't cause
189
+ # the problem, only the error message is written
190
+ #
191
+ def failure(message)
192
+ $stderr.puts "#{name}: #{message}"
193
+ exit 1
97
194
  end
98
- end
99
195
 
100
- # Raised when an error is detected in the command line
101
- class ParserError < Error; end
102
-
103
- # Raised when the command line error is caused by the user. It is raised by
104
- # the parser but can also be used by the application if the command line
105
- # fails a semantic check
106
- class UserError < ParserError; end
107
-
108
- # Raised when the error is caused by a failed assumption about the system. It
109
- # is not raised by the ShellOpts library as it only concerns itself with
110
- # command line syntax but can be used by the application to report a failure
111
- # through ShellOpts#fail method when the ShellOpts module is included
112
- class SystemFail < Error; end
113
-
114
- # Raised when an error is detected during conversion from the Idr to array,
115
- # hash, or struct
116
- class ConversionError < Error; end
117
-
118
- # Raised when an internal error is detected
119
- class InternalError < Error; end
120
-
121
- # The current compilation object. It is set by #process
122
- def self.shellopts() @shellopts end
123
-
124
- # Name of program
125
- def program_name() shellopts!.name end
126
- def program_name=(name) shellopts!.name = name end
127
-
128
- # Usage string
129
- def usage() shellopts!.spec end
130
- def usage=(spec) shellopts!.spec = spec end
131
-
132
- # Process command line, set current shellopts object, and return it.
133
- # Remaining arguments from the command line can be accessed through
134
- # +shellopts.args+
135
- def self.process(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
136
- @shellopts.nil? or reset
137
- @shellopts = ShellOpts.new(spec, argv, name: name, usage: usage)
138
- @shellopts.process
139
- end
196
+ # Print usage
197
+ def usage() Formatter.usage(@grammar) end
140
198
 
141
- # Process command line, set current shellopts object, and return a
142
- # [Idr::Program, argv] tuple. Automatically includes the ShellOpts module
143
- # if called from the main Ruby object (ie. your executable)
144
- def self.as_program(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
145
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
146
- process(spec, argv, name: name, usage: usage)
147
- [shellopts.idr, shellopts.args]
148
- end
199
+ # Print brief help
200
+ def brief() Formatter.brief(@grammar) end
149
201
 
150
- # Process command line, set current shellopts object, and return a [array,
151
- # argv] tuple. Automatically includes the ShellOpts module if called from the
152
- # main Ruby object (ie. your executable)
153
- def self.as_array(spec, argv, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage)
154
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
155
- process(spec, argv, name: name, usage: usage)
156
- [shellopts.to_a, shellopts.args]
157
- end
202
+ # Print help for the given subject or the full documentation if +subject+
203
+ # is nil
204
+ #
205
+ def help(subject = nil)
206
+ node = (subject ? @grammar[subject] : @grammar) or
207
+ raise ArgumentError, "No such command: '#{subject&.sub(".", " ")}'"
208
+ Formatter.help(node)
209
+ end
158
210
 
159
- # Process command line, set current shellopts object, and return a [hash,
160
- # argv] tuple. Automatically includes the ShellOpts module if called from the
161
- # main Ruby object (ie. your executable)
162
- def self.as_hash(
163
- spec, argv,
164
- name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
165
- key_type: ::ShellOpts.default_key_type,
166
- aliases: {})
167
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
168
- process(spec, argv, name: name, usage: usage)
169
- [shellopts.to_h(key_type: key_type, aliases: aliases), shellopts.args]
170
- end
211
+ def self.usage() ::ShellOpts.instance.usage end
212
+ def self.brief() ::ShellOpts.instance.brief end
213
+ def self.help(subject = nil) ::ShellOpts.instance.help(subject) end
214
+
215
+ private
216
+ def handle_exceptions(&block)
217
+ return yield if exception
218
+ begin
219
+ yield
220
+ rescue Error => ex
221
+ error(ex.message)
222
+ rescue Failure => ex
223
+ failure(ex.message)
224
+ rescue CompilerError => ex
225
+ filename = file =~ /\// ? file : "./#{file}"
226
+ lineno, charno = find_spec_in_file
227
+ charno = 1 if !@oneline
228
+ $stderr.puts "#{filename}:#{ex.token.pos(lineno, charno)} #{ex.message}"
229
+ exit(1)
230
+ end
231
+ end
232
+
233
+ def find_caller_file
234
+ caller.reverse.select { |line| line !~ /^\s*#{__FILE__}:/ }.last.sub(/:.*/, "").sub(/^\.\//, "")
235
+ end
236
+
237
+ def self.compare_lines(text, spec)
238
+ return true if text == spec
239
+ return true if text =~ /[#\$\\]/
240
+ false
241
+ end
242
+
243
+ public
244
+ # Find line and char index of spec in text. Returns [nil, nil] if not found
245
+ def self.find_spec_in_text(text, spec, oneline)
246
+ text_lines = text.split("\n")
247
+ spec_lines = spec.split("\n")
248
+ spec_lines.pop_while { |line| line =~ /^\s*$/ }
249
+
250
+ if oneline
251
+ line_i = nil
252
+ char_i = nil
253
+ char_z = 0
254
+
255
+ (0 ... text_lines.size).each { |text_i|
256
+ curr_char_i, curr_char_z =
257
+ LCS.find_longest_common_substring_index(text_lines[text_i], spec_lines.first.strip)
258
+ if curr_char_z > char_z
259
+ line_i = text_i
260
+ char_i = curr_char_i
261
+ char_z = curr_char_z
262
+ end
263
+ }
264
+ line_i ? [line_i, char_i] : [nil, nil]
265
+ else
266
+ spec_string = spec_lines.first.strip
267
+ line_i = (0 ... text_lines.size - spec_lines.size + 1).find { |text_i|
268
+ (0 ... spec_lines.size).all? { |spec_i|
269
+ compare_lines(text_lines[text_i + spec_i], spec_lines[spec_i])
270
+ }
271
+ } or return [nil, nil]
272
+ char_i, char_z =
273
+ LCS.find_longest_common_substring_index(text_lines[line_i], spec_lines.first.strip)
274
+ [line_i, char_i || 0]
275
+ end
276
+ end
277
+
278
+ def find_spec_in_file
279
+ self.class.find_spec_in_text(IO.read(@file), @spec, @oneline).map { |i| (i || 0) + 1 }
280
+ end
281
+
282
+ def lookup(name)
283
+ a = name.split(".")
284
+ cmd = grammar
285
+ while element = a.shift
286
+ cmd = cmd.commands[element]
287
+ end
288
+ cmd
289
+ end
171
290
 
172
- # Process command line, set current shellopts object, and return a [struct,
173
- # argv] tuple. Automatically includes the ShellOpts module if called from the
174
- # main Ruby object (ie. your executable)
175
- def self.as_struct(
176
- spec, argv,
177
- name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage,
178
- aliases: {})
179
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
180
- process(spec, argv, name: name, usage: usage)
181
- [shellopts.to_struct(aliases: aliases), shellopts.args]
291
+ def find_subject(obj)
292
+ case obj
293
+ when String; lookup(obj)
294
+ when Ast::Command; Command.grammar(obj) # FIXME
295
+ when Grammar::Command; obj
296
+ when NilClass; grammar
297
+ else
298
+ raise Internal, "Illegal object: #{obj.class}"
299
+ end
300
+ end
182
301
  end
183
302
 
184
- # Process command line, set current shellopts object, and then iterate
185
- # options and commands as an array. Returns an enumerator to the array
186
- # representation of the current shellopts object if not given a block
187
- # argument. Automatically includes the ShellOpts module if called from the
188
- # main Ruby object (ie. your executable)
189
- def self.each(spec = nil, argv = nil, name: ::ShellOpts.default_name, usage: ::ShellOpts.default_usage, &block)
190
- Main.main.send(:include, ::ShellOpts) if caller.last =~ Main::CALLER_RE
191
- process(spec, argv, name: name, usage: usage)
192
- shellopts.each(&block)
303
+ def self.process(spec, argv, msgopts: false, **opts)
304
+ msgopts ||= Messages.is_included?
305
+ ShellOpts.process(spec, argv, msgopts: msgopts, **opts)
193
306
  end
194
307
 
195
- # Print error usage and spec string and exit with status 1. This method
196
- # should be called in response to user-errors (eg. specifying an illegal
197
- # option)
198
- def self.error(*msgs, exit: true)
199
- shellopts!.error(msgs, exit: exit)
308
+ @instance = nil
309
+ def self.instance?() !@instance.nil? end
310
+ def self.instance() @instance or raise Error, "ShellOpts is not initialized" end
311
+ def self.instance=(instance) @instance = instance end
312
+
313
+ forward_self_to :instance, :error, :failure
314
+
315
+ # The Include module brings the reporting methods into the namespace when
316
+ # included
317
+ module Messages
318
+ @is_included = false
319
+ def self.is_included?() @is_included end
320
+ def self.include(...)
321
+ @is_included = true
322
+ super
323
+ end
324
+
325
+ def notice(message)
326
+ $stderr.puts "#{name}: #{message}" if !quiet?
327
+ end
328
+
329
+ def mesg(message)
330
+ $stdout.puts message if !quiet?
331
+ end
332
+
333
+ def verb(level = 1, message)
334
+ $stdout.puts message if level <= @verbose
335
+ end
336
+
337
+ def debug(message)
338
+ $stdout.puts message if debug?
339
+ end
200
340
  end
201
341
 
202
- # Print error usage and exit with status 1. This method should not be
203
- # called in response to system errors (eg. disk full)
204
- def self.fail(*msgs, exit: true)
205
- shellopts!.fail(*msgs, exit: exit)
342
+ module ErrorHandling
343
+ # TODO: Set up global exception handlers
206
344
  end
345
+ end
346
+
347
+
348
+
349
+
350
+
351
+
352
+
353
+
354
+ __END__
355
+
356
+ require "shellopts/version"
357
+
358
+ require "ext/algorithm.rb"
359
+ require "ext/ruby_env.rb"
360
+
361
+ require "shellopts/constants.rb"
362
+ require "shellopts/exceptions.rb"
363
+
364
+ require "shellopts/grammar/analyzer.rb"
365
+ require "shellopts/grammar/lexer.rb"
366
+ require "shellopts/grammar/parser.rb"
367
+ require "shellopts/grammar/command.rb"
368
+ require "shellopts/grammar/option.rb"
369
+
370
+ require "shellopts/ast/parser.rb"
371
+ require "shellopts/ast/command.rb"
372
+ require "shellopts/ast/option.rb"
373
+
374
+ require "shellopts/args.rb"
375
+ require "shellopts/formatter.rb"
207
376
 
208
- def self.included(base)
209
- # base.equal?(Object) is only true when included in main (we hope)
210
- if !@is_included_in_main && base.equal?(Object)
211
- @is_included_in_main = true
212
- at_exit do
213
- case $!
214
- when ShellOpts::UserError
215
- ::ShellOpts.error($!.message, exit: false)
216
- exit!(1)
217
- when ShellOpts::SystemFail
218
- ::ShellOpts.fail($!.message)
219
- exit!(1)
220
- end
377
+ if RUBY_ENV == "development"
378
+ require "shellopts/grammar/dump.rb"
379
+ require "shellopts/ast/dump.rb"
380
+ end
381
+
382
+ $verb = nil
383
+ $quiet = nil
384
+ $shellopts = nil
385
+
386
+ module ShellOpts
387
+ class ShellOpts
388
+ attr_reader :name # Name of program. Defaults to the name of the executable
389
+ attr_reader :spec
390
+ attr_reader :argv
391
+
392
+ attr_reader :grammar
393
+ attr_reader :program
394
+ attr_reader :arguments
395
+
396
+ def initialize(spec, argv, name: nil, exception: false)
397
+ @name = name || File.basename($PROGRAM_NAME)
398
+ @spec, @argv = spec, argv.dup
399
+ exprs = Grammar::Lexer.lex(@spec)
400
+ commands = Grammar::Parser.parse(@name, exprs)
401
+ @grammar = Grammar::Analyzer.analyze(commands)
402
+
403
+ begin
404
+ @program, @arguments = Ast::Parser.parse(@grammar, @argv)
405
+ rescue Error => ex
406
+ raise if exception
407
+ error(ex.subject, ex.message)
408
+ end
409
+ end
410
+
411
+ def error(subject = nil, message)
412
+ $stderr.puts "#{name}: #{message}"
413
+ usage(subject, device: $stderr)
414
+ exit 1
415
+ end
416
+
417
+ def fail(message)
418
+ $stderr.puts "#{name}: #{message}"
419
+ exit 1
420
+ end
421
+
422
+ def usage(subject = nil, device: $stdout, levels: 1, margin: "")
423
+ subject = find_subject(subject)
424
+ device.puts Formatter.usage_string(subject, levels: levels, margin: margin)
425
+ end
426
+
427
+ def help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
428
+ subject = find_subject(subject)
429
+ device.puts Formatter.help_string(subject, levels: levels, margin: margin, tab: tab)
430
+ end
431
+
432
+ private
433
+ def lookup(name)
434
+ a = name.split(".")
435
+ cmd = grammar
436
+ while element = a.shift
437
+ cmd = cmd.commands[element]
438
+ end
439
+ cmd
440
+ end
441
+
442
+ def find_subject(obj)
443
+ case obj
444
+ when String; lookup(obj)
445
+ when Ast::Command; Command.grammar(obj)
446
+ when Grammar::Command; obj
447
+ when NilClass; grammar
448
+ else
449
+ raise Internal, "Illegal object: #{obj.class}"
221
450
  end
222
451
  end
223
- super
224
452
  end
225
453
 
226
- private
227
- # Default default key type
228
- DEFAULT_KEY_TYPE = :name
454
+ def self.process(spec, argv, name: nil, exception: false)
455
+ $shellopts = ShellOpts.new(spec, argv, name: name, exception: exception)
456
+ [$shellopts.program, $shellopts.arguments]
457
+ end
229
458
 
230
- # Reset state variables
231
- def self.reset()
232
- @shellopts = nil
459
+ def self.error(subject = nil, message)
460
+ $shellopts.error(subject, message)
233
461
  end
234
462
 
235
- # (shorthand) Raise an InternalError if shellopts is nil. Return shellopts
236
- def self.shellopts!
237
- ::ShellOpts.shellopts or raise UserError, "No ShellOpts.shellopts object"
463
+ def self.fail(message)
464
+ $shellopts.fail(message)
238
465
  end
239
466
 
240
- @shellopts = nil
241
- @is_included_in_main = false
467
+ def self.help(subject = nil, device: $stdout, levels: 10, margin: "", tab: " ")
468
+ $shellopts.help(subject, device: device, levels: levels, margin: margin, tab: tab)
469
+ end
470
+
471
+ def self.usage(subject = nil, device: $stdout, levels: 1, margin: "")
472
+ $shellopts.usage(subject, device: device, levels: levels, margin: margin)
473
+ end
242
474
  end
243
475