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.
@@ -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