shellopts 1.0.0 → 2.0.0.pre.7

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.
@@ -0,0 +1,48 @@
1
+
2
+ module ShellOpts
3
+ # Specialization of Array for arguments lists. Args extends Array with a
4
+ # #extract and an #expect method to extract elements from the array. The
5
+ # methods call #error() in response to errors
6
+ class Args < Array
7
+ def initialize(shellopts, *args)
8
+ @shellopts = shellopts
9
+ super(*args)
10
+ end
11
+
12
+ # Remove and return elements from beginning of the array. If
13
+ # +count_or_range+ is a number, that number of elements will be returned.
14
+ # If the count is one, a simple value is returned instead of an array. If
15
+ # the count is negative, the elements will be removed from the end of the
16
+ # array. If +count_or_range+ is a range, the number of elements returned
17
+ # will be in that range. The range can't contain negative numbers #expect
18
+ # calls #error() if there's is not enough elements in the array to satisfy
19
+ # the request
20
+ def extract(count_or_range, message = nil)
21
+ if count_or_range.is_a?(Range)
22
+ range = count_or_range
23
+ range.min <= self.size or inoa(message)
24
+ n_extract = [self.size, range.max].min
25
+ n_extend = range.max > self.size ? range.max - self.size : 0
26
+ r = self.shift(n_extract) + Array.new(n_extend)
27
+ else
28
+ count = count_or_range
29
+ self.size >= count.abs or inoa(message)
30
+ start = count >= 0 ? 0 : size + count
31
+ r = slice!(start, count.abs)
32
+ r.size == 0 ? nil : (r.size == 1 ? r.first : r)
33
+ end
34
+ end
35
+
36
+ # As extract except it doesn't allow negative counts and that the array is
37
+ # expect to be emptied by the operation
38
+ def expect(count_or_range, message = nil)
39
+ count_or_range === self.size or inoa(message)
40
+ extract(count_or_range) # Can't fail
41
+ end
42
+
43
+ private
44
+ def inoa(message = nil)
45
+ raise ShellOpts::UserError, message || "Illegal number of arguments"
46
+ end
47
+ end
48
+ end
@@ -7,17 +7,17 @@ module ShellOpts
7
7
 
8
8
  # Optional sub-command (Ast::Command). Initially nil but assigned by the
9
9
  # parser
10
- attr_accessor :command
10
+ attr_accessor :subcommand
11
11
 
12
12
  def initialize(grammar, name)
13
13
  super(grammar, name)
14
14
  @options = []
15
- @command = nil
15
+ @subcommand = nil
16
16
  end
17
17
 
18
18
  # Array of option or command tuples
19
19
  def values
20
- (options + (Array(command || []))).map { |node| node.to_tuple }
20
+ (options + (Array(subcommand || []))).map { |node| node.to_tuple }
21
21
  end
22
22
 
23
23
  # :nocov:
@@ -26,10 +26,10 @@ module ShellOpts
26
26
  yield if block_given?
27
27
  puts "options:"
28
28
  indent { options.each { |opt| opt.dump } }
29
- print "command:"
30
- if command
29
+ print "subcommand:"
30
+ if subcommand
31
31
  puts
32
- indent { command.dump }
32
+ indent { subcommand.dump }
33
33
  else
34
34
  puts "nil"
35
35
  end
@@ -23,7 +23,7 @@ module ShellOpts
23
23
  end
24
24
 
25
25
  # Return either a value (option value), an array of values (command), or
26
- # nil (option without a value). Should be defined in sub-classes
26
+ # nil (option without a value). It must be defined in sub-classes of Ast::Node
27
27
  def values() raise end
28
28
 
29
29
  # :nocov:
@@ -8,12 +8,12 @@ require 'shellopts/grammar/program.rb'
8
8
  module ShellOpts
9
9
  module Grammar
10
10
  # Compiles an option definition string and returns a Grammar::Program
11
- # object. program_name is the name of the program and source is the
11
+ # object. name is the name of the program and source is the
12
12
  # option definition string
13
- def self.compile(program_name, source)
14
- program_name.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{program_name.class}"
13
+ def self.compile(name, source)
14
+ name.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{name.class}"
15
15
  source.is_a?(String) or raise Compiler::Error, "Expected String argument, got #{source.class}"
