shellopts 2.0.0.pre.14 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
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