shellopts 2.0.0.pre.14 → 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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.ruby-version +1 -1
  4. data/README.md +201 -267
  5. data/TODO +37 -5
  6. data/doc/format.rb +95 -0
  7. data/doc/grammar.txt +27 -0
  8. data/doc/syntax.rb +110 -0
  9. data/doc/syntax.txt +10 -0
  10. data/lib/ext/array.rb +62 -0
  11. data/lib/ext/forward_to.rb +15 -0
  12. data/lib/ext/lcs.rb +34 -0
  13. data/lib/shellopts/analyzer.rb +130 -0
  14. data/lib/shellopts/ansi.rb +8 -0
  15. data/lib/shellopts/args.rb +25 -15
  16. data/lib/shellopts/argument_type.rb +139 -0
  17. data/lib/shellopts/dump.rb +158 -0
  18. data/lib/shellopts/formatter.rb +292 -92
  19. data/lib/shellopts/grammar.rb +375 -0
  20. data/lib/shellopts/interpreter.rb +103 -0
  21. data/lib/shellopts/lexer.rb +175 -0
  22. data/lib/shellopts/parser.rb +293 -0
  23. data/lib/shellopts/program.rb +279 -0
  24. data/lib/shellopts/renderer.rb +227 -0
  25. data/lib/shellopts/stack.rb +7 -0
  26. data/lib/shellopts/token.rb +44 -0
  27. data/lib/shellopts/version.rb +1 -1
  28. data/lib/shellopts.rb +359 -3
  29. data/main +1180 -0
  30. data/shellopts.gemspec +8 -14
  31. metadata +86 -41
  32. data/lib/ext/algorithm.rb +0 -14
  33. data/lib/ext/ruby_env.rb +0 -8
  34. data/lib/shellopts/ast/command.rb +0 -112
  35. data/lib/shellopts/ast/dump.rb +0 -28
  36. data/lib/shellopts/ast/option.rb +0 -15
  37. data/lib/shellopts/ast/parser.rb +0 -106
  38. data/lib/shellopts/constants.rb +0 -88
  39. data/lib/shellopts/exceptions.rb +0 -21
  40. data/lib/shellopts/grammar/analyzer.rb +0 -76
  41. data/lib/shellopts/grammar/command.rb +0 -87
  42. data/lib/shellopts/grammar/dump.rb +0 -56
  43. data/lib/shellopts/grammar/lexer.rb +0 -56
  44. data/lib/shellopts/grammar/option.rb +0 -55
  45. data/lib/shellopts/grammar/parser.rb +0 -78
