Class: Dispatcher

Inherits:
Object
  • Object
show all
Defined in:
lib/cli-dispatcher.rb

Overview

Constructs a program that can operate a number of user-provided commands. To use this class, subclass it and define methods of the form:

def cmd_name(args...)

Then create an instance of the class and call one of the dispatch methods, typically:

[DispatcherSubclass].new.dispatch_argv

To provide help and/or a category for a command, define the methods

def help_name; [string] end
def cat_name;  [string] end

For the help command, the first line should be a short description of the command, which will be used in a summary table describing the command. For the category, all commands with the same category will be grouped together in the help summary display.

This class incorporates optparse, providing the commands setup_options and add_options to pass through options specifications.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.add_structured_commandsObject

Adds commands relevant when this dispatcher uses Structured data inputs. This method allows for generating documentation on Structured classes.

The calling class should implement a method explain_classes that lists at least one Structured class that can be explained.



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/cli-dispatcher.rb', line 201

def self.add_structured_commands
  def help_explain
    return "      Displays an explanation of a Structured class.\n\n      This will produce documentation on the elements permitted within the\n      class. If no class is given, then a listing of known Structured classes\n      is produced.\n    EOF\n  end\n\n  def cmd_explain(class_name = nil)\n    if class_name.nil?\n      cache = {}\n      puts \"The following are Structured classes known to this program. Run\"\n      puts \"the command \\\"explain [class]\\\" for documentation on any of them.\"\n      puts\n      explain_classes.each do |c|\n        list_classes(c, cache)\n      end\n      return\n    end\n\n    c = Object.const_get(class_name)\n    if c.is_a?(Class) && c.include?(Structured)\n      c.explain\n    else\n      raise \"Invalid class \#{class_name}\"\n    end\n  end\n\n  unless defined? explain_classes\n    def explain_classes\n      return []\n    end\n  end\n\n  def list_classes(c, cache, level = 0)\n    unless c.include?(Structured)\n      raise \"Cannot list classes of a non-Structured class\"\n    end\n    puts((\"  \" * level) + c.to_s)\n    return if cache.include?(c)\n    cache[c] = true\n    c.subtypes.each do |sc|\n      list_classes(sc, cache, level + 1)\n    end\n  end\n\n  def help_template\n    return <<~EOF\n      Produces a template for the given Structured class.\n    EOF\n  end\n\n  def cmd_template(class_name)\n    c = Object.const_get(class_name)\n    unless c.is_a?(Class) && c.include?(Structured)\n      raise(\"Invalid class \#{class_name}\")\n    end\n    puts c.template\n  end\nend\n"

Instance Method Details

#add_options(opts) ⇒ Object

Adds command-line options for this class. By default, this method does nothing. Subclasses may override this method to add options. The method will be automatically invoked during a dispatch_argv call, thereby constructing an OptionParser object to handle the command-line arguments.

The argument to this method is an OptionParser object, to which the desired options may be added. The banner and -h/–help options will be added automatically to the OptionParser object.



293
294
# File 'lib/cli-dispatcher.rb', line 293

def add_options(opts)
end

#cmd_explain(class_name = nil) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/cli-dispatcher.rb', line 212

def cmd_explain(class_name = nil)
  if class_name.nil?
    cache = {}
    puts "The following are Structured classes known to this program. Run"
    puts "the command \"explain [class]\" for documentation on any of them."
    puts
    explain_classes.each do |c|
      list_classes(c, cache)
    end
    return
  end

  c = Object.const_get(class_name)
  if c.is_a?(Class) && c.include?(Structured)
    c.explain
  else
    raise "Invalid class #{class_name}"
  end
end

#cmd_help(cmd = nil, all: true) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/cli-dispatcher.rb', line 121

def cmd_help(cmd = nil, all: true)

  if cmd
    warn("")
    warn(help_string(cmd))
    warn("")
    exit(1)
  end

  warn("Run 'help [command]' for further help on that command.")
  warn("")

  grouped_methods = methods.map { |m|
    s = m.to_s
    s.start_with?("cmd_") ? s.delete_prefix("cmd_") : nil
  }.compact.group_by { |m|
    cat_m = "cat_#{m}".to_sym
    respond_to?(cat_m) ? send(cat_m) : nil
  }
  help_cmds(grouped_methods.delete(nil)) if grouped_methods.include?(nil)

  grouped_methods.sort.each do |cat, cmds|
    warn("\n#{cat}\n\n")
    help_cmds(cmds)
  end
end

#cmd_interactiveObject

Runs the dispatcher in interactive mode, in which command lines are read from a prompt.



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/cli-dispatcher.rb', line 166

def cmd_interactive
  stty_save = `stty -g`.chomp
  loop do
    begin
      buf = Reline.readline(interactive_prompt, true)
      exit unless buf
      args = buf.shellsplit
      next if args.empty?
      exit if args.first == 'exit'
      dispatch(*args)
    rescue Interrupt
      system("stty", stty_save)
      exit
    rescue
      STDERR.puts $!.full_message
    end
  end
end

#cmd_template(class_name) ⇒ Object



256
257
258
259
260
261
262
# File 'lib/cli-dispatcher.rb', line 256

def cmd_template(class_name)
  c = Object.const_get(class_name)
  unless c.is_a?(Class) && c.include?(Structured)
    raise("Invalid class #{class_name}")
  end
  puts c.template
