Class: NeverBounce::CLI::Script::Meaningful Abstract

Inherits:
Base
  • Object
show all
Defined in:
lib/never_bounce/cli/script/meaningful.rb

Overview

This class is abstract.

A meaningful base script class. Features:

  • Handle command-line options.

  • Handle envars.

  • Handle boilerplate actions like printing usage on `–help`.

Direct Known Subclasses

RequestMaker

Instance Attribute Summary collapse

Attributes inherited from Base

#argv, #env, #stderr, #stdout

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#env_falsey?, #env_truthy?, env_value_truthy?, #system, #verbose?

Instance Attribute Details


27
28
29
# File 'lib/never_bounce/cli/script/meaningful.rb', line 27

def banner_text
  @banner_text ||= "#{manifest.name} - #{manifest.function}"
end

#envar_textString


32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/never_bounce/cli/script/meaningful.rb', line 32

def envar_text
  @envar_text ||= begin
    max_width = (envars = self.class.envars).map(&:name).map(&:size).max
    envars.sort_by { |_| [_.mandatory?? 0 : 1, _.name] }.map do |r|
      "%s %-#{max_width}s - %s%s" % [
        (r.mandatory?? "*" : "-"),
        r.name,
        r.comment,
        (s = self.class.format_envar_examples(r)) ? " (#{s})" : "",
      ]
    end.join("\n")
  end
end

#help_textObject


58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/never_bounce/cli/script/meaningful.rb', line 58

def help_text
  @help_text ||= begin
    [
      banner_text,
      "",
      "USAGE: #{manifest.name} #{manifest.cmdline}",
      "",
      options_text,
      "",
      "Environment variables:",
      envar_text,
    ].join("\n")
  end
end

#manifestManicest

This method is abstract.

Program manifest object. Successors should return it.

Raises:

  • (NotImplementedError)

191
192
193
# File 'lib/never_bounce/cli/script/meaningful.rb', line 191

def manifest
  raise NotImplementedError, "Redefine `manifest` in your class: #{self.class}"
end

#options_textObject


123
124
125
# File 'lib/never_bounce/cli/script/meaningful.rb', line 123

def options_text
  @options_text ||= option_parser.help.strip
end

Class Method Details

.error_klassesArray

Note:

Should be a few very high-level excaptions which you fully control.

Exception classes which are rescued fromc in #main and printed to user.


49
50
51
# File 'lib/never_bounce/cli/script/meaningful.rb', line 49

def self.error_klasses
  [Error]
end

.format_envar_examples(envar) ⇒ String

Format an envar examples string.

format_examples_string(envar)   # => ""a", *"B""

153
154
155
156
157
158
159
160
161
162
163
# File 'lib/never_bounce/cli/script/meaningful.rb', line 153

def self.format_envar_examples(envar)
  return nil if envar.examples.empty?

  envar.examples.map do |v|
    if envar.default and v == envar.default
      "[" + v.inspect + "]"
    else
      v.inspect
    end
  end.join(", ")
end

Instance Method Details

#call_slim_main(from_level = 3) ⇒ Integer (private)

Invoke one of the slim_main methods available in self.

call_slim_main(3)   # Try <tt>slim_main3</tt>, then <tt>slim_main2</tt> down to <tt>slim_main</tt>.

134
135
136
137
138
139
140
141
142
143
# File 'lib/never_bounce/cli/script/meaningful.rb', line 134

def call_slim_main(from_level = 3)
  from_level.downto(0) do |i|
    if respond_to?(m = "slim_main#{i > 0 ? i : ''}")
      return send(m)
    end
  end

  # This is in theory possible, stay sane.
  raise "No `slim_main` responded, check your class hierarchy"
end

#handle_help_and_optionsInteger?

Note:

See method source for important details.

Handle help request and invalid options.


169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/never_bounce/cli/script/meaningful.rb', line 169

def handle_help_and_options
  # NOTES:
  #
  # * Due to `OptionParser` specifics we don't use on-the-fly handling of options.
  #   We do it procedurally instead, to avoid running into a circular dependency.
  #   Thus, it's okay to call this method directly in specs as long as `argv` is concerned.
  # * The method has internal protection to ensure it's called just ones to make speccing a bit easier.
  igetset(:handle_help_and_options) do
    if help?
      # We ignore errors if there's a help request.
      stdout.puts help_text
      0
    elsif (ar = options[:errors])
      stderr.puts ar
      1
    end
  end
end

#help?Boolean

true if help has been requested via command-line options.


54
55
56
# File 'lib/never_bounce/cli/script/meaningful.rb', line 54

def help?
  !!options[:help]
end

#mainInteger

Main routine which handles most boilerplate.

See Also:


198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/never_bounce/cli/script/meaningful.rb', line 198

def main
  # Handle top-level errors like `UsageError`.
  begin
    if (res = handle_help_and_options)
      return res
    end

    # Invoke dump'n'exit, if `def dx` defined.
    # NOTE: This code is production-compatible. It's active only if final script responds to `dx`, which is strictly debug-time.
    if respond_to? :dx and env_truthy? "DX"
      dx
      return 1
    end

    # Do it.
    result = call_slim_main

    # Help us stay sane.
    raise "Unknown `slim_main` result: #{result.inspect}" if not result.is_a? Integer

    result
  rescue *self.class.error_klasses => e
    stderr.puts "#{e.class.to_s.split('::').last}: #{e.message}"    # Like "UsageError: this and that".
    1
  end
end

#option_parserObject

Our OptionParser object.


118
119
120
121
# File 'lib/never_bounce/cli/script/meaningful.rb', line 118

def option_parser
  options if not @options
  @option_parser
end

#optionsHash

Note:

This isn't an attribute and there's no writer for it. For simulation and testing we use Base#argv=.

Parse command-line options with OptionParser.

options   # => {:help => true}

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/never_bounce/cli/script/meaningful.rb', line 80

def options
  @options ||= begin
    h = {}

    @option_parser = OptionParser.new do |opts|
      opts.banner = ""    # A hack to remove Ruby's "-e".

      opts.on("-h", "--help", "Show help information") do
        h[:help] = true
      end
    end

    rmn_options = begin
      @option_parser.parse!(argv)
    rescue OptionParser::ParseError => e
      (h[:errors] ||= []) << "Error: #{e.message}"
      retry
    end

    # Parse and add "KEY=value" options to environment.
    rmn_options.reject! do |s|
      if s =~ /^(\w+)=(.*)$/m     # NOTE: `/m` allows for multiline options.
        env[$1] = $2
        true
      end
    end

    # Treat remaining options as errors.
    # If this behaviour changes, we should provide access to `rmn_options` via method.
    rmn_options.each do |s|
      (h[:errors] ||= []) << "Error: unexpected option: #{s}"
    end

    h
  end
end

#slim_mainInteger

This method is abstract.

Slim or “real” main routine of the successor class. Called by #main considering all boilerplate has been taken care of.

Raises:

  • (NotImplementedError)

229
230
231
# File 'lib/never_bounce/cli/script/meaningful.rb', line 229

def slim_main
  raise NotImplementedError, "Redefine `slim_main` in your class: #{self.class}"
end