shellopts 2.0.0.pre.13 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.ruby-version +1 -1
  4. data/README.md +201 -267
  5. data/TODO +46 -134
  6. data/doc/format.rb +95 -0
  7. data/doc/grammar.txt +27 -0
  8. data/doc/syntax.rb +110 -0
  9. data/doc/syntax.txt +10 -0
  10. data/lib/ext/array.rb +58 -5
  11. data/lib/ext/forward_to.rb +15 -0
  12. data/lib/ext/lcs.rb +34 -0
  13. data/lib/shellopts/analyzer.rb +130 -0
  14. data/lib/shellopts/ansi.rb +8 -0
  15. data/lib/shellopts/args.rb +29 -21
  16. data/lib/shellopts/argument_type.rb +139 -0
  17. data/lib/shellopts/dump.rb +158 -0
  18. data/lib/shellopts/formatter.rb +325 -0
  19. data/lib/shellopts/grammar.rb +375 -0
  20. data/lib/shellopts/interpreter.rb +103 -0
  21. data/lib/shellopts/lexer.rb +175 -0
  22. data/lib/shellopts/parser.rb +269 -82
  23. data/lib/shellopts/program.rb +279 -0
  24. data/lib/shellopts/renderer.rb +227 -0
  25. data/lib/shellopts/stack.rb +7 -0
  26. data/lib/shellopts/token.rb +44 -0
  27. data/lib/shellopts/version.rb +2 -2
  28. data/lib/shellopts.rb +439 -220
  29. data/main +1180 -0
  30. data/shellopts.gemspec +9 -15
  31. metadata +85 -42
  32. data/lib/main.rb +0 -1
  33. data/lib/shellopts/ast/command.rb +0 -41
  34. data/lib/shellopts/ast/node.rb +0 -37
  35. data/lib/shellopts/ast/option.rb +0 -21
  36. data/lib/shellopts/ast/program.rb +0 -14
  37. data/lib/shellopts/compiler.rb +0 -128
  38. data/lib/shellopts/generator.rb +0 -15
  39. data/lib/shellopts/grammar/command.rb +0 -80
  40. data/lib/shellopts/grammar/node.rb +0 -33
  41. data/lib/shellopts/grammar/option.rb +0 -66
  42. data/lib/shellopts/grammar/program.rb +0 -65
  43. data/lib/shellopts/idr.rb +0 -236
  44. data/lib/shellopts/main.rb +0 -10
  45. data/lib/shellopts/option_struct.rb +0 -148
  46. data/lib/shellopts/shellopts.rb +0 -123
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