end

#dispatch(cmd, *args) ⇒ Object

Dispatches a single command with given arguments. If the command is not found, then issues a help warning. Returns true or false depending on whether the command executed successfully.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/cli-dispatcher.rb', line 68

def dispatch(cmd, *args)
  cmd_sym = "cmd_#{cmd}".to_sym
  begin
    if respond_to?(cmd_sym)
      send(cmd_sym, *args)
      return true
    else
      warn("Unknown command #{cmd_sym}. Run 'help' for a list of commands.")
      return false
    end
  rescue ArgumentError
    if $!.backtrace_locations.first.base_label == cmd_sym.to_s
      warn("#{cmd}: wrong number of arguments.")
      warn("Usage: #{signature_string(cmd)}")
    else
      raise
    end
    return false
  end
end

#dispatch_argv(default_interactive = false) ⇒ Object

Reads ARGV and dispatches a command. If no arguments are given, an appropriate warning is issued and the program terminates.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/cli-dispatcher.rb', line 36

def dispatch_argv(default_interactive = false)
  @option_parser ||= OptionParser.new
  add_options(@option_parser)
  @option_parser.banner = "    Usage: \#{File.basename($0)} [options] command [arguments...]\n    Run '\#{File.basename($0)} help' for a list of commands.\n\n    Options:\n  EOF\n  @option_parser.on_tail('-h', '--help', 'Show this help') do\n    warn(@option_parser)\n    warn(\"\\nCommands:\")\n    cmd_help\n    exit 1\n  end\n\n  @option_parser.parse!\n  if !ARGV.empty?\n    exit dispatch(*ARGV) ? 0 : 1\n  elsif default_interactive\n    cmd_interactive\n  else\n    STDERR.puts(@option_parser)\n    exit 1\n  end\nend\n"

#help_cmds(cmds) ⇒ Object



148
149
150
151
152
153
154
155
156
# File 'lib/cli-dispatcher.rb', line 148

def help_cmds(cmds)
  cmds.sort.each do |cmd|
    warn(TextTools.line_break(
      help_string(cmd, all: false),
      prefix: " " * 12,
      first_prefix: cmd.ljust(11) + ' ',
    ))
  end
end

#help_explainObject



202
203
204
205
206
207
208
209
210
# File 'lib/cli-dispatcher.rb', line 202

def help_explain
  return "    Displays an explanation of a Structured class.\n\n    This will produce documentation on the elements permitted within the\n    class. If no class is given, then a listing of known Structured classes\n    is produced.\n  EOF\nend\n"

#help_helpObject



113
114
115
116
117
118
119
# File 'lib/cli-dispatcher.rb', line 113

def help_help
  return "  Displays help on commands.\n\n  Run 'help [command]' for further help on that command.\n  EOF\nend\n"

#help_interactiveObject



158
159
160
# File 'lib/cli-dispatcher.rb', line 158

def help_interactive
  return "Start an interactive shell for entering commands."
end

#help_string(cmd, all: true) ⇒ Object



89
90
91
92
93
94
95
96
97
# File 'lib/cli-dispatcher.rb', line 89

def help_string(cmd, all: true)
  cmd_sym = "help_#{cmd}".to_sym
  return signature_string(cmd) unless respond_to?(cmd_sym)
  if all
    return $0 + " " + signature_string(cmd) + "\n\n" + send(cmd_sym)
  else
    return send(cmd_sym).to_s.split("\n", 2).first
  end
end

#help_templateObject



250
251
252
253
254
# File 'lib/cli-dispatcher.rb', line 250

def help_template
  return "    Produces a template for the given Structured class.\n  EOF\nend\n"

#interactive_promptObject

Returns the string for the interactive prompt. Subclasses can override this method to offer more detailed prompts.



189
190
191
# File 'lib/cli-dispatcher.rb', line 189

def interactive_prompt
  return "#{File.basename($0)}> "
end

#list_classes(c, cache, level = 0) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
# File 'lib/cli-dispatcher.rb', line 238

def list_classes(c, cache, level = 0)
  unless c.include?(Structured)
    raise "Cannot list classes of a non-Structured class"
  end
  puts(("  " * level) + c.to_s)
  return if cache.include?(c)
  cache[c] = true
  c.subtypes.each do |sc|
    list_classes(sc, cache, level + 1)
  end
end

#setup_optionsObject

Creates an OptionParser object for this Dispatcher. The options for the OptionParser are defined in a block passed to this method. The block receives one argument, which is the OptionParser object being created.

The banner and -h/–help options will be added automatically to the created OptionParser object.

For a slightly simpler way to set up options for this Dispatcher object, see the add_options method.



277
278
279
280
281
# File 'lib/cli-dispatcher.rb', line 277

def setup_options
  @option_parser = OptionParser.new do |opts|
    yield(opts)
  end
end

#signature_string(cmd) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/cli-dispatcher.rb', line 99

def signature_string(cmd)
  cmd_sym = "cmd_#{cmd}".to_sym
  raise "No such command" unless respond_to?(cmd_sym)
  return cmd + " " + method(cmd_sym).parameters.map { |type, name|
    case type
    when :req then name.to_s
    when :opt then "[#{name}]"
    when :rest then "*#{name}"
    when :keyreq, :key, :keyrest, :block then nil
    else raise "Unknown parameter type #{type}"
    end
  }.compact.join(" ")
end