@@ -0,0 +1,227 @@
1
+ require 'terminfo'
2
+
3
+ # Option rendering
4
+ # -a, --all # Only used in brief and doc formats (enum)
5
+ # --all # Only used in usage (long)
6
+ # -a # Only used in usage (short)
7
+ #
8
+ # Option group rendering
9
+ # -a, --all -b, --beta # Only used in brief formats (enum)
10
+ # --all --beta # Used in usage (long)
11
+ # -a -b # Used in usage (short)
12
+ #
13
+ # -a, --all # Only used in doc format (:multi)
14
+ # -b, --beta
15
+ #
16
+ # Command rendering
17
+ # cmd --all --beta [cmd1|cmd2] ARG1 ARG2 # Single-line formats (:single)
18
+ # cmd --all --beta [cmd1|cmd2] ARGS...
19
+ # cmd -a -b [cmd1|cmd2] ARG1 ARG2
20
+ # cmd -a -b [cmd1|cmd2] ARGS...
21
+ #
22
+ # cmd -a -b [cmd1|cmd2] ARG1 ARG2 # One line for each argument description (:enum)
23
+ # cmd -a -b [cmd1|cmd2] ARG3 ARG4 # (used in the USAGE section)
24
+ #
25
+ # cmd --all --beta # Multi-line formats (:multi)
26
+ # [cmd1|cmd2] ARG1 ARG2
27
+ # cmd --all --beta
28
+ # <commands> ARGS
29
+ #
30
+ module ShellOpts
31
+ module Grammar
32
+ class Option
33
+ # Formats:
34
+ #
35
+ # :enum -a, --all
36
+ # :long --all
37
+ # :short -a
38
+ #
39
+ def render(format)
40
+ constrain format, :enum, :long, :short
41
+ case format
42
+ when :enum; names.join(", ")
43
+ when :long; name
44
+ when :short; short_names.first || name
45
+ else
46
+ raise ArgumentError, "Illegal format: #{format.inspect}"
47
+ end + (argument? ? "=#{argument_name}" : "")
48
+ end
49
+ end
50
+
51
+ class OptionGroup
52
+ # Formats:
53
+ #
54
+ # :enum -a, --all -r, --recursive
55
+ # :long --all --recursive
56
+ # :short -a -r
57
+ # :multi -a, --all
58
+ # -r, --recursive
59
+ #
60
+ def render(format)
61
+ constrain format, :enum, :long, :short, :multi
62
+ if format == :multi
63
+ options.map { |option| option.render(:enum) }.join("\n")
64
+ else
65
+ options.map { |option| option.render(format) }.join(" ")
66
+ end
67
+ end
68
+ end
69
+
70
+ # brief one-line commands should optionally use compact options
71
+ class Command
72
+ using Ext::Array::Wrap
73
+
74
+ OPTIONS_ABBR = "[OPTIONS]"
75
+ COMMANDS_ABBR = "[COMMANDS]"
76
+ DESCRS_ABBR = "ARGS..."
77
+
78
+ # Format can be one of :single, :enum, or :multi. :single force one-line
79
+ # output and compacts options and commands if needed. :enum outputs a
80
+ # :single line for each argument specification/description, :multi tries
81
+ # one-line output but wrap options if needed. Multiple argument
82
+ # specifications/descriptions are always compacted
83
+ #
84
+ def render(format, width, root: false, **opts)
85
+ case format
86
+ when :single; render_single(width, **opts)
87
+ when :enum; render_enum(width, **opts)
88
+ when :multi; render_multi2(width, **opts)
89
+ else
90
+ raise ArgumentError, "Illegal format: #{format.inspect}"
91
+ end
92
+ end
93
+
94
+ def names(root: false)
95
+ (root ? ancestors : []) + [self]
96
+ end
97
+
98
+ protected
99
+ # Force one line. Compact options, commands, arguments if needed
100
+ def render_single(width, args: nil)
101
+ long_options = options.map { |option| option.render(:long) }
102
+ short_options = options.map { |option| option.render(:short) }
103
+ compact_options = options.empty? ? [] : [OPTIONS_ABBR]
104
+ short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
105
+ compact_commands = commands.empty? ? [] : [COMMANDS_ABBR]
106
+
107
+ # TODO: Refactor and implement recursive detection of any argument
108
+ args ||=
109
+ case descrs.size
110
+ when 0; args = []
111
+ when 1; [descrs.first.text]
112
+ else [DESCRS_ABBR]
113
+ end
114
+
115
+ begin # to be able to use 'break' below
116
+ words = [name] + long_options + short_commands + args
117
+ break if pass?(words, width)
118
+ words = [name] + short_options + short_commands + args
119
+ break if pass?(words, width)
120
+ words = [name] + long_options + compact_commands + args
121
+ break if pass?(words, width)
122
+ words = [name] + short_options + compact_commands + args
123
+ break if pass?(words, width)
124
+ words = [name] + compact_options + short_commands + args
125
+ break if pass?(words, width)
126
+ words = [name] + compact_options + compact_commands + args
127
+ break if pass?(words, width)
128
+ words = [name] + compact_options + compact_commands + [DESCRS_ABBR]
129
+ end while false
130
+ words.join(" ")
131
+ end
132
+
133
+ # Render one line for each argument specification/description
134
+ def render_enum(width)
135
+ # TODO: Also refactor args here
136
+ args_texts = self.descrs.empty? ? [""] : descrs.map(&:text)
137
+ args_texts.map { |args_text| render_single(width, args: [args_text]) }
138
+ end
139
+
140
+ # Render the description using the given method (:single, :multi)
141
+ def render_descr(method, width, descr)
142
+ send.send method, width, args: descr
143
+ end
144
+
145
+ # Try to keep on one line but wrap options if needed. Multiple argument
146
+ # specifications/descriptions are always compacted
147
+ def render_multi(width, args: nil)
148
+ long_options = options.map { |option| option.render(:long) }
149
+ short_options = options.map { |option| option.render(:short) }
150
+ short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
151
+ compact_commands = [COMMANDS_ABBR]
152
+ args ||= self.descrs.size != 1 ? [DESCRS_ABBR] : descrs.map(&:text)
153
+
154
+ # On one line
155
+ words = long_options + short_commands + args
156
+ return [words.join(" ")] if pass?(words, width)
157
+ words = short_options + short_commands + args
158
+ return [words.join(" ")] if pass?(words, width)
159
+
160
+ # On multiple lines
161
+ options = long_options.wrap(width)
162
+ commands = [[short_commands, args].join(" ")]
163
+ return options + commands if pass?(commands, width)
164
+ options + [[compact_commands, args].join(" ")]
165
+ end
166
+
167
+ # Try to keep on one line but wrap options if needed. Multiple argument
168
+ # specifications/descriptions are always compacted
169
+ def render_multi2(width, args: nil)
170
+ long_options = options.map { |option| option.render(:long) }
171
+ short_options = options.map { |option| option.render(:short) }
172
+ short_commands = commands.empty? ? [] : ["[#{commands.map(&:name).join("|")}]"]
173
+ compact_commands = [COMMANDS_ABBR]
174
+
175
+ # TODO: Refactor and implement recursive detection of any argument
176
+ args ||=
177
+ case descrs.size
178
+ when 0; args = []
179
+ when 1; [descrs.first.text]
180
+ else [DESCRS_ABBR]
181
+ end
182
+
183
+ # On one line
184
+ words = [name] + long_options + short_commands + args
185
+ return [words.join(" ")] if pass?(words, width)
186
+ words = [name] + short_options + short_commands + args
187
+ return [words.join(" ")] if pass?(words, width)
188
+
189
+ # On multiple lines
190
+ lead = name + " "
191
+ options = long_options.wrap(width - lead.size)
192
+ options = [lead + options[0]] + indent_lines(lead.size, options[1..-1])
193
+
194
+ begin
195
+ words = short_commands + args
196
+ break if pass?(words, width)
197
+ words = compact_commands + args
198
+ break if pass?(words, width)
199
+ words = compact_commands + [DESCRS_ABBR]
200
+ end while false
201
+
202
+ cmdargs = words.empty? ? [] : [words.join(" ")]
203
+ options + indent_lines(lead.size, cmdargs)
204
+ end
205
+
206
+ protected
207
+ # Helper method that returns true if words can fit in width characters
208
+ def pass?(words, width)
209
+ words.sum(&:size) + words.size - 1 <= width
210
+ end
211
+
212
+ # Indent array of lines
213
+ def indent_lines(indent, lines)
214
+ indent = [indent, 0].max
215
+ lines.map { |line| ' ' * indent + line }
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+
222
+
223
+
224
+
225
+
226
+
227
+
@@ -0,0 +1,7 @@
1
+ module ShellOpts
2
+ module Stack
3
+ refine Array do
4
+ def top() last end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+
2
+ module ShellOpts
3
+ class Token
4
+ # Each kind should have a corresponding Grammar class with the same name
5
+ KINDS = [
6
+ :program, :section, :option, :command, :spec, :argument, :usage,
7
+ :usage_string, :brief, :text, :blank
8
+ ]
9
+
10
+ # Kind of token
11
+ attr_reader :kind
12
+
13
+ # Line number (one-based)
14
+ attr_reader :lineno
15
+
16
+ # Char number (one-based). The lexer may adjust the char number (eg. for
17
+ # blank lines)
18
+ attr_accessor :charno
19
+
20
+ # Source of the token
21
+ attr_reader :source
22
+
23
+ def initialize(kind, lineno, charno, source)
24
+ constrain kind, :program, *KINDS
25
+ @kind, @lineno, @charno, @source = kind, lineno, charno, source
26
+ end
27
+
28
+ forward_to :source, :to_s, :empty?
29
+
30
+ def pos(start_lineno = 1, start_charno = 1)
31
+ "#{start_lineno + lineno - 1}:#{start_charno + charno - 1}"
32
+ end
33
+
34
+ def to_s() source end
35
+
36
+ def inspect()
37
+ "<#{self.class.to_s.sub(/.*::/, "")} #{pos} #{kind.inspect} #{source.inspect}>"
38
+ end
39
+
40
+ def dump
41
+ puts "#{kind}@#{lineno}:#{charno} #{source.inspect}"
42
+ end
43
+ end
44
+ end
@@ -1,3 +1,3 @@
1
1
  module ShellOpts