16
- Compiler.new(program_name, source).call
16
+ Compiler.new(name, source).call
17
17
  end
18
18
 
19
19
  # Service object for compiling an option definition string. Returns a
@@ -26,14 +26,14 @@ module ShellOpts
26
26
  class Error < RuntimeError; end
27
27
 
28
28
  # Initialize a Compiler object. source is the option definition string
29
- def initialize(program_name, source)
30
- @program_name, @tokens = program_name, source.split(/\s+/).reject(&:empty?)
29
+ def initialize(name, source)
30
+ @name, @tokens = name, source.split(/\s+/).reject(&:empty?)
31
31
 
32
- # @commands_by_path is an hash from command-path to Command or Program
32
+ # @subcommands_by_path is an hash from subcommand-path to Command or Program
33
33
  # object. The top level Program object has nil as its path.
34
- # @commands_by_path is used to check for uniqueness of commands and to
35
- # link sub-commands to their parents
36
- @commands_by_path = {}
34
+ # @subcommands_by_path is used to check for uniqueness of subcommands and to
35
+ # link sub-subcommands to their parents
36
+ @subcommands_by_path = {}
37
37
  end
38
38
 
39
39
  def call
@@ -49,30 +49,26 @@ module ShellOpts
49
49
  # Returns the current token and advance to the next token
50
50
  def next_token() @tokens.shift end
51
51
 
52
- def error(msg) # Just a shorthand. Unrelated to ShellOpts.error
53
- raise Compiler::Error.new(msg)
54
- end
55
-
56
52
  def compile_program
57
- program = @commands_by_path[nil] = Grammar::Program.new(@program_name, compile_options)
53
+ program = @subcommands_by_path[nil] = Grammar::Program.new(@name, compile_options)
58
54
  while curr_token && curr_token != "--"
59
- compile_command
55
+ compile_subcommand
60
56
  end
61
57
  program.args.concat(@tokens[1..-1]) if curr_token
62
58
  program
63
59
  end
64
60
 
65
- def compile_command
61
+ def compile_subcommand
66
62
  path = curr_token[0..-2]
67
63
  ident_list = compile_ident_list(path, ".")
68
64
  parent_path = ident_list.size > 1 ? ident_list[0..-2].join(".") : nil
69
65
  name = ident_list[-1]
70
66
 
71
- parent = @commands_by_path[parent_path] or
72
- error "No such command: #{parent_path.inspect}"
73
- !@commands_by_path.key?(path) or error "Duplicate command: #{path.inspect}"
67
+ parent = @subcommands_by_path[parent_path] or
68
+ raise Compiler::Error, "No such subcommand: #{parent_path.inspect}"
69
+ !@subcommands_by_path.key?(path) or raise Compiler::Error, "Duplicate subcommand: #{path.inspect}"
74
70
  next_token
75
- @commands_by_path[path] = Grammar::Command.new(parent, name, compile_options)
71
+ @subcommands_by_path[path] = Grammar::Command.new(parent, name, compile_options)
76
72
  end
77
73
 
78
74
  def compile_options
@@ -81,7 +77,7 @@ module ShellOpts
81
77
  option_list << compile_option
82
78
  end
83
79
  dup = option_list.map(&:names).flatten.find_dup and
84
- error "Duplicate option name: #{dup.inspect}"
80
+ raise Compiler::Error, "Duplicate option name: #{dup.inspect}"
85
81
  option_list
86
82
  end
87
83
 
@@ -102,7 +98,7 @@ module ShellOpts
102
98
  long_names = []
103
99
  ident_list = compile_ident_list(names, ",")
104
100
  (dup = ident_list.find_dup).nil? or
