shellopts 2.0.0.pre.14 → 2.0.2

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 +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 +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 +360 -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.2"
3
3
  end
data/lib/shellopts.rb CHANGED
@@ -1,3 +1,359 @@
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/args.rb'
25
+ require 'shellopts/lexer.rb'
26
+ require 'shellopts/argument_type.rb'
27
+ require 'shellopts/parser.rb'
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
+
36
+ module ShellOpts
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
48
+ end
49
+
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
123
+
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
138
+
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
149
+
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
158
+
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
196
+
197
+ # Print usage
198
+ def usage() Formatter.usage(@grammar) end
199
+
200
+ # Print brief help
201
+ def brief() Formatter.brief(@grammar) end
202
+
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)
210
+ end
211
+
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
233
+
234
+ def find_caller_file
235
+ caller.reverse.select { |line| line !~ /^\s*#{__FILE__}:/ }.last.sub(/:.*/, "").sub(/^\.\//, "")
236
+ end
237
+
238
+ def self.compare_lines(text, spec)
239
+ return true if text == spec
240
+ return true if text =~ /[#\$\\]/
241
+ false
242
+ end
243
+
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
291
+
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
302
+ end
303
+
304
+ def self.process(spec, argv, msgopts: false, **opts)
305
+ msgopts ||= Messages.is_included?
306
+ ShellOpts.process(spec, argv, msgopts: msgopts, **opts)
307
+ end
308
+
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
341
+ end
342
+
343
+ module ErrorHandling
344
+ # TODO: Set up global exception handlers
345
+ end
346
+ end
347
+
348
+
349
+
350
+
351
+
352
+
353
+
354
+
355
+ __END__
356
+
1
357
  require "shellopts/version"
2
358
 
3
359
  require "ext/algorithm.rb"
@@ -38,7 +394,7 @@ module ShellOpts
38
394
  attr_reader :program
39
395
  attr_reader :arguments
40
396
 
41
- def initialize(spec, argv, name: nil)
397
+ def initialize(spec, argv, name: nil, exception: false)
42
398
  @name = name || File.basename($PROGRAM_NAME)
43
399
  @spec, @argv = spec, argv.dup
44
400
  exprs = Grammar::Lexer.lex(@spec)
@@ -48,6 +404,7 @@ module ShellOpts
48
404
  begin
49
405
  @program, @arguments = Ast::Parser.parse(@grammar, @argv)
50
406
  rescue Error => ex
407
+ raise if exception
51
408
  error(ex.subject, ex.message)
52
409
  end
53
410
  end
@@ -95,8 +452,8 @@ module ShellOpts
95
452
  end
96
453
  end
97
454
 
98
- def self.process(spec, argv, name: nil)
99
- $shellopts = ShellOpts.new(spec, argv, name: name)
455
+ def self.process(spec, argv, name: nil, exception: false)
456
+ $shellopts = ShellOpts.new(spec, argv, name: name, exception: exception)
100
457
  [$shellopts.program, $shellopts.arguments]
101
458
  end
102
459