2
- VERSION = "2.0.0.pre.14"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/shellopts.rb CHANGED
@@ -1,3 +1,358 @@
1
+
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'
26
+ require 'shellopts/parser.rb'
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
+
35
+ module ShellOpts
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
47
+ end
48
+
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
122
+
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
137
+
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
148
+
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
157
+
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
168
+
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
184
+
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
194
+ end
195
+
196
+ # Print usage
197
+ def usage() Formatter.usage(@grammar) end
198
+
199
+ # Print brief help
200
+ def brief() Formatter.brief(@grammar) end
201
+
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
210
+
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
290
+
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
301
+ end
302
+
303
+ def self.process(spec, argv, msgopts: false, **opts)
304
+ msgopts ||= Messages.is_included?
305
+ ShellOpts.process(spec, argv, msgopts: msgopts, **opts)
306
+ end
307
+
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
340
+ end
341
+
342
+ module ErrorHandling
343
+ # TODO: Set up global exception handlers
344
+ end
345
+ end
346
+
347
+
348
+
349
+
350
+
351
+
352
+
353
+
354
+ __END__
355
+
1
356
  require "shellopts/version"
2
357
 
3
358
  require "ext/algorithm.rb"
@@ -38,7 +393,7 @@ module ShellOpts
38
393
  attr_reader :program
39
394
  attr_reader :arguments
40
395
 
41
- def initialize(spec, argv, name: nil)
396
+ def initialize(spec, argv, name: nil, exception: false)
42
397
  @name = name || File.basename($PROGRAM_NAME)
43
398
  @spec, @argv = spec, argv.dup
44
399
  exprs = Grammar::Lexer.lex(@spec)
@@ -48,6 +403,7 @@ module ShellOpts
48
403
  begin
49
404
  @program, @arguments = Ast::Parser.parse(@grammar, @argv)
50
405
  rescue Error => ex
406
+ raise if exception
51
407
  error(ex.subject, ex.message)
52
408
  end
53
409
  end
@@ -95,8 +451,8 @@ module ShellOpts
95
451
  end
96
452
  end
97
453
 
98
- def self.process(spec, argv, name: nil)
99
- $shellopts = ShellOpts.new(spec, argv, name: name)
454
+ def self.process(spec, argv, name: nil, exception: false)
455
+ $shellopts = ShellOpts.new(spec, argv, name: name, exception: exception)
100
456
  [$shellopts.program, $shellopts.arguments]
101
457
  end
102
458