shellopts 2.0.0.pre.11 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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