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.



197
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
224
225
226
227
# File 'lib/cli-dispatcher.rb', line 197

def self.add_structured_commands
  def help_explain
    return <<~EOF
      Displays an explanation of a Structured class.

      Use this to assist in generating or checking a Rubric file.
    EOF
  end

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

  def help_template
    return <<~EOF
      Produces a template for the given Structured class.
    EOF
  end

  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
end

Instance Method Details

#add_options(opts) ⇒ Object

Given an OptionParser object, add options. By default, this method does nothing. The usage of this method, in contrast to #setup_options, is to override this method, invoking calls to the opts argument to add options. The method will be called automatically when the Dispatcher is invoked.



249
250
# File 'lib/cli-dispatcher.rb', line 249

def add_options(opts)
end

#cmd_explain(class_name) ⇒ Object



206
207
208
209
210
211
212
# File 'lib/cli-dispatcher.rb', line 206

def cmd_explain(class_name)
  c = Object.const_get(class_name)
  unless c.is_a?(Class) && c.include?(Structured)
    raise "Invalid class #{class_name}"
  end
  c.explain
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



220
221
222
223
224
225
226
# File 'lib/cli-dispatcher.rb', line 220

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 = <<~EOF
    Usage: #{File.basename($0)} [options] command [arguments...]
    Run '#{File.basename($0)} help' for a list of commands.

    Options:
  EOF
  @option_parser.on_tail('-h', '--help', 'Show this help') do
    warn(@option_parser)
    warn("\nCommands:")
    cmd_help
    exit 1
  end

  @option_parser.parse!
  if !ARGV.empty?
    exit dispatch(*ARGV) ? 0 : 1
  elsif default_interactive
    cmd_interactive
  else
    STDERR.puts(@option_parser)
    exit 1
  end
end

#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



198
199
200
201
202
203
204
# File 'lib/cli-dispatcher.rb', line 198

def help_explain
  return <<~EOF
    Displays an explanation of a Structured class.

    Use this to assist in generating or checking a Rubric file.
  EOF
end

#help_helpObject



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

def help_help
  return <<~EOF
  Displays help on commands.

  Run 'help [command]' for further help on that command.
  EOF
end

#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



214
215
216
217
218
# File 'lib/cli-dispatcher.rb', line 214

def help_template
  return <<~EOF
    Produces a template for the given Structured class.
  EOF
end

#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

#setup_optionsObject

Receives options, passing them to OptionParser. The options are processed when dispatch_argv is called. The usage of this method is that after the Dispatcher object is created, this method is called to instantiate the options for the class. See #add_options for another way of doing this.

The banner and -h/–help options will be added automatically.



237
238
239
240
241
# File 'lib/cli-dispatcher.rb', line 237

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