Class: RotorMachine::Shell

Inherits:
Object
  • Object
show all
Defined in:
lib/rotor_machine/shell.rb

Overview

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

Usage

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

Constant Summary collapse

COMMANDS =

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

{
  rotor: ["Add a rotor to the machine", "<kind> [position] [step_size]", "add_rotor"],
  reflector: ["Set the machine's reflector", "<kind> [position]", nil],
  connect: ["Connect two letters on the plugboard", "<from> <to>", "plug"],
  disconnect: ["Disconnnect a letter (and its inverse) on the plugboard", "<letter>", "unplug"],
  default_machine: ["Set the machine to its default configuration", nil, nil],
  empty_machine: ["Set the machine to its empty configuration", nil, nil],
  last_result: ["Print the result of the last encipherment operation", nil, nil],
  configuration: ["Print out the machine's current state", nil, "current_state,config"],
  set_positions: ["Set the rotor positions", "<positions>", "set_rotors"],
  clear_rotors: ["Clear the current rotor set", nil, nil],
  clear_plugboard: ["Clear the current plugboard configuration", nil, nil],
  help: ["Display help", "[command]", nil],
  version: ["Display the version of the rotor_machine library", nil, nil],
  about_prompt: ["Information about the shell prompt", nil, nil],
  about_rotors: ["List the names of available rotors", nil, nil],
  about_reflectors: ["List the names of available reflectors", nil, nil],
}.freeze
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.

{
  encipher: ["Encode a message", "[message]", "cipher,encode"],
  quit: ["Quit the application", nil, "exit"],
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeShell

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



52
53
54
55
# File 'lib/rotor_machine/shell.rb', line 52

def initialize()
  @session = RotorMachine::Session.new({})
  @session.default_machine
end

Class Method Details

.repl(commands = nil) ⇒ Object

Helper for instantiating a new REPL.

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.

Parameters:

  • commands (Array) (defaults to: nil)

    If provided, the commands passed in will be executed



456
457
458
# File 'lib/rotor_machine/shell.rb', line 456

def self.repl(commands=nil)
  RotorMachine::Shell.new().repl(commands)
end

Instance Method Details

#about_prompt(arglist) ⇒ Object

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.



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# 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) ⇒ Object



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# 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) ⇒ Object



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# 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) ⇒ Object

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



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# 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

Display the startup banner for the REPL application.



362
363
364
365
366
367
368
369
370
371
# File 'lib/rotor_machine/shell.rb', line 362

def banner
  puts "******************************************************************************".colorize(color: :white, background: :magenta)
  puts "*     rotor_machine: Simple simulation of the German WWII Enigma Machine     *".colorize(color: :white, background: :magenta)
  puts "*                   By Tammy Cravit <[email protected]>                     *".colorize(color: :white, background: :magenta)
  puts "*                http://github.com/tammycravit/rotor_machine                 *".colorize(color: :white, background: :magenta)
  puts "******************************************************************************".colorize(color: :white, background: :magenta)
  puts ""
  puts "rotor_machine version #{RotorMachine::VERSION}. Type 'help' for help. <Tab> to complete commands.".colorize(color: :magenta)
  puts ""
end

#clear_plugboard(args) ⇒ String

Wrapper around RotorMachine::Session#clear_plugboard. Arglist is ignored.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



201
202
203
204
# 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) ⇒ String

Wrapper around RotorMachine::Session#clear_rotors. Arglist is ignored.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



192
193
194
195
# File 'lib/rotor_machine/shell.rb', line 192

def clear_rotors(args)
  @session.clear_rotors
  "Removed all rotors from the machine"
end

#configuration(args) ⇒ String Also known as: current_state, config

Print out the current configuration of the machine. Arglist is ignored.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



170
171
172
# File 'lib/rotor_machine/shell.rb', line 170

def configuration(args)
  @session.the_machine.to_s
end

#connect(arglist) ⇒ String

Wrapper around RotorMachine::Session#connect. Expects from and to letters in the arglist.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



112
113
114
115
116
117
118
# 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

#default_machine(args) ⇒ String

Wrapper around RotorMachine::Session#default_machine. Arglist is ignored.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



144
145
146
147
# File 'lib/rotor_machine/shell.rb', line 144

def default_machine(args)
  @session.default_machine
  "Reset machine to default configuration"
end

#disconnect(arglist) ⇒ String

Wrapper around RotorMachine::Session#disconnect. Expects the letter to disconnect in the arglist.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



125
126
127
128
129
# 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) ⇒ String

Wrapper around RotorMachine::Session#empty_machine. Arglist is ignored.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



153
154
155
156
# File 'lib/rotor_machine/shell.rb', line 153

def empty_machine(args)
  @session.empty_machine
  "Reset machine to empty configuration"
end

#encipher(arglist) ⇒ String

Wrapper around RotorMachine::Session#encipher. Expects the text to encipher in the arglist.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



136
137
138
# File 'lib/rotor_machine/shell.rb', line 136

def encipher(arglist)
  @session.encipher(arglist)
end

#help(args) ⇒ Object

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.



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


247
248
249
250
251
252
253
254
255
256
257
# 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) ⇒ String

Wrapper around RotorMachine::Session#last_result. Arglist is ignored.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



162
163
164
# File 'lib/rotor_machine/shell.rb', line 162

def last_result(args)
  @session.last_result
end

#process_command_input(input) ⇒ Symbol

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.

depending on whether the command was recognized and executed.

Parameters:

  • input (String)

    The command line provided by the REPL (or the test).

Returns:

  • (Symbol)

    One of :SUCCESS, :EXCEPTION, :ARITY, :INVALID_COMMAND



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# 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_promptObject

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.



295
296
297
298
299
300
301
302
# 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) ⇒ String

Wrapper around RotorMachine::Session#reflector. Expects reflector kind and position in the arglist.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
# 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) ⇒ Object

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.

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.

Parameters:

  • commands (Array) (defaults to: nil)

    If provided, the commands passed in will be executed



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# 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) ⇒ String

Wrapper around RotorMachine::Session#rotor. Expects rotor kind, position and step size in the arglist.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# 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_stateObject

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



240
241
242
# 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) ⇒ String Also known as: set_rotors

Wrapper around RotorMachine::Session#set_positions. Expects a string specifying the rotor positions in arglist.

Parameters:

  • arglist (Array)

    Array of arguments provided from the REPL

Returns:

  • (String)

    A confirmation message, or an error message on failure.



181
182
183
184
185
# 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

#the_machineObject



373
374
375
# File 'lib/rotor_machine/shell.rb', line 373

def the_machine
  return @session.the_machine
end

#the_sessionObject



377
378
379
# File 'lib/rotor_machine/shell.rb', line 377

def the_session
  return @session
end

#verbsObject

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



281
282
283
# File 'lib/rotor_machine/shell.rb', line 281

def verbs
  COMMANDS.merge(EXTERNAL_COMMANDS)
end

#version(args) ⇒ Object

Print the version number of the rotor_machine.



232
233
234
# File 'lib/rotor_machine/shell.rb', line 232

def version(args)
  "rotor_machine version #{RotorMachine::VERSION}"
end