shellopts 2.0.0.pre.13 → 2.0.1

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