datalackeytools 0.3.4
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 +7 -0
- data/LICENSE.txt +17 -0
- data/bin/datalackey-make +1014 -0
- data/bin/datalackey-run +234 -0
- data/bin/datalackey-shell +1505 -0
- data/bin/datalackey-state +1005 -0
- data/bin/files2object +56 -0
- data/bin/object2files +44 -0
- data/lib/common.rb +9 -0
- data/lib/datalackeylib.rb +638 -0
- metadata +68 -0
@@ -0,0 +1,1505 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Copyright 2019 Ismo Kärkkäinen
|
4
|
+
# Licensed under Universal Permissive License. See LICENSE.txt.
|
5
|
+
|
6
|
+
# Argument handling and checking
|
7
|
+
# datalackey process
|
8
|
+
# Presenting command output
|
9
|
+
# Notification presenters
|
10
|
+
# Command handlers
|
11
|
+
# Run command handlers
|
12
|
+
|
13
|
+
require 'optparse'
|
14
|
+
require 'readline'
|
15
|
+
require 'shellwords'
|
16
|
+
require 'json'
|
17
|
+
require 'yaml'
|
18
|
+
require 'set'
|
19
|
+
require 'pathname'
|
20
|
+
require 'io/console'
|
21
|
+
require_relative '../lib/datalackeylib'
|
22
|
+
|
23
|
+
$version = 1
|
24
|
+
history_file_basename = '.datalackey-shell.history'
|
25
|
+
history_file_max_lines = 2000
|
26
|
+
$echo_lackey_output = false
|
27
|
+
$echo_lackey_input = false
|
28
|
+
|
29
|
+
# Argument handling and checking
|
30
|
+
|
31
|
+
class Arguments
|
32
|
+
attr_reader :directory, :memory, :lackey, :permissions
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
@directory = nil
|
36
|
+
@memory = nil
|
37
|
+
@lackey = nil
|
38
|
+
@permissions = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse(args)
|
42
|
+
parser = OptionParser.new do |opts|
|
43
|
+
opts.summary_indent = ' '
|
44
|
+
opts.summary_width = 26
|
45
|
+
opts.banner = "Usage: datalackey-shell [options]"
|
46
|
+
opts.separator ""
|
47
|
+
opts.separator "Options:"
|
48
|
+
opts.on("-m", "--memory", "Store data in memory.") do
|
49
|
+
@memory = true
|
50
|
+
end
|
51
|
+
opts.on("-d", "--directory [DIR]",
|
52
|
+
"Store data under (working) directory.") do |d|
|
53
|
+
@directory = d || ''
|
54
|
+
end
|
55
|
+
opts.on_tail("-h", "--help", "Print this help and exit.") do
|
56
|
+
puts opts
|
57
|
+
exit 0
|
58
|
+
end
|
59
|
+
opts.on("-l", "--lackey PROGRAM",
|
60
|
+
"Use specified datalackey executable.") do |e|
|
61
|
+
@lackey = e
|
62
|
+
end
|
63
|
+
opts.on("-p", "--permissions MODE", [:user, :group, :other],
|
64
|
+
"File permissions cover (user, group, other).") do |p|
|
65
|
+
@permissions = { :user => "600", :group => "660", :other => "666" }[p]
|
66
|
+
end
|
67
|
+
opts.on('-e', '--echo', 'Turn datalackey command and reply echo on.') do
|
68
|
+
$echo_lackey_output = true
|
69
|
+
$echo_lackey_input = true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
parser.parse!(args)
|
73
|
+
begin
|
74
|
+
@directory, @permissions, @memory =
|
75
|
+
DatalackeyProcess.verify_directory_permissions_memory(
|
76
|
+
@directory, @permissions, @memory)
|
77
|
+
rescue ArgumentError => e
|
78
|
+
STDERR.puts e.to_s
|
79
|
+
exit 1
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
arguments = Arguments.new
|
85
|
+
arguments.parse(ARGV)
|
86
|
+
|
87
|
+
# Output when multiple threads and possibly prompt are present.
|
88
|
+
|
89
|
+
$prompt = '> '
|
90
|
+
$in_prompt = false
|
91
|
+
$output_mutex = Mutex.new
|
92
|
+
|
93
|
+
def print_lines(messages)
|
94
|
+
return false if not (messages.is_a? Array or messages.is_a? String) or messages.empty?
|
95
|
+
messages = [ messages ] unless messages.is_a? Array
|
96
|
+
rows, cols = IO.console.winsize
|
97
|
+
cut = []
|
98
|
+
messages.each do |m|
|
99
|
+
if m.length < cols or m.include? '\n'
|
100
|
+
cut.push m
|
101
|
+
next
|
102
|
+
end
|
103
|
+
pieces = m.split
|
104
|
+
line = pieces.first
|
105
|
+
for k in 1...pieces.length
|
106
|
+
if line.length + 1 + pieces[k].length <= cols
|
107
|
+
line << ' ' << pieces[k]
|
108
|
+
next
|
109
|
+
end
|
110
|
+
cut.push line
|
111
|
+
line = ' ' + pieces[k]
|
112
|
+
end
|
113
|
+
cut.push line
|
114
|
+
end
|
115
|
+
in_prompt = $in_prompt
|
116
|
+
$output_mutex.synchronize do
|
117
|
+
puts() if in_prompt
|
118
|
+
cut.each { |m| puts m }
|
119
|
+
print($prompt) if in_prompt
|
120
|
+
$stdout.flush
|
121
|
+
Readline.redisplay() if in_prompt
|
122
|
+
end
|
123
|
+
return true
|
124
|
+
end
|
125
|
+
|
126
|
+
# datalackey process
|
127
|
+
|
128
|
+
def handle_notifcation(category, action, message, vars)
|
129
|
+
case category
|
130
|
+
when :internal
|
131
|
+
case action
|
132
|
+
when :stored then print_lines("Stored: #{vars.first}")
|
133
|
+
when :deleted then print_lines("Deleted: #{vars.first}")
|
134
|
+
when :data_error then print_lines("DATA ERROR: #{vars.first}")
|
135
|
+
when :started
|
136
|
+
when :ended
|
137
|
+
when :error_format then print_lines("FORMAT ERROR: #{vars.first}")
|
138
|
+
end
|
139
|
+
when :internal_error
|
140
|
+
case action
|
141
|
+
when :user_id
|
142
|
+
print_lines("Bad identifier in run command? #{msg.join(' ')}")
|
143
|
+
else
|
144
|
+
print_lines("INTERNAL ERROR: #{msg.join(' ')}")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
begin
|
150
|
+
$lackey_proc = DatalackeyProcess.new(arguments.lackey, arguments.directory, arguments.permissions, arguments.memory)
|
151
|
+
rescue ArgumentError => e
|
152
|
+
puts e.to_s
|
153
|
+
exit 1
|
154
|
+
end
|
155
|
+
stderr_discarder = DiscardReader.new($lackey_proc.stderr)
|
156
|
+
$lackey = DatalackeyIO.new($lackey_proc.stdin, $lackey_proc.stdout,
|
157
|
+
Proc.new do |category, action, message, vars|
|
158
|
+
handle_notifcation(category, action, message, vars)
|
159
|
+
end,
|
160
|
+
Proc.new { |json| print_lines(json) if $echo_lackey_input },
|
161
|
+
Proc.new { |json| print_lines(json) if $echo_lackey_output })
|
162
|
+
|
163
|
+
# Command handlers
|
164
|
+
|
165
|
+
$handler = Hash.new(nil)
|
166
|
+
|
167
|
+
class Handler
|
168
|
+
|
169
|
+
def syntax_array_requirements(arr)
|
170
|
+
return [ arr, arr.length, false ] unless arr.first.is_a? Integer
|
171
|
+
alternatives = false
|
172
|
+
required = arr.first
|
173
|
+
arr = arr[1...arr.length]
|
174
|
+
if required < 0
|
175
|
+
required = arr.length
|
176
|
+
alternatives = true
|
177
|
+
end
|
178
|
+
return arr, required, alternatives
|
179
|
+
end
|
180
|
+
|
181
|
+
def construct_usages(result, value, seen = [])
|
182
|
+
if value.is_a? Array
|
183
|
+
value, required, alternatives = syntax_array_requirements(value)
|
184
|
+
prev = nil
|
185
|
+
value.each_index do |k|
|
186
|
+
alt = value[k]
|
187
|
+
if alternatives and 0 < k
|
188
|
+
result.push(prev.is_a?(Array) ? ' || ' : '|')
|
189
|
+
elsif prev.is_a? String
|
190
|
+
result.push ' '
|
191
|
+
elsif prev.is_a? Symbol
|
192
|
+
result.push(' ') unless result.last == '[' or result.last == '|'
|
193
|
+
end
|
194
|
+
result.push('[') if 0 < required and k == required
|
195
|
+
prev = alt
|
196
|
+
construct_usages(result, alt, seen)
|
197
|
+
end
|
198
|
+
result.pop() if result.last == '|'
|
199
|
+
result.push(']') if 0 < required and required < value.length
|
200
|
+
return result
|
201
|
+
end
|
202
|
+
if seen.include? value
|
203
|
+
result.push '...'
|
204
|
+
return result
|
205
|
+
end
|
206
|
+
if @syntax.has_key? value
|
207
|
+
seen.push value
|
208
|
+
construct_usages(result, @syntax[value], seen)
|
209
|
+
seen.pop
|
210
|
+
return result
|
211
|
+
end
|
212
|
+
result.push value.to_s
|
213
|
+
return result
|
214
|
+
end
|
215
|
+
|
216
|
+
def usages
|
217
|
+
top = @syntax[:root]
|
218
|
+
return { :root => [ top ] } if top.is_a? String # Parameterless command.
|
219
|
+
results = { }
|
220
|
+
if top.first.is_a? Integer and top[1].is_a? String or top.first.is_a? String
|
221
|
+
results[:root] = construct_usages([], :root)
|
222
|
+
else # Command with variations. Expect array of arrays of symbol(s).
|
223
|
+
top = top[1...top.length] if top.first.is_a? Integer
|
224
|
+
top.each { |sym| results[sym] = construct_usages([], sym) }
|
225
|
+
end
|
226
|
+
return results
|
227
|
+
end
|
228
|
+
|
229
|
+
def print_help
|
230
|
+
msgs = []
|
231
|
+
usages.each_pair do |key, value|
|
232
|
+
m = "#{value.join}"
|
233
|
+
m.concat(" : #{@syntax[:help][key]}") if @syntax[:help].include? key
|
234
|
+
msgs.push m
|
235
|
+
end
|
236
|
+
print_lines(msgs.sort)
|
237
|
+
end
|
238
|
+
|
239
|
+
def completion_search(suggestions, parts, part_idx, key, item)
|
240
|
+
return true if part_idx >= parts.length
|
241
|
+
last = part_idx + 1 == parts.length
|
242
|
+
if item.is_a? Symbol # These either validate part or add suggestions.
|
243
|
+
cands = nil
|
244
|
+
case item
|
245
|
+
when :command
|
246
|
+
cands = $handler.keys
|
247
|
+
when :label
|
248
|
+
cands = $lackey.data.keys
|
249
|
+
when :process_id
|
250
|
+
cands = $lackey.process.keys
|
251
|
+
when :history_process_id
|
252
|
+
cands = [] # Run command identifiers from history.
|
253
|
+
scan_history([ Regexp.new('^run[[:blank:]]+') ]).each do |h|
|
254
|
+
pieces = h.shellsplit
|
255
|
+
cands.push(pieces[1]) if 1 < pieces.length
|
256
|
+
end
|
257
|
+
when :executable
|
258
|
+
return File.executable?(parts[part_idx]) unless last
|
259
|
+
cands = Dir[parts.last + '*'].grep(/^#{Regexp.escape(parts.last)}/)
|
260
|
+
when :string
|
261
|
+
return true # Anything goes.
|
262
|
+
when :int
|
263
|
+
begin
|
264
|
+
Integer(parts[part_idx])
|
265
|
+
return true # Any integer is fine.
|
266
|
+
rescue ArgumentError
|
267
|
+
return false
|
268
|
+
end
|
269
|
+
when :null
|
270
|
+
# Requires that :null is before :string in allowed values.
|
271
|
+
return $null_handler.null == parts[part_idx] unless last
|
272
|
+
cands = [ $null_handler.null ]
|
273
|
+
when :varname
|
274
|
+
return not(parts[part_idx].include?('=')) unless last
|
275
|
+
cands = []
|
276
|
+
else
|
277
|
+
return completion_search(suggestions, parts, part_idx, item, @syntax[item])
|
278
|
+
end
|
279
|
+
return false if cands.nil?
|
280
|
+
return cands.include?(parts[part_idx]) unless last
|
281
|
+
suggestions.concat cands
|
282
|
+
return true
|
283
|
+
elsif item.is_a? String
|
284
|
+
return item == parts[part_idx] unless last
|
285
|
+
suggestions.push item
|
286
|
+
return true
|
287
|
+
elsif item.is_a? Array
|
288
|
+
item, required, ored = syntax_array_requirements(item)
|
289
|
+
ok = true
|
290
|
+
item.each_index do |k|
|
291
|
+
sub = item[k]
|
292
|
+
res = completion_search(suggestions, parts, part_idx, key, sub)
|
293
|
+
ok = (ored ? (ok or res) : (required <= k ? ok : res))
|
294
|
+
break unless ok or ored
|
295
|
+
part_idx = part_idx + 1 unless ored
|
296
|
+
end
|
297
|
+
return ok
|
298
|
+
end
|
299
|
+
raise ArgumentError.new('Item other than nil, Symbol, String or Array.')
|
300
|
+
end
|
301
|
+
|
302
|
+
def completion_candidates(parts, str)
|
303
|
+
suggestions = []
|
304
|
+
parts.push('') if str.empty?
|
305
|
+
completion_search(suggestions, parts, 0, :root, @syntax[:root])
|
306
|
+
return suggestions.grep(/^#{Regexp.escape(str)}/).sort.uniq
|
307
|
+
end
|
308
|
+
|
309
|
+
def pop_false(match, condition)
|
310
|
+
match.pop() unless condition
|
311
|
+
return condition
|
312
|
+
end
|
313
|
+
|
314
|
+
def verify_search(match, parts, key, item)
|
315
|
+
return nil if match.length == parts.length
|
316
|
+
part = parts[match.length]
|
317
|
+
last = match.length + 1 == parts.length
|
318
|
+
if item.is_a? Symbol # These either validate part or add suggestions.
|
319
|
+
match.push item
|
320
|
+
case item
|
321
|
+
when :command
|
322
|
+
return pop_false(match, $handler.has_key?(part))
|
323
|
+
when :label
|
324
|
+
return pop_false(match, $lackey.data.has_key?(part))
|
325
|
+
when :process_id
|
326
|
+
return pop_false(match, $lackey.process.has_key?(part))
|
327
|
+
when :history_process_id
|
328
|
+
scan_history([ Regexp.new('^run[[:blank:]]+') ]).each do |h|
|
329
|
+
pieces = h.shellsplit
|
330
|
+
return true if pieces.length >= 2 and pieces[1] == part
|
331
|
+
end
|
332
|
+
return pop_false(match, false)
|
333
|
+
when :executable
|
334
|
+
return pop_false(match, File.executable?(part))
|
335
|
+
when :string
|
336
|
+
return true # Anything goes.
|
337
|
+
when :int
|
338
|
+
begin
|
339
|
+
Integer(part)
|
340
|
+
return true # Any integer is fine.
|
341
|
+
rescue ArgumentError
|
342
|
+
return pop_false(match, false)
|
343
|
+
end
|
344
|
+
when :null
|
345
|
+
nv = $null_handler.null
|
346
|
+
return pop_false(match, nv == part)
|
347
|
+
when :varname
|
348
|
+
return pop_false(match, not(part.include?('=')))
|
349
|
+
end
|
350
|
+
match.pop
|
351
|
+
return verify_search(match, parts, item, @syntax[item])
|
352
|
+
elsif item.is_a? String
|
353
|
+
match.push item
|
354
|
+
return pop_false(match, item == part)
|
355
|
+
elsif item.is_a? Array
|
356
|
+
item, required, alternatives = syntax_array_requirements(item)
|
357
|
+
required = 1 if alternatives
|
358
|
+
return false if parts.length < match.length + required
|
359
|
+
orig_count = match.length
|
360
|
+
item.each_index do |k|
|
361
|
+
sub = item[k]
|
362
|
+
ok = verify_search(match, parts, key, sub)
|
363
|
+
return nil if ok.nil? or (ok and match.length == parts.length)
|
364
|
+
if alternatives
|
365
|
+
return true if ok # One match is enough.
|
366
|
+
next
|
367
|
+
elsif not ok
|
368
|
+
if k < required # Looking for required consecutive matches.
|
369
|
+
match.pop(match.length - orig_count)
|
370
|
+
return false
|
371
|
+
elsif required == 0
|
372
|
+
return false # Optional repeating element not matched. Don't recurse.
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
return true unless alternatives
|
377
|
+
match.pop(match.length - orig_count)
|
378
|
+
return false
|
379
|
+
end
|
380
|
+
raise ArgumentError.new('Item other than nil, Symbol, String or Array.')
|
381
|
+
end
|
382
|
+
|
383
|
+
def verify(parts)
|
384
|
+
top = @syntax[:root]
|
385
|
+
return (parts.length == 1 and parts[0] == top) if top.is_a? String
|
386
|
+
match = []
|
387
|
+
return false unless verify_search(match, parts, :root, top).nil?
|
388
|
+
# Convert according to match information as needed.
|
389
|
+
parts.each_index do |k|
|
390
|
+
case match[k]
|
391
|
+
when :null
|
392
|
+
parts[k] = nil
|
393
|
+
when :int
|
394
|
+
parts[k] = Integer(parts[k])
|
395
|
+
end
|
396
|
+
end
|
397
|
+
return true
|
398
|
+
end
|
399
|
+
|
400
|
+
def register_search
|
401
|
+
# Currently it holds that the strings are at top level first or second
|
402
|
+
# level first items.
|
403
|
+
top = @syntax[:root]
|
404
|
+
return [ top ] if top.is_a? String
|
405
|
+
raise ArgumentError.new('Not String or Array.') unless top.is_a? Array
|
406
|
+
items, required, search_children = syntax_array_requirements(top)
|
407
|
+
return [ items.first ] unless search_children
|
408
|
+
cmds = []
|
409
|
+
items.each do |item|
|
410
|
+
sub = @syntax[item]
|
411
|
+
if sub.is_a? String
|
412
|
+
cmds.push sub
|
413
|
+
next
|
414
|
+
end
|
415
|
+
sub, required, wrong_assumption = syntax_array_requirements(sub)
|
416
|
+
raise ArgumentError.new('Alternatives at level 2') if wrong_assumption
|
417
|
+
raise ArgumentError.new('Symbol at level 2') unless sub.first.is_a? String
|
418
|
+
cmds.push sub.first
|
419
|
+
end
|
420
|
+
return cmds.uniq
|
421
|
+
end
|
422
|
+
|
423
|
+
def register
|
424
|
+
register_search.each do |cmd|
|
425
|
+
raise KeyError.new("Command in handlers: #{cmd}") if $handler.has_key? cmd
|
426
|
+
$handler[cmd] = self
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def send(patt_act, message, user_id = false)
|
431
|
+
$lackey.send(patt_act, message, user_id)
|
432
|
+
end
|
433
|
+
|
434
|
+
def dump(json_as_string)
|
435
|
+
$lackey.dump(json_as_string)
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
class HelpHandler < Handler
|
440
|
+
def initialize
|
441
|
+
@syntax = {
|
442
|
+
:root => [ 1, 'help', :cmds ],
|
443
|
+
:cmds => [ 0, :command, :cmds ],
|
444
|
+
:help => { :root => "Print command help." }
|
445
|
+
}
|
446
|
+
register
|
447
|
+
end
|
448
|
+
|
449
|
+
def handle(parts)
|
450
|
+
printed = Set.new
|
451
|
+
keys = (parts.length > 1) ? parts[1...parts.length] : $handler.keys
|
452
|
+
keys.sort.each do |key|
|
453
|
+
if $handler.include? key
|
454
|
+
object = $handler[key]
|
455
|
+
next if printed.include? object
|
456
|
+
printed.add object
|
457
|
+
object.print_help
|
458
|
+
else
|
459
|
+
print_lines("Unknown command: #{key}")
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
#$handler['help'] = HelpHandler.new
|
465
|
+
HelpHandler.new
|
466
|
+
|
467
|
+
class ExitHandler < Handler
|
468
|
+
attr_reader :code, :exiting
|
469
|
+
|
470
|
+
def initialize
|
471
|
+
@syntax = {
|
472
|
+
:root => [ 1, 'exit', :int ],
|
473
|
+
:help => { :root => "Waits for datalackey to finish, exits with code." }
|
474
|
+
}
|
475
|
+
@code = 0
|
476
|
+
@exiting = false
|
477
|
+
register
|
478
|
+
end
|
479
|
+
|
480
|
+
def handle(parts)
|
481
|
+
@exiting = true
|
482
|
+
@code = parts[1] if parts.length == 2
|
483
|
+
end
|
484
|
+
|
485
|
+
def command_name
|
486
|
+
return @syntax[:root][1]
|
487
|
+
end
|
488
|
+
end
|
489
|
+
$exit_handler = ExitHandler.new
|
490
|
+
|
491
|
+
class NullHandler < Handler
|
492
|
+
attr_reader :null
|
493
|
+
|
494
|
+
def initialize
|
495
|
+
@syntax = {
|
496
|
+
:root => [ -1, :nullcmd, :nullcmdwhat ],
|
497
|
+
:nullcmd => [ 'null', :string ],
|
498
|
+
:nullcmdwhat => 'null?',
|
499
|
+
:help => {
|
500
|
+
:nullcmd => "Sets string that is interpreted as null when null is allowed.",
|
501
|
+
:nullcmdwhat => "Prints out string that is interpreted as null."
|
502
|
+
}
|
503
|
+
}
|
504
|
+
@null = 'null'
|
505
|
+
register
|
506
|
+
end
|
507
|
+
|
508
|
+
def handle(parts)
|
509
|
+
if parts.length == 2
|
510
|
+
@null = parts.last
|
511
|
+
else
|
512
|
+
print_lines("String interpreted as null is: '#{@null}'")
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
$null_handler = NullHandler.new
|
517
|
+
|
518
|
+
class ListHandler < Handler
|
519
|
+
def initialize
|
520
|
+
@syntax = {
|
521
|
+
:root => [ 1, 'ls', :regexps ],
|
522
|
+
:regexps => [ 0, :string, :regexps ],
|
523
|
+
:help => { :root => "Prints out the list of data labels that match optional regular expressions." }
|
524
|
+
}
|
525
|
+
register
|
526
|
+
end
|
527
|
+
|
528
|
+
def handle(parts)
|
529
|
+
exps = []
|
530
|
+
parts[1...parts.length].sort.uniq.each { |e| exps.push Regexp.new(e) }
|
531
|
+
matching = []
|
532
|
+
$lackey.data.keys.sort.each do |label|
|
533
|
+
match = exps.length == 0
|
534
|
+
exps.each do |e|
|
535
|
+
match = e.match(label)
|
536
|
+
break if match
|
537
|
+
end
|
538
|
+
matching.push(label) if match
|
539
|
+
end
|
540
|
+
print_lines(matching)
|
541
|
+
end
|
542
|
+
end
|
543
|
+
ListHandler.new
|
544
|
+
|
545
|
+
class SetHandler < Handler
|
546
|
+
def initialize
|
547
|
+
@syntax = {
|
548
|
+
:root => [ 'set', :string, :string ],
|
549
|
+
:help => {
|
550
|
+
:root => "Sets label to JSON-encoded value (use ' or \" as needed)."
|
551
|
+
}
|
552
|
+
}
|
553
|
+
register
|
554
|
+
end
|
555
|
+
|
556
|
+
def handle(parts)
|
557
|
+
k = JSON.generate parts[1]
|
558
|
+
dump "{#{k}:#{parts.last}}"
|
559
|
+
end
|
560
|
+
end
|
561
|
+
SetHandler.new
|
562
|
+
|
563
|
+
class GetHandler < Handler
|
564
|
+
def initialize
|
565
|
+
@syntax = {
|
566
|
+
:root => [ 2, 'get', :label, :labels ],
|
567
|
+
:labels => [ 0, :label, :labels ],
|
568
|
+
:help => { :root => "Gets labels." }
|
569
|
+
}
|
570
|
+
pretty_print = Proc.new do |category, action, message, vars|
|
571
|
+
m = nil
|
572
|
+
case category
|
573
|
+
when :print then m = JSON.pretty_generate(vars.first)
|
574
|
+
when :error then m = "Get failure: #{vars.join(' ')}"
|
575
|
+
when :note then m = "Missing: #{vars.join(' ')}"
|
576
|
+
end
|
577
|
+
print_lines m
|
578
|
+
end
|
579
|
+
@actions = PatternAction.new([{
|
580
|
+
:print => [ { :get => [ '@', 'get', '', '?' ] } ],
|
581
|
+
:error => [ { :fail => [ '@', 'get', 'failed', '*' ] } ],
|
582
|
+
:note => [ { :missing => [ '@', 'get', 'missing', '*' ] } ]
|
583
|
+
}], [ pretty_print ])
|
584
|
+
register
|
585
|
+
end
|
586
|
+
|
587
|
+
def handle(parts)
|
588
|
+
send(@actions, [ 'get' ].concat(parts[1...parts.length]))
|
589
|
+
end
|
590
|
+
end
|
591
|
+
GetHandler.new
|
592
|
+
|
593
|
+
class RenameHandler < Handler
|
594
|
+
def initialize
|
595
|
+
@syntax = {
|
596
|
+
:root => [ 2, 'mv', :label_pair, :label_pairs ],
|
597
|
+
:label_pairs => [ 0, :label_pair, :label_pairs ],
|
598
|
+
:label_pair => [ :label, :string ],
|
599
|
+
:help => { :root => "Re-labels values. Takes label and new name pairs." }
|
600
|
+
}
|
601
|
+
message_proc = Proc.new do |category, action, message, vars|
|
602
|
+
case category
|
603
|
+
when :note then print_lines "Missing: #{vars.join(' ')}"
|
604
|
+
else false
|
605
|
+
end
|
606
|
+
end
|
607
|
+
@actions = PatternAction.new([{
|
608
|
+
:note => [ { :missing => [ '@', 'rename', 'missing', '*' ] } ]
|
609
|
+
}], [ message_proc ])
|
610
|
+
register
|
611
|
+
end
|
612
|
+
|
613
|
+
def handle(parts)
|
614
|
+
send(@actions, [ 'rename' ].concat(parts[1...parts.length]))
|
615
|
+
end
|
616
|
+
end
|
617
|
+
RenameHandler.new
|
618
|
+
|
619
|
+
class DeleteHandler < Handler
|
620
|
+
def initialize
|
621
|
+
@syntax = {
|
622
|
+
:root => [ 2, 'rm', :label, :labels ],
|
623
|
+
:labels => [ 0, :label, :labels ],
|
624
|
+
:help => { :root => "Removes labels from storage." }
|
625
|
+
}
|
626
|
+
message_proc = Proc.new do |category, action, message, vars|
|
627
|
+
case category
|
628
|
+
when :note then print_lines "Missing: #{vars.join(' ')}"
|
629
|
+
else nil
|
630
|
+
end
|
631
|
+
end
|
632
|
+
@actions = PatternAction.new([{
|
633
|
+
:note => [ { :missing => [ '@', 'delete', 'missing', '*' ] } ]
|
634
|
+
}], [ message_proc ])
|
635
|
+
register
|
636
|
+
end
|
637
|
+
|
638
|
+
def handle(parts)
|
639
|
+
send(@actions, [ 'delete' ].concat(parts[1...parts.length]))
|
640
|
+
end
|
641
|
+
end
|
642
|
+
DeleteHandler.new
|
643
|
+
|
644
|
+
class StorageInfoHandler < Handler
|
645
|
+
def initialize
|
646
|
+
@syntax = {
|
647
|
+
:root => 'info',
|
648
|
+
:help => { :root => "Print information about storage." }
|
649
|
+
}
|
650
|
+
present = Proc.new do |category, action, message, vars|
|
651
|
+
out = []
|
652
|
+
if category == :return and action == :storage_info
|
653
|
+
vars.first.each_pair do |label, info|
|
654
|
+
description = "#{label} : "
|
655
|
+
fmts = []
|
656
|
+
info.each_pair { |format, size| fmts.push "#{format}: #{size}" }
|
657
|
+
fmts.sort!
|
658
|
+
out.push(description + fmts.join(', '))
|
659
|
+
end
|
660
|
+
out.sort!
|
661
|
+
end
|
662
|
+
print_lines out
|
663
|
+
end
|
664
|
+
@actions = PatternAction.new([{
|
665
|
+
:return => [ { :storage_info => [ '@', 'storage-info', '', '?' ] } ]
|
666
|
+
}], [ present ])
|
667
|
+
register
|
668
|
+
end
|
669
|
+
|
670
|
+
def handle(parts)
|
671
|
+
send(@actions, [ 'storage-info' ])
|
672
|
+
end
|
673
|
+
end
|
674
|
+
StorageInfoHandler.new
|
675
|
+
|
676
|
+
class PingHandler < Handler
|
677
|
+
def initialize
|
678
|
+
@syntax = {
|
679
|
+
:root => 'ping',
|
680
|
+
:help => { :root => "Check that datalackey responds." }
|
681
|
+
}
|
682
|
+
@actions = PatternAction.new([]) # Normal internal done is enough.
|
683
|
+
register
|
684
|
+
end
|
685
|
+
|
686
|
+
def handle(parts)
|
687
|
+
tracker = send(@actions, [ 'no-op' ])
|
688
|
+
if tracker.nil?
|
689
|
+
print_lines("No connection to datalackey.")
|
690
|
+
return
|
691
|
+
end
|
692
|
+
print_lines("Datalackey responded.") if tracker.status
|
693
|
+
end
|
694
|
+
end
|
695
|
+
PingHandler.new
|
696
|
+
|
697
|
+
class VersionHandler < Handler
|
698
|
+
def initialize
|
699
|
+
@syntax = {
|
700
|
+
:root => 'version',
|
701
|
+
:help => { :root => "Print version information." }
|
702
|
+
}
|
703
|
+
register
|
704
|
+
end
|
705
|
+
|
706
|
+
def handle(parts)
|
707
|
+
vs = [ "shell: #{$version}" ]
|
708
|
+
$lackey.version.each_pair { |k, v| vs.push "#{k}: #{v}" }
|
709
|
+
vs.sort!
|
710
|
+
print_lines vs
|
711
|
+
end
|
712
|
+
end
|
713
|
+
VersionHandler.new
|
714
|
+
|
715
|
+
class EchoHandler < Handler
|
716
|
+
def initialize
|
717
|
+
@syntax = {
|
718
|
+
:root => [ -1, :lackey, :lackeywhat ],
|
719
|
+
:lackey => [ 'echo', :inout, :onoff ],
|
720
|
+
:lackeywhat => 'echo?',
|
721
|
+
:inout => [ -1, 'command', 'reply', 'all' ],
|
722
|
+
:onoff => [ -1, 'on', 'off' ],
|
723
|
+
:help => {
|
724
|
+
:lackey => "Set the printing of communication with datalackey on or off.",
|
725
|
+
:lackeywhat => "Print datalackey command/reply printing status."
|
726
|
+
}
|
727
|
+
}
|
728
|
+
register
|
729
|
+
end
|
730
|
+
|
731
|
+
def handle(parts)
|
732
|
+
if parts.length > 1
|
733
|
+
what = parts[1]
|
734
|
+
on = parts[2] == 'on'
|
735
|
+
$echo_lackey_input = on if what == 'command' or what == 'all'
|
736
|
+
$echo_lackey_output = on if what == 'reply'or what == 'all'
|
737
|
+
else
|
738
|
+
print_lines([ "command: #{$echo_lackey_input ? 'on' : 'off'}",
|
739
|
+
"reply : #{$echo_lackey_output ? 'on' : 'off'}" ])
|
740
|
+
end
|
741
|
+
end
|
742
|
+
end
|
743
|
+
EchoHandler.new
|
744
|
+
|
745
|
+
def scan_history(regexps)
|
746
|
+
matched = Array.new
|
747
|
+
Readline::HISTORY.take(Readline::HISTORY.length - 1).each do |h|
|
748
|
+
match = regexps.length == 0
|
749
|
+
regexps.each do |e|
|
750
|
+
match = e.match(h)
|
751
|
+
break if match
|
752
|
+
end
|
753
|
+
matched.push(h) if match
|
754
|
+
end
|
755
|
+
return matched
|
756
|
+
end
|
757
|
+
|
758
|
+
class HistoryHandler < Handler
|
759
|
+
def initialize
|
760
|
+
@syntax = {
|
761
|
+
:root => [ 1, 'history', :regexps ],
|
762
|
+
:regexps => [ 0, :string, :regexps ],
|
763
|
+
:help => { :root => "Print commands that match any regex, or all." }
|
764
|
+
}
|
765
|
+
register
|
766
|
+
end
|
767
|
+
|
768
|
+
def handle(parts)
|
769
|
+
exps = []
|
770
|
+
parts[1...parts.length].each { |e| exps.push Regexp.new(e) }
|
771
|
+
print_lines scan_history(exps)
|
772
|
+
end
|
773
|
+
end
|
774
|
+
HistoryHandler.new
|
775
|
+
|
776
|
+
class RecallHandler < Handler
|
777
|
+
def initialize
|
778
|
+
@syntax = {
|
779
|
+
:root => [ 'recall', :history_process_id ],
|
780
|
+
:help => { :root => "Fetch last \"run identifier\" related settings from history. Finds the output, env, arg, channel, and notify settings that were in effect when last run with given identifier was done. If no identifier is given, last run command identifier is used. Prints the lines found from history." }
|
781
|
+
}
|
782
|
+
register
|
783
|
+
end
|
784
|
+
|
785
|
+
def scan_until_first(new_to_old, exps)
|
786
|
+
regexps = []
|
787
|
+
exps.each { |e| regexps.push Regexp.new(e) }
|
788
|
+
matches = []
|
789
|
+
new_to_old.each do |c|
|
790
|
+
for k in 0...regexps.length
|
791
|
+
next unless regexps[k].match(c)
|
792
|
+
matches.push(c)
|
793
|
+
return matches if k == 0
|
794
|
+
break
|
795
|
+
end
|
796
|
+
end
|
797
|
+
return matches
|
798
|
+
end
|
799
|
+
|
800
|
+
def print_until_first(new_to_old, exps)
|
801
|
+
print_lines scan_until_first(new_to_old, exps).reverse
|
802
|
+
end
|
803
|
+
|
804
|
+
def handle(parts)
|
805
|
+
id_given = parts.length == 2
|
806
|
+
es = [ '^output[+]?[[:blank:]]+',
|
807
|
+
'^arg[+]?[[:blank:]]+', '^env[+]?[[:blank:]]+', '^env-clear$',
|
808
|
+
'^channel[[:blank:]]+', "^#{$exit_handler.command_name}",
|
809
|
+
'^notify[[:blank:]]+' ]
|
810
|
+
es.push('^run[[:blank:]]+' + (id_given ? parts[1] + '[[:blank:]]+' : ''))
|
811
|
+
exps = []
|
812
|
+
es.each { |e| exps.push Regexp.new(e) }
|
813
|
+
candidates = scan_history(exps)
|
814
|
+
# Drop everything after the last matching run command.
|
815
|
+
while not candidates.empty? and not candidates.last.start_with? 'run'
|
816
|
+
candidates.pop
|
817
|
+
end
|
818
|
+
# Pick commands before last exit. Reverses order.
|
819
|
+
session = []
|
820
|
+
while not candidates.empty? and not candidates.last.start_with? $exit_handler.command_name
|
821
|
+
session.push candidates.pop
|
822
|
+
end
|
823
|
+
print_until_first(session, [ '^notify[[:blank:]]+data[[:blank:]]' ])
|
824
|
+
print_until_first(session, [ '^notify[[:blank:]]+process[[:blank:]]' ])
|
825
|
+
print_until_first(session, [ '^channel[[:blank:]]+in[[:blank:]]' ])
|
826
|
+
print_until_first(session, [ '^channel[[:blank:]]+out[[:blank:]]' ])
|
827
|
+
print_until_first(session, [ '^channel[[:blank:]]+err[[:blank:]]' ])
|
828
|
+
print_until_first(session,
|
829
|
+
[ '^env[[:blank:]]', '^env[+][[:blank:]]', '^env-clear$' ])
|
830
|
+
print_until_first(session, [ '^arg[[:blank:]]', '^arg[+][[:blank:]]' ])
|
831
|
+
print_until_first(session, [ '^output[[:blank:]]', '^output[+][[:blank:]]', '^output-naming[[:blank:]]' ])
|
832
|
+
end
|
833
|
+
end
|
834
|
+
RecallHandler.new
|
835
|
+
|
836
|
+
# Run command handlers
|
837
|
+
|
838
|
+
class NotifyHandler < Handler
|
839
|
+
def initialize
|
840
|
+
@syntax = {
|
841
|
+
:root => [ -1, :notify, :notifywhat ],
|
842
|
+
:notify => [ 'notify', :procdata, :onoff ],
|
843
|
+
:notifywhat => 'notify?',
|
844
|
+
:procdata => [ -1, 'data', 'process' ],
|
845
|
+
:onoff => [ -1, 'on', 'off' ],
|
846
|
+
:help => {
|
847
|
+
:notify => "Turn process or data notifications on or off.",
|
848
|
+
:notifywhat => "Print data/process notification status."
|
849
|
+
}
|
850
|
+
}
|
851
|
+
@notify = { 'process' => 'off', 'data' => 'off' }
|
852
|
+
register
|
853
|
+
end
|
854
|
+
|
855
|
+
def handle(parts)
|
856
|
+
if parts.length > 1
|
857
|
+
@notify[parts[1]] = parts[2]
|
858
|
+
else
|
859
|
+
msgs = []
|
860
|
+
@notify.keys.sort.each { |key| msgs.push "#{key} : #{@notify[key]}" }
|
861
|
+
print_lines msgs
|
862
|
+
end
|
863
|
+
end
|
864
|
+
|
865
|
+
def command_part
|
866
|
+
command = []
|
867
|
+
@notify.each_pair do |key, value|
|
868
|
+
command.concat(['notify', key]) if value == 'on'
|
869
|
+
end
|
870
|
+
return command
|
871
|
+
end
|
872
|
+
end
|
873
|
+
$notify_handler = NotifyHandler.new
|
874
|
+
|
875
|
+
class InputHandler < Handler
|
876
|
+
def initialize
|
877
|
+
@syntax = {
|
878
|
+
:root => [ -1, :input, :inputadd, :inputwhat ],
|
879
|
+
:input => [ 1, 'input', :args ],
|
880
|
+
:inputadd => [ 2, 'input+', :a2b, :args ],
|
881
|
+
:inputwhat => 'input?',
|
882
|
+
:args => [ 0, :a2b, :args ],
|
883
|
+
:a2b => [ -1, [ :string, ':', :label ], [ :string, '=', :value ] ],
|
884
|
+
:value => [ -1, :null, :int, :string ],
|
885
|
+
:help => {
|
886
|
+
:input => "Clears input mapping and passes label as name, or value of name as given directly.",
|
887
|
+
:inputadd => "Adds to current input mapping.",
|
888
|
+
:inputwhat => "Prints current input mapping."
|
889
|
+
}
|
890
|
+
}
|
891
|
+
@order = []
|
892
|
+
@mapping = { }
|
893
|
+
register
|
894
|
+
end
|
895
|
+
|
896
|
+
def handle(parts)
|
897
|
+
if parts[0] == 'input?'
|
898
|
+
msgs = []
|
899
|
+
@order.each do |name|
|
900
|
+
direct, val = @mapping[name]
|
901
|
+
t = direct ? "=" : ":"
|
902
|
+
msgs.push "#{name} #{t} #{val}"
|
903
|
+
end
|
904
|
+
print_lines msgs
|
905
|
+
return
|
906
|
+
end
|
907
|
+
clear = parts[0] == 'input'
|
908
|
+
order_in = []
|
909
|
+
map_in = { }
|
910
|
+
idx = 1
|
911
|
+
while idx < parts.length
|
912
|
+
name = parts[idx]
|
913
|
+
direct = parts[idx + 1] == '='
|
914
|
+
src = parts[idx + 2]
|
915
|
+
idx += 3
|
916
|
+
if map_in.has_key?(name) or not clear and @mapping.has_key?(name)
|
917
|
+
print_lines "Error: #{name} already in mapping."
|
918
|
+
return
|
919
|
+
end
|
920
|
+
order_in.push name
|
921
|
+
map_in[name] = [direct, src]
|
922
|
+
end
|
923
|
+
if clear
|
924
|
+
@order.clear
|
925
|
+
@mapping.clear
|
926
|
+
end
|
927
|
+
@order.concat order_in
|
928
|
+
@mapping.merge! map_in
|
929
|
+
end
|
930
|
+
|
931
|
+
def command_part
|
932
|
+
command = []
|
933
|
+
@order.each do |name|
|
934
|
+
direct, val = @mapping[name]
|
935
|
+
command.concat [ direct ? 'direct' : 'input', val, name ]
|
936
|
+
end
|
937
|
+
return command
|
938
|
+
end
|
939
|
+
end
|
940
|
+
$input_handler = InputHandler.new
|
941
|
+
|
942
|
+
class OutputHandler < Handler
|
943
|
+
def initialize
|
944
|
+
@syntax = {
|
945
|
+
:root => [ -1, :output, :outputadd, :outputwhat, :outputnaming ],
|
946
|
+
:output => [ 1, 'output', :maps ],
|
947
|
+
:outputadd => [ 2, 'output+', :map, :maps ],
|
948
|
+
:outputwhat => 'output?',
|
949
|
+
:outputnaming => [ 'output-naming', :string, :string ],
|
950
|
+
:maps => [ 0, :map, :maps ],
|
951
|
+
:map => [ -1, [ :string, :string ], [ :string, :null ] ],
|
952
|
+
:help => {
|
953
|
+
:output => "Clears output mapping and maps name to label. Using null as label discards that name.",
|
954
|
+
:outputadd => "Adds to current output mapping.",
|
955
|
+
:outputwhat => "Prints current output mapping.",
|
956
|
+
:outputnaming => "Sets prefix and postfix to unmapped names."
|
957
|
+
}
|
958
|
+
}
|
959
|
+
@mapping = Hash.new
|
960
|
+
@prefix = nil
|
961
|
+
@postfix = nil
|
962
|
+
register
|
963
|
+
end
|
964
|
+
|
965
|
+
def handle(parts)
|
966
|
+
if parts[0] == 'output?'
|
967
|
+
msgs = []
|
968
|
+
msgs.push("Prefix: #{@prefix}") unless @prefix.nil?
|
969
|
+
msgs.push("Postfix: #{@postfix}") unless @postfix.nil?
|
970
|
+
@mapping.keys.sort.each do |name|
|
971
|
+
label = @mapping[name]
|
972
|
+
msgs.push "\"#{name}\" \"#{label}\""
|
973
|
+
end
|
974
|
+
print_lines msgs
|
975
|
+
return
|
976
|
+
elsif parts[0] == 'output-naming'
|
977
|
+
@prefix = parts[1].length ? parts[1] : nil
|
978
|
+
@postfix = parts[2].length ? parts[2] : nil
|
979
|
+
return
|
980
|
+
elsif parts[0] == 'output'
|
981
|
+
@mapping.clear
|
982
|
+
@prefix = nil
|
983
|
+
@postfix = nil
|
984
|
+
end
|
985
|
+
msgs = []
|
986
|
+
idx = 1
|
987
|
+
while idx < parts.length
|
988
|
+
name = parts[idx]
|
989
|
+
src = parts[idx + 1]
|
990
|
+
idx += 2
|
991
|
+
msgs.push("#{name} : #{@mapping[name]} -> #{src}") if @mapping.has_key? name
|
992
|
+
@mapping[name] = src
|
993
|
+
end
|
994
|
+
print_lines msgs
|
995
|
+
end
|
996
|
+
|
997
|
+
def command_part
|
998
|
+
command = []
|
999
|
+
command.concat([ 'output-prefix', @prefix ]) unless @prefix.nil?
|
1000
|
+
command.concat([ 'output-postfix', @postfix ]) unless @postfix.nil?
|
1001
|
+
@mapping.each_pair { |name, val| command.concat [ 'output', name, val ] }
|
1002
|
+
return command
|
1003
|
+
end
|
1004
|
+
end
|
1005
|
+
$output_handler = OutputHandler.new
|
1006
|
+
|
1007
|
+
class EnvHandler < Handler
|
1008
|
+
def initialize
|
1009
|
+
@syntax = {
|
1010
|
+
:root => [ -1, :env, :envadd, :envwhat, :envclear ],
|
1011
|
+
:env => [ 1, 'env', :varvalues ],
|
1012
|
+
:envadd => [ 2, 'env+', :varval, :varvalues ],
|
1013
|
+
:envwhat => 'env?',
|
1014
|
+
:envclear => 'env-clear',
|
1015
|
+
:varvalues => [ 0, :varval, :varvalues ],
|
1016
|
+
:varval => [ :varname, :string ],
|
1017
|
+
:help => {
|
1018
|
+
:env => "Clears environment variable mapping and sets var to value.",
|
1019
|
+
:envadd => "Sets var to value in environment variable mapping.",
|
1020
|
+
:envwhat => "Prints out current mapping.",
|
1021
|
+
:envclear => "Ignores environment variables outside current mapping."
|
1022
|
+
}
|
1023
|
+
}
|
1024
|
+
@mapping = { }
|
1025
|
+
@clear = false
|
1026
|
+
register
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
def handle(parts)
|
1030
|
+
if parts[0] == 'env?'
|
1031
|
+
msgs = []
|
1032
|
+
msgs.push("Restrict environment to given values.") if @clear == true
|
1033
|
+
@mapping.keys.sort.each do |name|
|
1034
|
+
value = @mapping[name]
|
1035
|
+
msgs.push "\"#{name}\" \"#{value}\""
|
1036
|
+
end
|
1037
|
+
print_lines msgs
|
1038
|
+
return
|
1039
|
+
elsif parts[0] == 'env-clear'
|
1040
|
+
@clear = true
|
1041
|
+
return
|
1042
|
+
elsif parts[0] == 'env'
|
1043
|
+
@mapping.clear
|
1044
|
+
@clear = false
|
1045
|
+
end
|
1046
|
+
msgs = []
|
1047
|
+
idx = 1
|
1048
|
+
while idx < parts.length
|
1049
|
+
name = parts[idx]
|
1050
|
+
value = parts[idx + 1]
|
1051
|
+
idx += 2
|
1052
|
+
msgs.push("#{name} : #{@mapping[name]} -> #{value}") if @mapping.has_key? name
|
1053
|
+
@mapping[name] = value
|
1054
|
+
end
|
1055
|
+
print_lines msgs
|
1056
|
+
end
|
1057
|
+
|
1058
|
+
def command_part
|
1059
|
+
command = []
|
1060
|
+
command.push('env-clear') if @clear
|
1061
|
+
@mapping.each_pair { |name, val| command.concat [ 'env', name, val ] }
|
1062
|
+
return command
|
1063
|
+
end
|
1064
|
+
end
|
1065
|
+
$env_handler = EnvHandler.new
|
1066
|
+
|
1067
|
+
class ArgHandler < Handler
|
1068
|
+
attr_reader :command_part
|
1069
|
+
|
1070
|
+
def initialize
|
1071
|
+
@syntax = {
|
1072
|
+
:root => [ -1, :arg, :argadd, :argwhat ],
|
1073
|
+
:arg => [ 1, 'arg', :values ],
|
1074
|
+
:argadd => [ 2, 'arg+', :string, :values ],
|
1075
|
+
:argwhat => 'arg?',
|
1076
|
+
:values => [ 0, :string, :values ],
|
1077
|
+
:help => {
|
1078
|
+
:arg => "Clear argument list first and add to argument list.",
|
1079
|
+
:argadd => "Add to argument list.",
|
1080
|
+
:argwhat => "Print current program argument list."
|
1081
|
+
}
|
1082
|
+
}
|
1083
|
+
@command_part = []
|
1084
|
+
register
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
def handle(parts)
|
1088
|
+
if parts[0] == 'arg?'
|
1089
|
+
msgs = []
|
1090
|
+
@command_part.each { |arg| msgs.push "\"#{arg}\"" }
|
1091
|
+
print_lines msgs
|
1092
|
+
return
|
1093
|
+
end
|
1094
|
+
@command_part.clear if parts[0] == 'arg'
|
1095
|
+
@command_part.concat parts[1...parts.length]
|
1096
|
+
end
|
1097
|
+
end
|
1098
|
+
$arg_handler = ArgHandler.new
|
1099
|
+
|
1100
|
+
class ChannelHandler < Handler
|
1101
|
+
def initialize
|
1102
|
+
@syntax = {
|
1103
|
+
:root => [ -1, :channelin, :channelout, :channelwhat ],
|
1104
|
+
:channelin => [ 'channel', 'in', :informat ],
|
1105
|
+
:channelout => [ 'channel', :outchannel, :outformat ],
|
1106
|
+
:channelwhat => 'channel?',
|
1107
|
+
:informat => [ -1, 'json', 'none' ],
|
1108
|
+
:outchannel => [ -1, 'out', 'err' ],
|
1109
|
+
:outformat => [ -1, 'json', 'bytes', 'none' ],
|
1110
|
+
:help => {
|
1111
|
+
:channelin => "Set program stdin format to JSON for data etc. input or closed if none.",
|
1112
|
+
:channelout => "Set program stdout/stderr format. Bytes is passed on as JSON array and none means to ignore any output.",
|
1113
|
+
:channelwhat => "Print current channel setting."
|
1114
|
+
}
|
1115
|
+
}
|
1116
|
+
@channels = { 'in' => 'json', 'out' => 'json', 'err' => 'bytes' }
|
1117
|
+
register
|
1118
|
+
end
|
1119
|
+
|
1120
|
+
def handle(parts)
|
1121
|
+
if parts[0] == 'channel?'
|
1122
|
+
msgs = []
|
1123
|
+
@channels.each_pair { |c, fmt| msgs.push "#{c} #{fmt}" }
|
1124
|
+
print_lines msgs.sort
|
1125
|
+
return
|
1126
|
+
end
|
1127
|
+
@channels[parts[1]] = parts[2]
|
1128
|
+
end
|
1129
|
+
|
1130
|
+
def command_part
|
1131
|
+
command = []
|
1132
|
+
@channels.each_pair do |c, f|
|
1133
|
+
next if f == 'none'
|
1134
|
+
command.push((c == 'in') ? 'in' : 'out')
|
1135
|
+
command.push((f == 'json') ? 'JSON' : f)
|
1136
|
+
command.push "std#{c}"
|
1137
|
+
end
|
1138
|
+
return command
|
1139
|
+
end
|
1140
|
+
end
|
1141
|
+
$channel_handler = ChannelHandler.new
|
1142
|
+
|
1143
|
+
$actions_run_common = %q(
|
1144
|
+
---
|
1145
|
+
error:
|
1146
|
+
- args_missing: [ "@", run, error, missing, "*" ]
|
1147
|
+
- command_error: [ "@", run, error, identifier, in-use ]
|
1148
|
+
- syntax: [ "@", run, error, "?", argument, unknown ]
|
1149
|
+
- syntax: [ "@", run, error, "?", duplicate, "?" ]
|
1150
|
+
- command_error: [ "@", run, error, change-directory, "?", "?" ]
|
1151
|
+
- command_error: [ "@", run, error, env, argument, duplicate, "?" ]
|
1152
|
+
- command_error: [ "@", run, error, env, argument, invalid, "?" ]
|
1153
|
+
- command_error: [ "@", run, error, in, missing ]
|
1154
|
+
- command_error: [ "@", run, error, in, multiple ]
|
1155
|
+
- command_error: [ "@", run, error, notify, no-input ]
|
1156
|
+
- command_error: [ "@", run, error, out, duplicate ]
|
1157
|
+
- command_error: [ "@", run, error, out, missing ]
|
1158
|
+
- command_error: [ "@", run, error, output, duplicate, "?" ]
|
1159
|
+
- command_error: [ "@", run, error, program, "*" ]
|
1160
|
+
- run_internal: [ "@", run, error, exception ]
|
1161
|
+
- run_internal: [ "@", run, error, no-memory ]
|
1162
|
+
- run_internal: [ "@", run, error, no-processes ]
|
1163
|
+
- run_internal: [ "@", run, error, no-thread ]
|
1164
|
+
- run_internal: [ "@", run, error, pipe ]
|
1165
|
+
- syntax: [ "@", "?", error, argument, invalid ]
|
1166
|
+
- syntax: [ "@", "?", error, argument, not-integer ]
|
1167
|
+
- syntax: [ "@", "?", missing, "*" ]
|
1168
|
+
note:
|
1169
|
+
- run_error_input_failed: [ "@", run, error, input, failed ]
|
1170
|
+
- run_child_error_output_format: [ "@", run, error, format ]
|
1171
|
+
- run_child_error_output_format: [ "@", error, format ]
|
1172
|
+
- run_terminated: [ "@", run, terminated, "?" ]
|
1173
|
+
- run_exit: [ "@", run, exit, "?" ]
|
1174
|
+
- run_signal: [ "@", run, signal, "?" ]
|
1175
|
+
- run_stop: [ "@", run, stopped, "?" ]
|
1176
|
+
- run_continue: [ "@", run, continued ]
|
1177
|
+
bytes:
|
1178
|
+
- bytes: [ "@", run, bytes, "?", "*" ]
|
1179
|
+
)
|
1180
|
+
|
1181
|
+
$actions_bgrun_only = %q(
|
1182
|
+
---
|
1183
|
+
return:
|
1184
|
+
- run_running: [ "@", run, running, "?" ]
|
1185
|
+
note:
|
1186
|
+
- run_closed: [ "@", run, input, closed ]
|
1187
|
+
)
|
1188
|
+
|
1189
|
+
class RunHandler < Handler
|
1190
|
+
@@actions_run_common = YAML.load($actions_run_common)
|
1191
|
+
@@actions_bgrun_only = YAML.load($actions_bgrun_only)
|
1192
|
+
|
1193
|
+
def initialize
|
1194
|
+
@syntax = {
|
1195
|
+
:root => [ 3, 'run', :identifier, :executable, '&' ],
|
1196
|
+
:identifier => [ -1, :null, :int, :string ],
|
1197
|
+
:help => { :root => "Runs executable as identifier. Uses current input, output, channel, env, and arg settings." }
|
1198
|
+
}
|
1199
|
+
run_common = Proc.new do |category, action, message, vars|
|
1200
|
+
m = nil
|
1201
|
+
case category.to_s
|
1202
|
+
when 'error' then m = "ERROR: #{action} : #{message.join(' ')}"
|
1203
|
+
when 'note'
|
1204
|
+
case action.to_s
|
1205
|
+
when 'run_error_input_failed'
|
1206
|
+
m = "Input failed: #{message.first.to_s}"
|
1207
|
+
when 'run_child_error_output_format'
|
1208
|
+
m = "Output format error: #{message.first}"
|
1209
|
+
end
|
1210
|
+
when 'bytes' then m = ''.concat(*vars)
|
1211
|
+
end
|
1212
|
+
print_lines m
|
1213
|
+
end
|
1214
|
+
run_only = Proc.new do |category, action, message, vars|
|
1215
|
+
m = nil
|
1216
|
+
case category.to_s
|
1217
|
+
when 'note'
|
1218
|
+
case action.to_s
|
1219
|
+
when 'run_terminated' then m = "Terminated."
|
1220
|
+
when 'run_exit' then m = "Exit: #{vars.first}" if 0 < vars.first
|
1221
|
+
when 'run_signal' then m = "Signal: #{vars.first}"
|
1222
|
+
when 'run_stop' then m = "Stopped by signal: #{vars.first}"
|
1223
|
+
when 'run_continue' then m = "Continued."
|
1224
|
+
end
|
1225
|
+
end
|
1226
|
+
print_lines m
|
1227
|
+
end
|
1228
|
+
@actions = PatternAction.new([ @@actions_run_common ],
|
1229
|
+
[ run_only, run_common ])
|
1230
|
+
bgrun_only = Proc.new do |category, action, message, vars|
|
1231
|
+
m = nil
|
1232
|
+
case category.to_s
|
1233
|
+
when 'return'
|
1234
|
+
case action.to_s
|
1235
|
+
when 'run_running' then m = "PID: #{vars.first}"
|
1236
|
+
end
|
1237
|
+
when 'note'
|
1238
|
+
case action.to_s
|
1239
|
+
when 'run_terminated' then m = "Terminated: #{message.first}"
|
1240
|
+
when 'run_exit'
|
1241
|
+
m = "Exit #{message.first}: #{vars.first}" if 0 < vars.first
|
1242
|
+
when 'run_signal' then m = "Signal #{message.first}: #{vars.first}"
|
1243
|
+
when 'run_stop' then m = "Stopped #{message.first}: #{vars.first}"
|
1244
|
+
when 'run_continue' then m = "Continued: #{message.first}"
|
1245
|
+
end
|
1246
|
+
end
|
1247
|
+
print_lines m
|
1248
|
+
end
|
1249
|
+
@bgactions = PatternAction.new(
|
1250
|
+
[ @@actions_bgrun_only, @@actions_run_common ],
|
1251
|
+
[ bgrun_only, run_common ])
|
1252
|
+
register
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
def handle(parts)
|
1256
|
+
command = [ parts[1], 'run' ]
|
1257
|
+
command.concat $notify_handler.command_part
|
1258
|
+
command.concat $input_handler.command_part
|
1259
|
+
command.concat $output_handler.command_part
|
1260
|
+
command.concat $channel_handler.command_part
|
1261
|
+
command.concat $env_handler.command_part
|
1262
|
+
command.concat [ 'program', parts[2] ]
|
1263
|
+
command.concat $arg_handler.command_part
|
1264
|
+
send(parts.last == '&' ? @bgactions : @actions, command, true)
|
1265
|
+
end
|
1266
|
+
end
|
1267
|
+
RunHandler.new
|
1268
|
+
|
1269
|
+
class PsHandler < Handler
|
1270
|
+
def initialize
|
1271
|
+
@syntax = {
|
1272
|
+
:root => 'ps',
|
1273
|
+
:help => { :root => "List running proceses." }
|
1274
|
+
}
|
1275
|
+
register
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
def handle(parts)
|
1279
|
+
report = [ " PID\t: Identifier" ]
|
1280
|
+
procs = $lackey.process
|
1281
|
+
procs.keys.sort.each { |id| report.push " #{procs[id]}\t: #{id}" }
|
1282
|
+
print_lines report
|
1283
|
+
end
|
1284
|
+
end
|
1285
|
+
PsHandler.new
|
1286
|
+
|
1287
|
+
class KillHandler < Handler
|
1288
|
+
def initialize
|
1289
|
+
@syntax = {
|
1290
|
+
:root => [ 2, 'kill', :process_id, :identifiers ],
|
1291
|
+
:identifiers => [ 0, :process_id, :identifiers ],
|
1292
|
+
:help => { :root => "Terminates processes." }
|
1293
|
+
}
|
1294
|
+
message_proc = Proc.new do |category, action, message, vars|
|
1295
|
+
case category
|
1296
|
+
when :note then print_lines "Missing: #{vars.join(' ')}"
|
1297
|
+
else nil
|
1298
|
+
end
|
1299
|
+
end
|
1300
|
+
@actions = PatternAction.new([{
|
1301
|
+
:note => [ { :missing => [ '@', 'terminate', 'missing', '*' ] } ]
|
1302
|
+
}], [ message_proc ])
|
1303
|
+
register
|
1304
|
+
end
|
1305
|
+
|
1306
|
+
def handle(parts)
|
1307
|
+
send(@actions, [ 'terminate' ].concat(parts[1...parts.length]))
|
1308
|
+
end
|
1309
|
+
end
|
1310
|
+
KillHandler.new
|
1311
|
+
|
1312
|
+
$actions_feed = %q(
|
1313
|
+
---
|
1314
|
+
error:
|
1315
|
+
- args_missing: [ "@", feed, error, missing, "*" ]
|
1316
|
+
- argument_unknown: [ "@", feed, error, "?", argument, unknown ]
|
1317
|
+
- duplicate: [ "@", feed, error, "?", duplicate, "?" ]
|
1318
|
+
- feed_closed: [ "@", feed, error, closed ]
|
1319
|
+
- feed_process: [ "@", feed, error, not-found ]
|
1320
|
+
)
|
1321
|
+
|
1322
|
+
class FeedHandler < Handler
|
1323
|
+
@@actions_feed = YAML.load($actions_feed)
|
1324
|
+
|
1325
|
+
def initialize
|
1326
|
+
@syntax = {
|
1327
|
+
:root => [ 2, 'feed', :process_id, :identifiers ],
|
1328
|
+
:identifiers => [ 0, :process_id, :identifiers ],
|
1329
|
+
:help => { :root => "Pass current input set to processes." }
|
1330
|
+
}
|
1331
|
+
message_proc = Proc.new do |category, action, message, vars|
|
1332
|
+
m = nil
|
1333
|
+
case category.to_s
|
1334
|
+
when 'error'
|
1335
|
+
case action.to_s
|
1336
|
+
when 'args_missing' then m = "Missing: #{vars.join(' ')}"
|
1337
|
+
when 'argument_unknown' then m = "Unknown: #{vars.join(' ')}"
|
1338
|
+
when 'duplicate' then m = "Duplicate: #{vars.join(' ')}"
|
1339
|
+
when 'feed_closed' then m = "Closed."
|
1340
|
+
when 'feed_process' then m = "No process."
|
1341
|
+
end
|
1342
|
+
end
|
1343
|
+
print_lines m
|
1344
|
+
end
|
1345
|
+
@actions = PatternAction.new([ @@actions_feed ], [ message_proc ])
|
1346
|
+
register
|
1347
|
+
end
|
1348
|
+
|
1349
|
+
def handle(parts)
|
1350
|
+
c = $handler['input'].command_part
|
1351
|
+
parts[1...parts.length].each do |procid|
|
1352
|
+
command = [ 'feed', procid ]
|
1353
|
+
command.concat(c) unless c.empty? # Empty feed sends empty JSON object.
|
1354
|
+
send(@actions, command)
|
1355
|
+
end
|
1356
|
+
end
|
1357
|
+
end
|
1358
|
+
FeedHandler.new
|
1359
|
+
|
1360
|
+
class EndFeedHandler < Handler
|
1361
|
+
def initialize
|
1362
|
+
@syntax = {
|
1363
|
+
:root => [ 2, 'close', :process_id, :identifiers ],
|
1364
|
+
:identifiers => [ 0, :process_id, :identifiers ],
|
1365
|
+
:help => { :root => "Closes process input." }
|
1366
|
+
}
|
1367
|
+
notify = Proc.new do |category, action, message, vars|
|
1368
|
+
m = nil
|
1369
|
+
case category
|
1370
|
+
when :note
|
1371
|
+
case action
|
1372
|
+
when :not_open then m = "Closed already: #{vars.join(' ')}"
|
1373
|
+
when :missing then m = "Missing: #{vars.join(' ')}"
|
1374
|
+
end
|
1375
|
+
end
|
1376
|
+
print_lines m
|
1377
|
+
end
|
1378
|
+
@actions = PatternAction.new([{
|
1379
|
+
:note => [ { :not_open => [ '@', 'end-feed', 'not-open', '*' ],
|
1380
|
+
:missing => [ '@', 'end-feed', 'missing', '*' ] } ]
|
1381
|
+
}], [ notify ])
|
1382
|
+
register
|
1383
|
+
end
|
1384
|
+
|
1385
|
+
def handle(parts)
|
1386
|
+
send(@actions, [ 'end-feed' ].concat(parts[1...parts.length]))
|
1387
|
+
end
|
1388
|
+
end
|
1389
|
+
EndFeedHandler.new
|
1390
|
+
|
1391
|
+
class RawHandler < Handler
|
1392
|
+
def initialize
|
1393
|
+
@syntax = {
|
1394
|
+
:root => [ 'raw', :string ],
|
1395
|
+
:help => {
|
1396
|
+
:root => "Pass unchecked JSON input as single string to datalackey." }
|
1397
|
+
}
|
1398
|
+
register
|
1399
|
+
end
|
1400
|
+
|
1401
|
+
def handle(parts)
|
1402
|
+
dump parts[1]
|
1403
|
+
end
|
1404
|
+
end
|
1405
|
+
RawHandler.new
|
1406
|
+
|
1407
|
+
|
1408
|
+
def completer(str)
|
1409
|
+
# See if we can get the command that is being typed.
|
1410
|
+
begin
|
1411
|
+
full = Readline.line_buffer.clone
|
1412
|
+
rescue NotImplementedError
|
1413
|
+
# No context available so just pick matching words.
|
1414
|
+
cands = $handler.keys.grep(/^#{Regexp.escape(str)}/)
|
1415
|
+
cands.concat $lackey.data.keys.grep(/^#{Regexp.escape(str)}/)
|
1416
|
+
cands.concat $lackey.process.keys.grep(/^#{Regexp.escape(str)}/)
|
1417
|
+
# Should add various words from syntax. Basically scan for strings.
|
1418
|
+
cands.sort!
|
1419
|
+
return cands
|
1420
|
+
end
|
1421
|
+
begin
|
1422
|
+
parts = Shellwords.shellsplit(full)
|
1423
|
+
rescue ArgumentError
|
1424
|
+
begin
|
1425
|
+
parts = Shellwords.shellsplit(full + '"')
|
1426
|
+
rescue ArgumentError
|
1427
|
+
parts = Shellwords.shellsplit(full + "'")
|
1428
|
+
end
|
1429
|
+
end
|
1430
|
+
if parts.length < 2 and not full.end_with? ' '
|
1431
|
+
return $handler.keys.sort.grep(/^#{Regexp.escape(str)}/)
|
1432
|
+
end
|
1433
|
+
h = $handler[parts[0]]
|
1434
|
+
return (h.nil? ? [] : h.completion_candidates(parts, str))
|
1435
|
+
end
|
1436
|
+
Readline.completion_proc = Proc.new { |str| completer(str) }
|
1437
|
+
|
1438
|
+
|
1439
|
+
# Load history.
|
1440
|
+
history_dir = arguments.directory.nil? ? Dir.pwd : arguments.directory
|
1441
|
+
history_file = File.join(history_dir, history_file_basename)
|
1442
|
+
if File.exist? history_file
|
1443
|
+
begin
|
1444
|
+
fp = File.new history_file
|
1445
|
+
fp.readlines.each { |line| Readline::HISTORY << line.chomp }
|
1446
|
+
rescue StandardError
|
1447
|
+
STDERR.puts "Failed to read history file: #{history_file}"
|
1448
|
+
end
|
1449
|
+
end
|
1450
|
+
|
1451
|
+
while not $exit_handler.exiting
|
1452
|
+
$output_mutex.synchronize { } # So we do not print prompt during other output.
|
1453
|
+
$in_prompt = true
|
1454
|
+
line = Readline.readline($prompt, false)
|
1455
|
+
$in_prompt = false
|
1456
|
+
break if line.nil?
|
1457
|
+
line.strip!
|
1458
|
+
begin
|
1459
|
+
parts = line.shellsplit
|
1460
|
+
rescue ArgumentError => e
|
1461
|
+
print_lines e.to_s
|
1462
|
+
next
|
1463
|
+
end
|
1464
|
+
next if parts.length == 0
|
1465
|
+
h = $handler[parts[0]]
|
1466
|
+
if h.nil?
|
1467
|
+
print_lines "Unknown command: #{parts[0]}"
|
1468
|
+
next
|
1469
|
+
end
|
1470
|
+
if h.verify(parts)
|
1471
|
+
if $stdin.tty? and (Readline::HISTORY.empty? or Readline::HISTORY[Readline::HISTORY.length - 1] != line)
|
1472
|
+
Readline::HISTORY.push line
|
1473
|
+
end
|
1474
|
+
h.handle(parts)
|
1475
|
+
else
|
1476
|
+
print_lines "Command verify failed."
|
1477
|
+
end
|
1478
|
+
end
|
1479
|
+
|
1480
|
+
# For recall to work, ensure there is exit in case ctrl-D was pressed.
|
1481
|
+
if Readline::HISTORY.empty? or not Readline::HISTORY[Readline::HISTORY.length - 1].start_with? $exit_handler.command_name
|
1482
|
+
Readline::HISTORY.push $exit_handler.command_name
|
1483
|
+
end
|
1484
|
+
|
1485
|
+
# Save history.
|
1486
|
+
if $stdin.tty?
|
1487
|
+
begin
|
1488
|
+
fp = Pathname.new history_file
|
1489
|
+
fp.open('w') do |f|
|
1490
|
+
Readline::HISTORY.to_a.last(history_file_max_lines).each { |l| f.puts l }
|
1491
|
+
end
|
1492
|
+
rescue StandardError
|
1493
|
+
STDERR.puts "Failed to write history file: #{history_file}"
|
1494
|
+
end
|
1495
|
+
end
|
1496
|
+
|
1497
|
+
$lackey_proc.finish
|
1498
|
+
$lackey.close
|
1499
|
+
stderr_discarder.close
|
1500
|
+
$lackey.finish
|
1501
|
+
puts() unless $exit_handler.exiting
|
1502
|
+
if $lackey_proc.exit_code != 0 and not $lackey_proc.exit_code.nil?
|
1503
|
+
puts("datalackey exit: #{$lackey_proc.exit_code}")
|
1504
|
+
end
|
1505
|
+
exit $exit_handler.code
|