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
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
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
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
# 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
# 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
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
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
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
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
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
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
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
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
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
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
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
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 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
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
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
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
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
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
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
# File lib/rotor_machine/shell.rb, line 373 def the_machine return @session.the_machine end
# File lib/rotor_machine/shell.rb, line 377 def the_session return @session end
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
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