rotor_machine 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/README.md +16 -0
- data/exe/rotor_machine +8 -1
- data/lib/rotor_machine/plugboard.rb +5 -1
- data/lib/rotor_machine/session.rb +9 -1
- data/lib/rotor_machine/shell.rb +460 -0
- data/lib/rotor_machine/string_extensions.rb +31 -1
- data/lib/rotor_machine/version.rb +1 -1
- data/rotor_machine.gemspec +1 -0
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1434f2b4192ac16aca51423849b94d8c53456fa27371d55ac97ef4df9f0d82cf
|
4
|
+
data.tar.gz: 775be6d47e8db0655ead3338289df1040cbcacdc9565e062064c9cb6a3f5e168
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8e34d8afdeb57b48c992be30ce4d042f95519d87537a5c9c6422beaa7f1ed0bd0aa63d54cfb03aae92604fd1600a5d2b0a6fe470bafe6f70327d635251486acf
|
7
|
+
data.tar.gz: d6cd87c934b973e50ac79907508a33507cdd827164b71f631ed33b0052ca39cbe192f4cfa52832bb4aa9c842dfe97a6a9270a3aab2b7e49dfa1e786de251db76
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-2.5.
|
1
|
+
ruby-2.5.3
|
data/README.md
CHANGED
@@ -189,6 +189,22 @@ After the operations in the block are executed, the `RotorMachine.Session` metho
|
|
189
189
|
will return the `RotorMachine::Session` object, which can be further reused if
|
190
190
|
needed.
|
191
191
|
|
192
|
+
## Using the `rotor_machine` REPL
|
193
|
+
|
194
|
+
The `rotor_machine` executable instantatiates an instance of the
|
195
|
+
`RotorMachine::Shell` class and then runs its `repl()` method. This creates an
|
196
|
+
interactive shell whereby you can interact with a rotor machine. The
|
197
|
+
`RotorMachine::Shell` is an interactive wrapper around `RotorMachine::Session`
|
198
|
+
with help, readline and ANSI colorization added.
|
199
|
+
|
200
|
+
Either run the `rotor_machine` executable, or run the following code to instantiate
|
201
|
+
a REPL:
|
202
|
+
|
203
|
+
`RotorMachine::Shell.new().repl`
|
204
|
+
|
205
|
+
The REPL provides interactive usage help. Type `help` at the REPL prompt for more
|
206
|
+
details.
|
207
|
+
|
192
208
|
## Documentation
|
193
209
|
|
194
210
|
The classes in
|
data/exe/rotor_machine
CHANGED
@@ -95,7 +95,11 @@ module RotorMachine
|
|
95
95
|
#
|
96
96
|
# @return [String] A description of the current state.
|
97
97
|
def to_s
|
98
|
-
|
98
|
+
seen = []
|
99
|
+
@connections.each { |k, v| seen << k.to_sym unless seen.include?(v.to_sym) }
|
100
|
+
conn = @connections.clone
|
101
|
+
conn.keys.each { |k| conn.delete(k.to_s) unless seen.include?(k.to_sym) }
|
102
|
+
"a RotorMachine::Plugboard with connections: #{conn.to_s}"
|
99
103
|
end
|
100
104
|
|
101
105
|
##
|
@@ -76,7 +76,15 @@ module RotorMachine
|
|
76
76
|
##
|
77
77
|
# Encipher a string.
|
78
78
|
def encipher(the_string="")
|
79
|
-
|
79
|
+
if the_string.nil?
|
80
|
+
res = ""
|
81
|
+
elsif the_string.is_a?(Array)
|
82
|
+
res = @machine.encipher(the_string.join(" "))
|
83
|
+
elsif the_string.is_a?(String)
|
84
|
+
res = @machine.encipher(the_string)
|
85
|
+
else
|
86
|
+
res = @machine.encipher(the_string.to_s)
|
87
|
+
end
|
80
88
|
@last_result = res
|
81
89
|
res
|
82
90
|
end
|
@@ -0,0 +1,460 @@
|
|
1
|
+
require 'readline'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module RotorMachine
|
5
|
+
##
|
6
|
+
# Provide an interactive REPL for manipulating a {RotorMachine::Session} to create
|
7
|
+
# and interact with a rotor machine.
|
8
|
+
#
|
9
|
+
# == Usage
|
10
|
+
#
|
11
|
+
# shell = RotorMachine::Shell.new()
|
12
|
+
# shell.repl()
|
13
|
+
class Shell
|
14
|
+
|
15
|
+
##
|
16
|
+
# Shell command map. Each command in this list corresponds to a method in the
|
17
|
+
# {RotorMachine::Shell class}. The key is the name of the command, and the
|
18
|
+
# value is an array listing [description, arguments, aliases].
|
19
|
+
COMMANDS =
|
20
|
+
{
|
21
|
+
rotor: ["Add a rotor to the machine", "<kind> [position] [step_size]", "add_rotor"],
|
22
|
+
reflector: ["Set the machine's reflector", "<kind> [position]", nil],
|
23
|
+
connect: ["Connect two letters on the plugboard", "<from> <to>", "plug"],
|
24
|
+
disconnect: ["Disconnnect a letter (and its inverse) on the plugboard", "<letter>", "unplug"],
|
25
|
+
default_machine: ["Set the machine to its default configuration", nil, nil],
|
26
|
+
empty_machine: ["Set the machine to its empty configuration", nil, nil],
|
27
|
+
last_result: ["Print the result of the last encipherment operation", nil, nil],
|
28
|
+
configuration: ["Print out the machine's current state", nil, "current_state,config"],
|
29
|
+
set_positions: ["Set the rotor positions", "<positions>", "set_rotors"],
|
30
|
+
clear_rotors: ["Clear the current rotor set", nil, nil],
|
31
|
+
clear_plugboard: ["Clear the current plugboard configuration", nil, nil],
|
32
|
+
help: ["Display help", "[command]", nil],
|
33
|
+
version: ["Display the version of the rotor_machine library", nil, nil],
|
34
|
+
about_prompt: ["Information about the shell prompt", nil, nil],
|
35
|
+
about_rotors: ["List the names of available rotors", nil, nil],
|
36
|
+
about_reflectors: ["List the names of available reflectors", nil, nil],
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
##
|
40
|
+
# Shell "external command" map. This functions the same as the {COMMANDS} list,
|
41
|
+
# except for commands that are internal to the REPL and are implemented within the
|
42
|
+
# logic of the {#repl} method.
|
43
|
+
EXTERNAL_COMMANDS =
|
44
|
+
{
|
45
|
+
encipher: ["Encode a message", "[message]", "cipher,encode"],
|
46
|
+
quit: ["Quit the application", nil, "exit"],
|
47
|
+
}.freeze
|
48
|
+
|
49
|
+
##
|
50
|
+
# Initialize a new {RotorMachine::Shell} instance, and the interior
|
51
|
+
# {RotorMachine::Session} object which the shell manages.
|
52
|
+
def initialize()
|
53
|
+
@session = RotorMachine::Session.new({})
|
54
|
+
@session.default_machine
|
55
|
+
end
|
56
|
+
|
57
|
+
########## shell command handlers ##########
|
58
|
+
|
59
|
+
##
|
60
|
+
# Wrapper around {RotorMachine::Session#rotor}. Expects rotor kind, position
|
61
|
+
# and step size in the arglist.
|
62
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
63
|
+
# @return [String] A confirmation message, or an error message on failure.
|
64
|
+
def rotor(arglist)
|
65
|
+
kind = arglist[0].to_sym
|
66
|
+
|
67
|
+
if arglist[1].nil? || (arglist[1].is_a?(String) && arglist[1].empty?)
|
68
|
+
position = 0
|
69
|
+
elsif arglist[1].is_a?(String) && arglist[1].is_number?
|
70
|
+
position = arglist[1].to_i
|
71
|
+
else
|
72
|
+
position = arglist[1]
|
73
|
+
end
|
74
|
+
|
75
|
+
if arglist[2].nil? || (arglist[2].is_a?(String) && arglist[2].empty?)
|
76
|
+
step_size = 1
|
77
|
+
elsif arglist[2].is_a?(String) && arglist[2].is_number?
|
78
|
+
step_size = arglist[2].to_i
|
79
|
+
else
|
80
|
+
step_size = 1
|
81
|
+
end
|
82
|
+
|
83
|
+
@session.rotor(kind, position, step_size)
|
84
|
+
"Added rotor #{@session.the_machine.rotors.count} of kind #{kind}"
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Wrapper around {RotorMachine::Session#reflector}. Expects reflector kind and
|
89
|
+
# position in the arglist.
|
90
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
91
|
+
# @return [String] A confirmation message, or an error message on failure.
|
92
|
+
def reflector(arglist)
|
93
|
+
kind = arglist[0].to_sym
|
94
|
+
|
95
|
+
if arglist[1].nil? || (arglist[1].is_a?(String) && arglist[1].empty?)
|
96
|
+
position = 0
|
97
|
+
elsif arglist[1].is_a?(String) && arglist[1].is_number?
|
98
|
+
position = arglist[1].to_i
|
99
|
+
else
|
100
|
+
position = arglist[1]
|
101
|
+
end
|
102
|
+
|
103
|
+
@session.reflector(kind, position)
|
104
|
+
"Set reflector of kind #{kind}"
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Wrapper around {RotorMachine::Session#connect}. Expects from and to letters
|
109
|
+
# in the arglist.
|
110
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
111
|
+
# @return [String] A confirmation message, or an error message on failure.
|
112
|
+
def connect(arglist)
|
113
|
+
from = arglist[0]
|
114
|
+
to = arglist[1]
|
115
|
+
|
116
|
+
@session.connect(from.upcase, to.upcase)
|
117
|
+
"Connected #{from.upcase} to #{to.upcase} on plugboard"
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Wrapper around {RotorMachine::Session#disconnect}. Expects the letter to
|
122
|
+
# disconnect in the arglist.
|
123
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
124
|
+
# @return [String] A confirmation message, or an error message on failure.
|
125
|
+
def disconnect(arglist)
|
126
|
+
letter = arglist[0]
|
127
|
+
@session.disconnect(letter.upcase)
|
128
|
+
"Disconnected #{letter} and its inverse on plugboard"
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Wrapper around {RotorMachine::Session#encipher}. Expects the text to encipher
|
133
|
+
# in the arglist.
|
134
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
135
|
+
# @return [String] A confirmation message, or an error message on failure.
|
136
|
+
def encipher(arglist)
|
137
|
+
@session.encipher(arglist)
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Wrapper around {RotorMachine::Session#default_machine}. Arglist is ignored.
|
142
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
143
|
+
# @return [String] A confirmation message, or an error message on failure.
|
144
|
+
def default_machine(args)
|
145
|
+
@session.default_machine
|
146
|
+
"Reset machine to default configuration"
|
147
|
+
end
|
148
|
+
|
149
|
+
##
|
150
|
+
# Wrapper around {RotorMachine::Session#empty_machine}. Arglist is ignored.
|
151
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
152
|
+
# @return [String] A confirmation message, or an error message on failure.
|
153
|
+
def empty_machine(args)
|
154
|
+
@session.empty_machine
|
155
|
+
"Reset machine to empty configuration"
|
156
|
+
end
|
157
|
+
|
158
|
+
##
|
159
|
+
# Wrapper around {RotorMachine::Session#last_result}. Arglist is ignored.
|
160
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
161
|
+
# @return [String] A confirmation message, or an error message on failure.
|
162
|
+
def last_result(args)
|
163
|
+
@session.last_result
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Print out the current configuration of the machine. Arglist is ignored.
|
168
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
169
|
+
# @return [String] A confirmation message, or an error message on failure.
|
170
|
+
def configuration(args)
|
171
|
+
@session.the_machine.to_s
|
172
|
+
end
|
173
|
+
alias_method :current_state, :configuration
|
174
|
+
alias_method :config, :configuration
|
175
|
+
|
176
|
+
##
|
177
|
+
# Wrapper around {RotorMachine::Session#set_positions}. Expects a string
|
178
|
+
# specifying the rotor positions in arglist[0].
|
179
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
180
|
+
# @return [String] A confirmation message, or an error message on failure.
|
181
|
+
def set_positions(arglist)
|
182
|
+
pos_string = arglist[0]
|
183
|
+
@session.set_positions(pos_string)
|
184
|
+
"Set rotors; rotor state is now #{rotor_state}"
|
185
|
+
end
|
186
|
+
alias_method :set_rotors, :set_positions
|
187
|
+
|
188
|
+
##
|
189
|
+
# Wrapper around {RotorMachine::Session#clear_rotors}. Arglist is ignored.
|
190
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
191
|
+
# @return [String] A confirmation message, or an error message on failure.
|
192
|
+
def clear_rotors(args)
|
193
|
+
@session.clear_rotors
|
194
|
+
"Removed all rotors from the machine"
|
195
|
+
end
|
196
|
+
|
197
|
+
##
|
198
|
+
# Wrapper around {RotorMachine::Session#clear_plugboard}. Arglist is ignored.
|
199
|
+
# @param arglist [Array] Array of arguments provided from the REPL
|
200
|
+
# @return [String] A confirmation message, or an error message on failure.
|
201
|
+
def clear_plugboard(args)
|
202
|
+
@session.clear_plugboard
|
203
|
+
"Removed all connections from the plugboard"
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# Print command help. If an argument is specified in the first position of the
|
208
|
+
# arglist, help about that specific command is printed. If no argument is
|
209
|
+
# supplied, a list of commands is printed instead.
|
210
|
+
def help(args)
|
211
|
+
if args[0].nil? || args[0].empty?
|
212
|
+
<<~HEREDOC
|
213
|
+
|
214
|
+
#{verbs.keys.sort.collect { |k| "#{k}#{' ' *(20-k.length)} #{verbs[k][0]}" }.join("\n")}
|
215
|
+
HEREDOC
|
216
|
+
else
|
217
|
+
cmd_info = verbs[args[0].to_sym]
|
218
|
+
|
219
|
+
<<~HEREDOC
|
220
|
+
|
221
|
+
#{args[0]}: #{cmd_info[0]}
|
222
|
+
|
223
|
+
Usage : #{args[0]} #{cmd_info[1]}
|
224
|
+
Aliases: #{cmd_info[2] || "none"}
|
225
|
+
|
226
|
+
HEREDOC
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
##
|
231
|
+
# Print the version number of the rotor_machine.
|
232
|
+
def version(args)
|
233
|
+
"rotor_machine version #{RotorMachine::VERSION}"
|
234
|
+
end
|
235
|
+
|
236
|
+
########## Internal Helper Methods ##########
|
237
|
+
|
238
|
+
##
|
239
|
+
# Return the selected letters on each of the rotors in the machine.
|
240
|
+
def rotor_state
|
241
|
+
@session.the_machine.rotors.collect { |r| r.letters[r.position] }.join("")
|
242
|
+
end
|
243
|
+
|
244
|
+
##
|
245
|
+
# Check if `cmd` is included on the list of internal command verbs or is an
|
246
|
+
# alias for an internal verb.
|
247
|
+
def is_internal_verb?(cmd)
|
248
|
+
aliases = []
|
249
|
+
|
250
|
+
COMMANDS.each do |k, v|
|
251
|
+
unless v[2].nil?
|
252
|
+
v[2].split(',').each { |a| aliases << a.to_sym }
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
COMMANDS.keys.include?(cmd) || aliases.include?(cmd)
|
257
|
+
end
|
258
|
+
|
259
|
+
##
|
260
|
+
# Return the "arity" (in this case, the number of mandatory arguments) for a
|
261
|
+
# command or its alias.
|
262
|
+
def arity(cmd)
|
263
|
+
if COMMANDS.keys.include?(cmd)
|
264
|
+
return COMMANDS[cmd][1].split(' ').select { |x| x.start_with?("<") }.count
|
265
|
+
else
|
266
|
+
COMMANDS.each do |k, v|
|
267
|
+
unless v[2].nil?
|
268
|
+
v[2].split(',').each do |a|
|
269
|
+
if a.to_sym == cmd.to_sym
|
270
|
+
return v[1].split(' ').select { |x| x.start_with?("<") }.count
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
return 0
|
277
|
+
end
|
278
|
+
|
279
|
+
##
|
280
|
+
# Return the combined list of command verbs and their arguments/usage.
|
281
|
+
def verbs
|
282
|
+
COMMANDS.merge(EXTERNAL_COMMANDS)
|
283
|
+
end
|
284
|
+
|
285
|
+
##
|
286
|
+
# Build the Readline prompt for the rotor machine. By default, displays the
|
287
|
+
# following pieces of information:
|
288
|
+
#
|
289
|
+
# - Count of rotors mounted to the machine
|
290
|
+
# - Count of connections on the plugboard
|
291
|
+
# - Current selected letters on the rotors
|
292
|
+
#
|
293
|
+
# If you redefine this method, you should also redefine the {#about_prompt}
|
294
|
+
# method to describe the new prompt correctly.
|
295
|
+
def readline_prompt
|
296
|
+
[
|
297
|
+
"[#{@session.the_machine.rotors.count}]".colorize(color: :light_blue),
|
298
|
+
"<#{@session.the_machine.plugboard.connections.count}>".colorize(color: :light_blue),
|
299
|
+
"#{rotor_state}".colorize(color: :light_blue),
|
300
|
+
"> ".colorize(color: :white),
|
301
|
+
].join(" ")
|
302
|
+
end
|
303
|
+
|
304
|
+
##
|
305
|
+
# Display the about help for the REPL prompt. If you redefine the {#readline_prompt}
|
306
|
+
# method, you should also redefine this to reflect the new prompt.
|
307
|
+
def about_prompt(arglist)
|
308
|
+
#:nocov:
|
309
|
+
puts ""
|
310
|
+
puts "The prompt for the shell is in the following format:"
|
311
|
+
puts ""
|
312
|
+
puts " [XXX] <YYY> ZZZ>"
|
313
|
+
puts ""
|
314
|
+
puts "The components of the prompt are as follows:"
|
315
|
+
puts ""
|
316
|
+
puts " XXX - the number of rotors mounted to the machine"
|
317
|
+
puts " YYY - the number of connections on the plugboard"
|
318
|
+
puts " ZZZ - the current positions of the rotors"
|
319
|
+
""
|
320
|
+
#:nocov:
|
321
|
+
end
|
322
|
+
|
323
|
+
def about_reflectors(arglist)
|
324
|
+
#:nocov:
|
325
|
+
puts ""
|
326
|
+
puts "The following reflectors are available with this machine:"
|
327
|
+
puts ""
|
328
|
+
RotorMachine::Reflector.constants.each { |r| puts " #{r.to_s.colorize(color: :light_blue)}" }
|
329
|
+
puts ""
|
330
|
+
puts "To set the reflector for the machine, use a command like this:"
|
331
|
+
puts ""
|
332
|
+
puts " Specify reflector: #{'reflector REFLECTOR_A'.colorize(color: :light_blue)}"
|
333
|
+
puts " Specify reflector/pos: #{'reflector REFLECTOR_A 13'.colorize(color: :light_blue)}"
|
334
|
+
puts ""
|
335
|
+
puts "The REPL does not currently support custom reflectors."
|
336
|
+
puts ""
|
337
|
+
""
|
338
|
+
#:nocov:
|
339
|
+
end
|
340
|
+
|
341
|
+
def about_rotors(arglist)
|
342
|
+
#:nocov:
|
343
|
+
puts ""
|
344
|
+
puts "The following rotors are available with this machine:"
|
345
|
+
puts ""
|
346
|
+
RotorMachine::Rotor.constants.each { |r| puts " #{r.to_s.colorize(color: :light_blue)}" }
|
347
|
+
puts ""
|
348
|
+
puts "To add a rotor to the machine, use a command like this:"
|
349
|
+
puts ""
|
350
|
+
puts " Specify rotor : #{'rotor ROTOR_I'.colorize(color: :light_blue)}"
|
351
|
+
puts " Specify rotor/pos : #{'rotor ROTOR_I 13'.colorize(color: :light_blue)}"
|
352
|
+
puts " #{'rotor ROTOR_I Q'.colorize(color: :light_blue)}"
|
353
|
+
puts " Specify rotor/pos/step: #{'rotor ROTOR_I 13 2'.colorize(color: :light_blue)}"
|
354
|
+
puts ""
|
355
|
+
puts "The REPL does not currently support custom rotors."
|
356
|
+
""
|
357
|
+
#:nocov:
|
358
|
+
end
|
359
|
+
|
360
|
+
##
|
361
|
+
# Display the startup banner for the REPL application.
|
362
|
+
def banner
|
363
|
+
puts "******************************************************************************".colorize(color: :white, background: :magenta)
|
364
|
+
puts "* rotor_machine: Simple simulation of the German WWII Enigma Machine *".colorize(color: :white, background: :magenta)
|
365
|
+
puts "* By Tammy Cravit <tammycravit@me.com> *".colorize(color: :white, background: :magenta)
|
366
|
+
puts "* http://github.com/tammycravit/rotor_machine *".colorize(color: :white, background: :magenta)
|
367
|
+
puts "******************************************************************************".colorize(color: :white, background: :magenta)
|
368
|
+
puts ""
|
369
|
+
puts "rotor_machine version #{RotorMachine::VERSION}. Type 'help' for help. <Tab> to complete commands.".colorize(color: :magenta)
|
370
|
+
puts ""
|
371
|
+
end
|
372
|
+
|
373
|
+
def the_machine
|
374
|
+
return @session.the_machine
|
375
|
+
end
|
376
|
+
|
377
|
+
def the_session
|
378
|
+
return @session
|
379
|
+
end
|
380
|
+
|
381
|
+
########## REPL MAIN FUNCTION ##########
|
382
|
+
|
383
|
+
##
|
384
|
+
# Process a single command from the REPL and display its output.
|
385
|
+
#
|
386
|
+
# This method is called for all commands except for "quit" and "exit", which are
|
387
|
+
# processed by {#repl} directly.
|
388
|
+
#
|
389
|
+
# @param input [String] The command line provided by the REPL (or the test).
|
390
|
+
# @return [Symbol] One of :SUCCESS, :EXCEPTION, :ARITY, :INVALID_COMMAND
|
391
|
+
# depending on whether the command was recognized and executed.
|
392
|
+
def process_command_input(input)
|
393
|
+
begin
|
394
|
+
unless input.empty?
|
395
|
+
toks = input.tokenize
|
396
|
+
cmd = toks.shift.downcase.strip
|
397
|
+
|
398
|
+
if ['cipher', 'encipher', 'encode'].include?(cmd)
|
399
|
+
message = toks.join(' ')
|
400
|
+
puts self.encipher(message).colorize(color: :white).bold
|
401
|
+
elsif self.is_internal_verb?(cmd.to_sym)
|
402
|
+
begin
|
403
|
+
if toks.length >= arity(cmd.to_sym)
|
404
|
+
if cmd == "last_result"
|
405
|
+
puts self.send(cmd.to_sym, toks).colorize(color: :white).bold
|
406
|
+
else
|
407
|
+
puts self.send(cmd.to_sym, toks).colorize(color: :green)
|
408
|
+
end
|
409
|
+
else
|
410
|
+
puts "Command #{cmd} requires at least #{arity(cmd.to_sym)} arguments".colorize(color: :red)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
else
|
414
|
+
puts "Unknown command: #{cmd}".colorize(color: :light_red).bold
|
415
|
+
end
|
416
|
+
end
|
417
|
+
rescue StandardError => e
|
418
|
+
puts "Rescued exception: #{e}".colorize(color: :red)
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
##
|
423
|
+
# Provide an interactive REPL for manipulating the Rotor Machine. Essentially
|
424
|
+
# this REPL is an interactive wrapper around the {RotorMachine::Session} object,
|
425
|
+
# with tab completion and command history provided by the {Readline} library.
|
426
|
+
#
|
427
|
+
# @param commands [Array] If provided, the commands passed in will be executed
|
428
|
+
# in sequence. This is mainly intended for RSpec testing. If no commands are
|
429
|
+
# passed in, the interactive REPL loop (with Readline) will be launched instead.
|
430
|
+
def repl(commands=nil)
|
431
|
+
Readline.completion_append_character = " "
|
432
|
+
Readline.completion_proc = proc { |s| verbs.keys.grep(/^#{Regexp.escape(s)}/) }
|
433
|
+
|
434
|
+
banner
|
435
|
+
|
436
|
+
if commands.nil? || commands.empty?
|
437
|
+
#:nocov:
|
438
|
+
while input = Readline.readline(readline_prompt, true).strip
|
439
|
+
if ['exit', 'quit'].include?(input.downcase)
|
440
|
+
return
|
441
|
+
end
|
442
|
+
process_command_input(input)
|
443
|
+
end
|
444
|
+
#:nocov:
|
445
|
+
else
|
446
|
+
commands.each { |cmd| process_command_input(cmd) }
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
##
|
451
|
+
# Helper for instantiating a new REPL.
|
452
|
+
#
|
453
|
+
# @param commands [Array] If provided, the commands passed in will be executed
|
454
|
+
# in sequence. This is mainly intended for RSpec testing. If no commands are
|
455
|
+
# passed in, the interactive REPL loop (with Readline) will be launched instead.
|
456
|
+
def self.repl(commands=nil)
|
457
|
+
RotorMachine::Shell.new().repl(commands)
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
@@ -40,6 +40,36 @@ class String
|
|
40
40
|
self.chars.reject{|s| s.match(/\s/)}.each_slice(block_size).map(&:join)
|
41
41
|
end
|
42
42
|
end
|
43
|
-
|
44
43
|
alias :in_blocks :in_blocks_of
|
44
|
+
|
45
|
+
##
|
46
|
+
# Break a string into words, including handling quoted substrings.
|
47
|
+
#
|
48
|
+
# @return An array of tokens, if the string can be tokenized.
|
49
|
+
def tokenize
|
50
|
+
self.
|
51
|
+
split(/\s(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/).
|
52
|
+
select {|s| not s.empty? }.
|
53
|
+
map {|s| s.gsub(/(^ +)|( +$)|(^["']+)|(["']+$)/,'')}
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Test if the string is a number.
|
58
|
+
#
|
59
|
+
# @return True if the string is a number, false otherwise.
|
60
|
+
def is_number?
|
61
|
+
true if Float(self) rescue false
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
##
|
66
|
+
# Strip ANSI color sequences from the string.
|
67
|
+
#
|
68
|
+
# @return The string with ANSI color sequences escaped.
|
69
|
+
def uncolorize
|
70
|
+
pattern = /\033\[([0-9]+);([0-9]+);([0-9]+)m(.+?)\033\[0m|([^\033]+)/m
|
71
|
+
self.scan(pattern).inject("") do |str, match|
|
72
|
+
str << (match[3] || match[4])
|
73
|
+
end
|
74
|
+
end
|
45
75
|
end
|
data/rotor_machine.gemspec
CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.require_paths = ['lib']
|
22
22
|
|
23
23
|
spec.add_dependency 'tcravit_ruby_lib', '~> 0.2'
|
24
|
+
spec.add_dependency 'colorize', '~> 0.8'
|
24
25
|
|
25
26
|
spec.add_development_dependency 'pry', '~> 0.11'
|
26
27
|
spec.add_development_dependency 'pry-byebug', '~> 3.6'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rotor_machine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tammy Cravit
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: tcravit_ruby_lib
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: colorize
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.8'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: pry
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -196,6 +210,7 @@ files:
|
|
196
210
|
- lib/rotor_machine/reflector.rb
|
197
211
|
- lib/rotor_machine/rotor.rb
|
198
212
|
- lib/rotor_machine/session.rb
|
213
|
+
- lib/rotor_machine/shell.rb
|
199
214
|
- lib/rotor_machine/string_extensions.rb
|
200
215
|
- lib/rotor_machine/version.rb
|
201
216
|
- rotor_machine.gemspec
|