rotor_machine 1.2.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|