class RotorMachine::Shell

Provide an interactive REPL for manipulating a {RotorMachine::Session} to create and interact with a rotor machine.

Usage

shell = RotorMachine::Shell.new()
shell.repl()

Constants

COMMANDS

Shell command map. Each command in this list corresponds to a method in the {RotorMachine::Shell class}. The key is the name of the command, and the value is an array listing [description, arguments, aliases].

EXTERNAL_COMMANDS

Shell “external command” map. This functions the same as the {COMMANDS} list, except for commands that are internal to the REPL and are implemented within the logic of the {#repl} method.

Public Class Methods

new() click to toggle source

Initialize a new {RotorMachine::Shell} instance, and the interior {RotorMachine::Session} object which the shell manages.

# File lib/rotor_machine/shell.rb, line 52
def initialize()
  @session = RotorMachine::Session.new({})
  @session.default_machine
end
repl(commands=nil) click to toggle source

Helper for instantiating a new REPL.

@param commands [Array] If provided, the commands passed in will be executed in sequence. This is mainly intended for RSpec testing. If no commands are passed in, the interactive REPL loop (with Readline) will be launched instead.

# File lib/rotor_machine/shell.rb, line 456
def self.repl(commands=nil)
  RotorMachine::Shell.new().repl(commands)
end

Public Instance Methods

about_prompt(arglist) click to toggle source

Display the about help for the REPL prompt. If you redefine the {#readline_prompt} method, you should also redefine this to reflect the new prompt.

# File lib/rotor_machine/shell.rb, line 307
def about_prompt(arglist)
  #:nocov:
  puts ""
  puts "The prompt for the shell is in the following format:"
  puts ""
  puts "     [XXX] <YYY> ZZZ>"
  puts ""
  puts "The components of the prompt are as follows:"
  puts ""
  puts "     XXX - the number of rotors mounted to the machine"
  puts "     YYY - the number of connections on the plugboard"
  puts "     ZZZ - the current positions of the rotors"
  ""
  #:nocov:
end
about_reflectors(arglist) click to toggle source
# File lib/rotor_machine/shell.rb, line 323
def about_reflectors(arglist)
  #:nocov:
  puts ""
  puts "The following reflectors are available with this machine:"
  puts ""
  RotorMachine::Reflector.constants.each { |r| puts "     #{r.to_s.colorize(color: :light_blue)}" }
  puts ""
  puts "To set the reflector for the machine, use a command like this:"
  puts ""
  puts "    Specify reflector:       #{'reflector REFLECTOR_A'.colorize(color: :light_blue)}"
  puts "    Specify reflector/pos:   #{'reflector REFLECTOR_A 13'.colorize(color: :light_blue)}"
  puts ""
  puts "The REPL does not currently support custom reflectors."
  puts ""
  ""
  #:nocov:
end
about_rotors(arglist) click to toggle source
# File lib/rotor_machine/shell.rb, line 341
def about_rotors(arglist)
  #:nocov:
  puts ""
  puts "The following rotors are available with this machine:"
  puts ""
  RotorMachine::Rotor.constants.each { |r| puts "     #{r.to_s.colorize(color: :light_blue)}" }
  puts ""
  puts "To add a rotor to the machine, use a command like this:"
  puts ""
  puts "    Specify rotor         :  #{'rotor ROTOR_I'.colorize(color: :light_blue)}"
  puts "    Specify rotor/pos     :  #{'rotor ROTOR_I 13'.colorize(color: :light_blue)}"
  puts "                             #{'rotor ROTOR_I Q'.colorize(color: :light_blue)}"
  puts "    Specify rotor/pos/step:  #{'rotor ROTOR_I 13 2'.colorize(color: :light_blue)}"
  puts ""
  puts "The REPL does not currently support custom rotors."
  ""
  #:nocov:
end
arity(cmd) click to toggle source

Return the “arity” (in this case, the number of mandatory arguments) for a command or its alias.

# File lib/rotor_machine/shell.rb, line 262
def arity(cmd)
  if COMMANDS.keys.include?(cmd)
    return COMMANDS[cmd][1].split(' ').select { |x| x.start_with?("<") }.count
  else
    COMMANDS.each do |k, v|
      unless v[2].nil?
        v[2].split(',').each do |a|
          if a.to_sym == cmd.to_sym
            return v[1].split(' ').select { |x| x.start_with?("<") }.count
          end
        end
      end
    end
  end
  return 0
end
banner() click to toggle source

Display the startup banner for the REPL application.

clear_plugboard(args) click to toggle source

Wrapper around {RotorMachine::Session#clear_plugboard}. Arglist is ignored. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 201
def clear_plugboard(args)
  @session.clear_plugboard
  "Removed all connections from the plugboard"
end
clear_rotors(args) click to toggle source

Wrapper around {RotorMachine::Session#clear_rotors}. Arglist is ignored. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 192
def clear_rotors(args)
  @session.clear_rotors
  "Removed all rotors from the machine"
end
config(args)
Alias for: configuration
configuration(args) click to toggle source

Print out the current configuration of the machine. Arglist is ignored. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 170
def configuration(args)
  @session.the_machine.to_s
end
Also aliased as: current_state, config
connect(arglist) click to toggle source

Wrapper around {RotorMachine::Session#connect}. Expects from and to letters in the arglist. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 112
def connect(arglist)
  from = arglist[0]
  to = arglist[1]

  @session.connect(from.upcase, to.upcase)
  "Connected #{from.upcase} to #{to.upcase} on plugboard"
end
current_state(args)
Alias for: configuration
default_machine(args) click to toggle source

Wrapper around {RotorMachine::Session#default_machine}. Arglist is ignored. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 144
def default_machine(args)
  @session.default_machine
  "Reset machine to default configuration"
end
disconnect(arglist) click to toggle source

Wrapper around {RotorMachine::Session#disconnect}. Expects the letter to disconnect in the arglist. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 125
def disconnect(arglist)
  letter = arglist[0]
  @session.disconnect(letter.upcase)
  "Disconnected #{letter} and its inverse on plugboard"
end
empty_machine(args) click to toggle source

Wrapper around {RotorMachine::Session#empty_machine}. Arglist is ignored. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 153
def empty_machine(args)
  @session.empty_machine
  "Reset machine to empty configuration"
end
encipher(arglist) click to toggle source

Wrapper around {RotorMachine::Session#encipher}. Expects the text to encipher in the arglist. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 136
def encipher(arglist)
  @session.encipher(arglist)
end
help(args) click to toggle source

Print command help. If an argument is specified in the first position of the arglist, help about that specific command is printed. If no argument is supplied, a list of commands is printed instead.

# File lib/rotor_machine/shell.rb, line 210
    def help(args)
      if args[0].nil? || args[0].empty?
        <<~HEREDOC

        #{verbs.keys.sort.collect { |k| "#{k}#{' ' *(20-k.length)} #{verbs[k][0]}" }.join("\n")}
        HEREDOC
      else
        cmd_info = verbs[args[0].to_sym]

        <<~HEREDOC

        #{args[0]}: #{cmd_info[0]}

        Usage  : #{args[0]} #{cmd_info[1]}
        Aliases: #{cmd_info[2] || "none"}

        HEREDOC
      end
    end
is_internal_verb?(cmd) click to toggle source

Check if `cmd` is included on the list of internal command verbs or is an alias for an internal verb.

# File lib/rotor_machine/shell.rb, line 247
def is_internal_verb?(cmd)
  aliases = []

  COMMANDS.each do |k, v|
    unless  v[2].nil?
      v[2].split(',').each { |a| aliases << a.to_sym  }
    end
  end

  COMMANDS.keys.include?(cmd) || aliases.include?(cmd)
end
last_result(args) click to toggle source

Wrapper around {RotorMachine::Session#last_result}. Arglist is ignored. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 162
def last_result(args)
  @session.last_result
end
process_command_input(input) click to toggle source

Process a single command from the REPL and display its output.

This method is called for all commands except for “quit” and “exit”, which are processed by {#repl} directly.

@param input [String] The command line provided by the REPL (or the test). @return [Symbol] One of :SUCCESS, :EXCEPTION, :ARITY, :INVALID_COMMAND depending on whether the command was recognized and executed.

# File lib/rotor_machine/shell.rb, line 392
def process_command_input(input)
  begin
    unless input.empty?
      toks = input.tokenize
      cmd = toks.shift.downcase.strip

      if ['cipher', 'encipher', 'encode'].include?(cmd)
        message = toks.join(' ')
        puts self.encipher(message).colorize(color: :white).bold
      elsif self.is_internal_verb?(cmd.to_sym)
        begin
          if toks.length >= arity(cmd.to_sym)
            if cmd == "last_result"
              puts self.send(cmd.to_sym, toks).colorize(color: :white).bold
            else
              puts self.send(cmd.to_sym, toks).colorize(color: :green)
            end
          else
            puts "Command #{cmd} requires at least #{arity(cmd.to_sym)} arguments".colorize(color: :red)
          end
        end
      else
        puts "Unknown command: #{cmd}".colorize(color: :light_red).bold
      end
    end
  rescue StandardError => e
    puts "Rescued exception: #{e}".colorize(color: :red)
  end
end
readline_prompt() click to toggle source

Build the Readline prompt for the rotor machine. By default, displays the following pieces of information:

- Count of rotors mounted to the machine
- Count of connections on the plugboard
- Current selected letters on the rotors

If you redefine this method, you should also redefine the {#about_prompt} method to describe the new prompt correctly.

# File lib/rotor_machine/shell.rb, line 295
def readline_prompt
  [
    "[#{@session.the_machine.rotors.count}]".colorize(color: :light_blue),
    "<#{@session.the_machine.plugboard.connections.count}>".colorize(color: :light_blue),
    "#{rotor_state}".colorize(color: :light_blue),
    "> ".colorize(color: :white),
  ].join(" ")
end
reflector(arglist) click to toggle source

Wrapper around {RotorMachine::Session#reflector}. Expects reflector kind and position in the arglist. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 92
def reflector(arglist)
  kind = arglist[0].to_sym

  if arglist[1].nil? || (arglist[1].is_a?(String) && arglist[1].empty?)
    position = 0
  elsif arglist[1].is_a?(String) && arglist[1].is_number?
    position = arglist[1].to_i
  else
    position = arglist[1]
  end

  @session.reflector(kind, position)
  "Set reflector of kind #{kind}"
end
repl(commands=nil) click to toggle source

Provide an interactive REPL for manipulating the Rotor Machine. Essentially this REPL is an interactive wrapper around the {RotorMachine::Session} object, with tab completion and command history provided by the {Readline} library.

@param commands [Array] If provided, the commands passed in will be executed in sequence. This is mainly intended for RSpec testing. If no commands are passed in, the interactive REPL loop (with Readline) will be launched instead.

# File lib/rotor_machine/shell.rb, line 430
def repl(commands=nil)
  Readline.completion_append_character = " "
  Readline.completion_proc = proc { |s| verbs.keys.grep(/^#{Regexp.escape(s)}/)  }

  banner

  if commands.nil? || commands.empty?
    #:nocov:
    while input = Readline.readline(readline_prompt, true).strip
      if ['exit', 'quit'].include?(input.downcase)
        return
      end
      process_command_input(input)
    end
    #:nocov:
  else
    commands.each { |cmd| process_command_input(cmd) }
  end
end
rotor(arglist) click to toggle source

Wrapper around {RotorMachine::Session#rotor}. Expects rotor kind, position and step size in the arglist. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 64
def rotor(arglist)
  kind = arglist[0].to_sym

  if arglist[1].nil? || (arglist[1].is_a?(String) && arglist[1].empty?)
    position = 0
  elsif arglist[1].is_a?(String) && arglist[1].is_number?
    position = arglist[1].to_i
  else
    position = arglist[1]
  end

  if arglist[2].nil? || (arglist[2].is_a?(String) && arglist[2].empty?)
    step_size = 1
  elsif arglist[2].is_a?(String) && arglist[2].is_number?
    step_size = arglist[2].to_i
  else
    step_size = 1
  end

  @session.rotor(kind, position, step_size)
  "Added rotor #{@session.the_machine.rotors.count} of kind #{kind}"
end
rotor_state() click to toggle source

Return the selected letters on each of the rotors in the machine.

# File lib/rotor_machine/shell.rb, line 240
def rotor_state
  @session.the_machine.rotors.collect { |r| r.letters[r.position]  }.join("")
end
set_positions(arglist) click to toggle source

Wrapper around {RotorMachine::Session#set_positions}. Expects a string specifying the rotor positions in arglist. @param arglist [Array] Array of arguments provided from the REPL @return [String] A confirmation message, or an error message on failure.

# File lib/rotor_machine/shell.rb, line 181
def set_positions(arglist)
  pos_string = arglist[0]
  @session.set_positions(pos_string)
  "Set rotors; rotor state is now #{rotor_state}"
end
Also aliased as: set_rotors
set_rotors(arglist)
Alias for: set_positions
the_machine() click to toggle source
# File lib/rotor_machine/shell.rb, line 373
def the_machine
  return @session.the_machine
end
the_session() click to toggle source
# File lib/rotor_machine/shell.rb, line 377
def the_session
  return @session
end
verbs() click to toggle source

Return the combined list of command verbs and their arguments/usage.

# File lib/rotor_machine/shell.rb, line 281
def verbs
  COMMANDS.merge(EXTERNAL_COMMANDS)
end
version(args) click to toggle source

Print the version number of the rotor_machine.

# File lib/rotor_machine/shell.rb, line 232
def version(args)
  "rotor_machine version #{RotorMachine::VERSION}"
end