shellopts 2.0.0.pre.14 → 2.0.0

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 +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