105
- error "Duplicate identifier #{dup.inspect} in #{curr_token.inspect}"
101
+ raise Compiler::Error, "Duplicate identifier #{dup.inspect} in #{curr_token.inspect}"
106
102
  ident_list.each { |ident|
107
103
  if ident.size == 1
108
104
  short_names << "-#{ident}"
@@ -115,13 +111,15 @@ module ShellOpts
115
111
  Grammar::Option.new(short_names, long_names, flags, label)
116
112
  end
117
113
 
118
- # Compile list of option names or a command path
114
+ # Compile list of option names or a subcommand path
119
115
  def compile_ident_list(ident_list_str, sep)
120
116
  ident_list_str.split(sep, -1).map { |str|
121
- !str.empty? or error "Empty identifier in #{curr_token.inspect}"
122
- !str.start_with?("-") or error "Identifier can't start with '-' in #{curr_token.inspect}"
117
+ !str.empty? or
118
+ raise Compiler::Error, "Empty identifier in #{curr_token.inspect}"
119
+ !str.start_with?("-") or
120
+ raise Compiler::Error, "Identifier can't start with '-' in #{curr_token.inspect}"
123
121
  str !~ /([^\w\d#{sep}-])/ or
124
- error "Illegal character #{$1.inspect} in #{curr_token.inspect}"
122
+ raise Compiler::Error, "Illegal character #{$1.inspect} in #{curr_token.inspect}"
125
123
  str
126
124
  }
127
125
  end
@@ -0,0 +1,15 @@
1
+
2
+ require 'shellopts/idr.rb'
3
+
4
+ module ShellOpts
5
+ module Idr
6
+ # Generates an Idr::Program from a ShellOpts object
7
+ def self.generate(shellopts)
8
+ Idr::Program.new(shellopts)
9
+ end
10
+ end
11
+ end
12
+
13
+
14
+
15
+
@@ -11,32 +11,50 @@ module ShellOpts
11
11
  # Name of command (String). Name doesn't include the exclamation point ('!')
12
12
  attr_reader :name
13
13
 
14
- # Hash from option names (both short and long names) to option. This
15
- # means an option can occur more than once as the hash value
16
- attr_reader :options
17
-
18
- # Sub-commands of this command. Is a hash from sub-command name to command object
19
- attr_reader :commands
14
+ # Same as #name. TODO Define in Grammar::Node instead
15
+ alias :key_name :name
20
16
 
21
17
  # List of options in declaration order
22
18
  attr_reader :option_list
23
19
 
24
20
  # List of commands in declaration order
25
- attr_reader :command_list
21
+ attr_reader :subcommand_list
22
+
23
+ # Multihash from option key or names (both short and long names) to option. This
24
+ # means an option can occur more than once as the hash value
25
+ def options()
26
+ @option_multihash ||= @option_list.flat_map { |option|
27
+ option.identifiers.map { |ident| [ident, option] }
28
+ }.to_h
29
+ end
30
+
31
+ # Sub-commands of this command. Is a multihash from sub-command key or
32
+ # name to command object. Lazily constructed because subcommands are added
33
+ # after initialization
34
+ def subcommands()
35
+ @subcommand_multihash ||= @subcommand_list.flat_map { |subcommand|
36
+ subcommand.identifiers.map { |name| [name, subcommand] }
37
+ }.to_h
38
+ end
26
39
 
27
40
  # Initialize a Command object. parent is the parent Command object or nil
28
41
  # if this is the root object. name is the name of the command (without
29
42
  # the exclamation mark), and option_list a list of Option objects
30
43
  def initialize(parent, name, option_list)
31
- super("#{name}!".to_sym)
32
- @name = name
44
+ super("#{name}!".to_sym, name)
33
45
  parent.attach(self) if parent
34
46
  @option_list = option_list
35
- @options = @option_list.flat_map { |opt| opt.names.map { |name| [name, opt] } }.to_h
36
- @commands = {}
37
- @command_list = []
47
+ @subcommand_list = []
38
48
  end
39
49
 
50
+ # Return key for the identifier
51
+ def identifier2key(ident)
52
+ options[ident]&.key || subcommands[ident]&.key
53
+ end
54
+
55
+ # Return list of identifiers for the command
56
+ def identifiers() [key, name] end
57
+
40
58
  # :nocov:
41
59
  def dump(&block)
42
60
  puts "#{key.inspect}"
@@ -46,17 +64,16 @@ module ShellOpts
46
64
  yield if block_given?
47
65
  puts "options:"
48
66
  indent { option_list.each { |opt| opt.dump } }
49
- puts "commands: "
50
- indent { command_list.each { |cmd| cmd.dump } }
67
+ puts "subcommands: "
68
+ indent { subcommand_list.each { |cmd| cmd.dump } }
51
69
  }
52
70
  end
53
71
  # :nocov:
54
72
 
55
73
  protected
56
- def attach(command)
57
- command.instance_variable_set(:@parent, self)
58
- @commands[command.name] = command
59
- @command_list << command
74
+ def attach(subcommand)
75
+ subcommand.instance_variable_set(:@parent, self)
76
+ @subcommand_list << subcommand
60
77
  end
61
78
  end
62
79
  end
@@ -10,14 +10,22 @@ module ShellOpts
10
10
  # Key (Symbol) of node. Unique within the enclosing command
11
11
  attr_reader :key
12
12
 
13
- def initialize(key)
14
- @key = key
13
+ # Name of node. The name of an option is without the prefixed '-' or
14
+ # '--', the name of a command is without the suffixed '!'. Note that name
15
+ # collisions can happen between options and commands names
16
+ attr_reader :name
17
+
18
+ def initialize(key, name)
19
+ @key, @name = key, name
15
20
  end
16
21
 
17
22
  # :nocov:
18
23
  def dump(&block)
19
24
  puts key.inspect
20
- indent { yield } if block_given?
25
+ indent {
26
+ puts "name: #{name.inspect}"
27
+ yield if block_given?
28
+ }
21
29
  end
22
30
  # :nocov:
23
31
  end
@@ -10,6 +10,9 @@ module ShellOpts
10
10
  # List of long names (incl. '--')
11
11
  attr_reader :long_names
12
12
 
13
+ # Name of the key attribute (eg. if key is :all then key_name is '--all'
14
+ attr_reader :key_name
15
+
13
16
  # List of flags (Symbol)
14
17
  def flags() @flags.keys end
15
18
 
@@ -23,7 +26,9 @@ module ShellOpts
23
26
  # there's no :string flag, it's status is inferred. label is the optional
24
27
  # informal name of the option argument (eg. 'FILE') or nil if not present
25
28
  def initialize(short_names, long_names, flags, label = nil)
26
- super((long_names.first || short_names.first).sub(/^-+/, "").to_sym)
29
+ @key_name = long_names.first || short_names.first
30
+ name = @key_name.sub(/^-+/, "")
31
+ super(name.to_sym, name)
27
32
  @short_names, @long_names = short_names, long_names
28
33
  @flags = flags.map { |flag| [flag, true] }.to_h
29
34
  @label = label
@@ -32,6 +37,12 @@ module ShellOpts
32
37
  # Array of option names with short names first and then the long names
33
38
  def names() @short_names + @long_names end
34
39
 
40
+ # Array of names and the key
41
+ def identifiers() names + [key] end
42
+
43
+ # Return true if +ident+ is equal to any name or to key
44
+ def match?(ident) names.include?(ident) || ident == key end
45
+
35
46
  # Flag query methods. Returns true if the flag is present and otherwise nil
36
47
  def repeated?() @flags[:repeated] || false end
37
48
  def argument?() @flags[:argument] || false end
@@ -17,7 +17,7 @@ module ShellOpts
17
17
  def usage
18
18
  (
19
19
  render_options(option_list) +
20
- commands.values.map { |cmd| render_command(cmd) } +
20
+ subcommand_list.map { |cmd| render_subcommand(cmd) } +
21
21
  args
22
22
  ).flatten.join(" ")
23
23
  end
@@ -32,9 +32,9 @@ module ShellOpts
32
32
  # :nocov:
33
33
 
34
34
  private
35
- def render_command(command)
36
- [command.name] + render_options(command.option_list) +
37
- command.commands.values.map { |cmd| render_command(cmd) }.flatten
35
+ def render_subcommand(subcommand)
36
+ [subcommand.name] + render_options(subcommand.option_list) +
37
+ subcommand.subcommand_list.map { |cmd| render_subcommand(cmd) }.flatten
38
38
  end
39
39
 
40
40
  def render_options(options)
@@ -0,0 +1,236 @@
1
+
2
+ module ShellOpts
3
+ # Idr models the Internal Data Representation of a program. It is the native
4
+ # representation of a command
5
+ #
6
+ # The IDR should ideally be completely detached from the compile-time grammar
7
+ # and AST but they are only hidden from view in this implementation. Create
8
+ # a Shellopts object instead to access the compiler data
9
+ #
10
+ module Idr
11
+ # Base class for the Idr class hierarchy. It is constructed from an Ast
12
+ # object by #generate. Node is modelled as an element of a hash with a key
13
+ # and a value. Options have their (optional) argument as value while
14
+ # commands use +self+ as value
15
+ class Node
16
+ # Parent node. nil for the top-level Program object
17
+ attr_reader :parent
18
+
19
+ # Unique key (within context) for the option or command. nil for the
20
+ # top-level Program object
21
+ attr_reader :key
22
+
23
+ # Name of command or option as used on the command line
24
+ attr_reader :name
25
+
26
+ # Value of node. This can be a simple value (String, Integer, or Float),
27
+ # an Array of values, or a Idr::Command object. Note that the value of a
28
+ # Command object is the object itself
29
+ #
30
+ # Repeated options are implemented as an Array with one element for each
31
+ # use of the option. The element is nil if the option doesn't take
32
+ # arguments or if an optional argument is missing.
33
+ attr_reader :value
34
+
35
+ # The top-level Program object
36
+ def program() @program ||= (parent&.program || self) end
37
+
38
+ protected
39
+ # Copy arguments into instance variables
40
+ def initialize(parent, ast, key, name, value)
41
+ @parent, @ast, @key, @name, @value = parent, ast, key, name, value
42
+ end
43
+
44
+ # The AST node for this Idr object
45
+ attr_reader :ast
46
+
47
+ # Shorthand to the grammar node for this Idr object
48
+ def grammar() @ast.grammar end
49
+ end
50
+
51
+ # Base class for Options
52
+ class Option < Node
53
+ end
54
+
55
+ class SimpleOption < Option
56
+ protected
57
+ # Initialize with defauls from the Ast. +value+ is set to true if option
58
+ # doesn't take an argument
59
+ def initialize(parent, ast)
60
+ value = ast.grammar.argument? ? ast.value : true
61
+ super(parent, ast, ast.key, ast.name, value)
62
+ end
63
+ end
64
+
65
+ # An OptionGroup models repeated options collapsed into a single key. The
66
+ # name of the group should be set to the name of the key (eg. '--all' if
67
+ # the key is :all)
68
+ class OptionGroup < Option
69
+ # Array of names of the options
70
+ attr_reader :names
71
+
72
+ # Array of values of the options
73
+ alias :values :value
74
+
75
+ # Name is set to the key name and value to an array of option values
76
+ def initialize(parent, key, name, options)
77
+ @names = options.map(&:name)
78
+ super(parent, nil, key, name, options.map(&:value))
79
+ end
80
+ end
81
+
82
+ class Command < Node
83
+ # Hash from key to options with repeated option_list collapsed into a
84
+ # option group. It also include an entry for the subcommand. Options are
85
+ # ordered by first use on the command line. The command entry will always
86
+ # be last
87
+ attr_reader :options
88
+
89
+ # List of command line options in the same order as on the command line
90
+ attr_reader :option_list
91
+
92
+ # Subcommand object. Possibly nil
93
+ attr_reader :subcommand
94
+
95
+ # True if ident is declared
96
+ def declared?(ident) option?(ident) || subcommand?(ident) end
97
+
98
+ # True if ident is declared as an option
99
+ def option?(ident) grammar.options.key?(ident) end
100
+
101
+ # True if ident is declared as a command
102
+ def subcommand?(ident) grammar.subcommands.key?(ident) end
103
+
104
+ # True if ident is present
105
+ def key?(ident)
106
+ declared?(ident) or raise InternalError, "Undefined identifier: #{ident.inspect}"
107
+ key = grammar.identifier2key(ident)
108
+ @options.key?(key)
109
+ end
110
+
111
+ # Value of ident. Repeated options are collapsed into an OptionGroup object
112
+ def [](ident)
113
+ declared?(ident) or raise InternalError, "Undefined identifier: #{ident.inspect}"
114
+ key = grammar.identifier2key(ident)
115
+ if @options.key?(key)
116
+ @options[key].value
117
+ elsif option?(key)
118
+ false
119
+ else
120
+ nil
121
+ end
122
+ end
123
+
124
+ # Apply defaults recursively. Values can be lambdas that will be evaluated to
125
+ # get the default value. TODO
126
+ def apply(defaults = {}) raise InternalError, "Not implemented" end
127
+
128
+ # Return options and command as an array
129
+ def to_a() @ast.values end
130
+
131
+ # Return options and command as a hash. The hash also define the
132
+ # singleton method #subcommand that returns the key of the subcommand
133
+ #
134
+ # +key_type+ controls the type of keys used: +:key+ (the default) use the
135
+ # symbolic key, +:name+ use #name. Note that using +:name+ can cause
136
+ # name collisions between option and command names
137
+ #
138
+ # +aliases+ maps from key to replacement key (which could be any object).
139
+ # +aliases+ can be used to avoid name collisions between options and
140
+ # commands when using key_format: :name
141
+ #
142
+ # IDEA: Add a singleton methods to the hash with #name, #usage, etc.
143
+ #
144
+ def to_h(key_type: ::ShellOpts.default_key_type, aliases: {})
145
+ keys = map_keys(key_type, aliases)
146
+ value = {}
147
+ value.define_singleton_method(:subcommand) { nil }
148
+ options.values.each { |opt| # includes subcommand
149
+ key = keys[opt.key]
150
+ case opt
151
+ when Option
152
+ value[key] = opt.value
153
+ when Command
154
+ value[key] = opt.value.to_h(key_type: key_type, aliases: aliases[opt.key] || {})
155
+ value.define_singleton_method(:subcommand) { key } # Redefine
156
+ else
157
+ # :nocov:
158
+ raise InternalError, "Oops"
159
+ # :nocov:
160
+ end
161
+ }
162
+ value
163
+ end
164
+
165
+ # Return options and command as a struct
166
+ def to_struct(key_type: ::ShellOpts.default_key_type, aliases: {})
167
+ OptionStruct.new(self, key_type, aliases)
168
+ end
169
+
170
+ protected
171
+ # Initialize an Idr::Command object and all dependent objects
172
+ def initialize(parent, ast)
173
+ super(parent, ast, ast.key, ast.name, self)
174
+ @option_list = ast.options.map { |node| SimpleOption.new(self, node) }
175
+ @subcommand = Command.new(self, ast.subcommand) if ast.subcommand
176
+ @options = @option_list.group_by { |option| option.key }.map { |key, option_list|
177
+ option =
178
+ if ast.grammar.options[key].repeated?
179
+ OptionGroup.new(self, key, ast.grammar.options[key].key_name, option_list)
180
+ else
181
+ option_list.first
182
+ end
183
+ [key, option]
184
+ }.to_h
185
+ @options[subcommand.key] = @subcommand if @subcommand
186
+ end
187
+
188
+ # Internal-key to used-key map. Checks for reserved words and
189
+ # name-collisions
190
+ def map_keys(key_type, aliases, reserved_words = [])
191
+ keys = {}
192
+ used_keys = {}
193
+ (grammar.option_list + grammar.subcommand_list).each { |node|
194
+ internal_key = node.key
195
+ key = aliases[internal_key] || (key_type == :name ? node.name.to_sym : internal_key)
196
+ !reserved_words.include?(key) or
197
+ raise ::ShellOpts::ConversionError, "'#{key}' is a reserved word"
198
+ !used_keys.key?(key) or
199
+ raise ::ShellOpts::ConversionError, "Name collision between '--#{key}' and '#{key}!'"
200
+ keys[internal_key] = key
201
+ used_keys[key] = true
202
+ }
203
+ keys
204
+ end
205
+ end
206
+
207
+ class Program < Command
208
+ # Name of program
209
+ def name() @shellopts.name end
210
+ def name=(name) @shellopts.name = name end
211
+
212
+ # Usage string
213
+ def usage() @shellopts.usage end
214
+ def usage=(usage) @shellopts.usage = usage end
215
+
216
+ # #key is nil for the top-level Program object
217
+ def key() nil end
218
+
219
+ # Remaining command line arguments
220
+ def args() @shellopts.args end
221
+
222
+ # Initialize the top-level Idr::Program object
223
+ def initialize(shellopts)
224
+ @shellopts = shellopts
225
+ super(nil, shellopts.ast)
226
+ end
227
+
228
+ # Emit error message and a usage description before exiting with status 1
229
+ def error(*args) @shellopts.error(*error_messages) end
230
+
231
+ # Emit error message before exiting with status 1
232
+ def fail(*args) @shellopts.fail(*error_messages) end
233
+ end
234
+ end
235
+ end
236
+