shells 0.1.5

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,846 @@
1
+ require 'shells/errors'
2
+
3
+ module Shells
4
+
5
+ ##
6
+ # Provides a base interface for all shells to build on.
7
+ #
8
+ # Instantiating this class will raise an error.
9
+ # All shell sessions should inherit this class.
10
+ class ShellBase
11
+
12
+ ##
13
+ # Raise a QuitNow to tell the shell to stop processing and exit.
14
+ class QuitNow < Exception
15
+
16
+ end
17
+
18
+ ##
19
+ # The options provided to this shell.
20
+ attr_reader :options
21
+
22
+ ##
23
+ # Gets the exit code from the last command if it was retrieved.
24
+ attr_accessor :last_exit_code
25
+
26
+ ##
27
+ # Initializes the shell with the supplied options.
28
+ #
29
+ # These options are common to all shells.
30
+ # +prompt+::
31
+ # Defaults to "~~#". Most special characters will be stripped.
32
+ # +retrieve_exit_code+::
33
+ # Defaults to false. Can also be true.
34
+ # +on_non_zero_exit_code+::
35
+ # Defaults to :ignore. Can also be :raise.
36
+ # +silence_timeout+::
37
+ # Defaults to 0.
38
+ # If greater than zero, will raise an error after waiting this many seconds for a prompt.
39
+ # +command_timeout+::
40
+ # Defaults to 0.
41
+ # If greater than zero, will raise an error after a command runs for this long without finishing.
42
+ #
43
+ # Please check the documentation for each session class for specific shell options.
44
+ #
45
+ # Once the shell is initialized, the shell is yielded to the provided code block which can then interact with
46
+ # the shell. Once the code block completes, the shell is closed and the session object is returned.
47
+ #
48
+ # After completion the session object can only be used to review the +stdout+, +stderr+, and +combined_output+
49
+ # properties. The +exec+ method and any other methods that interact with the shell will no longer be functional.
50
+ def initialize(options = {}, &block)
51
+
52
+ # cannot instantiate a ShellBase
53
+ raise NotImplementedError if self.class == Shells::ShellBase
54
+
55
+ raise ArgumentError, 'A code block is required.' unless block_given?
56
+ raise ArgumentError, '\'options\' must be a hash.' unless options.is_a?(Hash)
57
+
58
+ @options = {
59
+ prompt: '~~#',
60
+ retrieve_exit_code: false,
61
+ on_non_zero_exit_code: :ignore,
62
+ silence_timeout: 0,
63
+ command_timeout: 0
64
+ }.merge( options.inject({}){ |m,(k,v)| m[k.to_sym] = v; m } )
65
+
66
+ @options[:prompt] = @options[:prompt]
67
+ .to_s.strip
68
+ .gsub('!', '#')
69
+ .gsub('$', '#')
70
+ .gsub('\\', '.')
71
+ .gsub('/', '.')
72
+ .gsub('"', '-')
73
+ .gsub('\'', '-')
74
+
75
+ @options[:prompt] = '~~#' if @options[:prompt] == ''
76
+
77
+ raise Shells::InvalidOption, ':on_non_zero_exit_code must be :ignore, :raise, or nil.' unless [:ignore, :raise].include?(@options[:on_non_zero_exit_code])
78
+
79
+ validate_options
80
+ @options.freeze # no more changes to options now.
81
+
82
+ @session_complete = false
83
+ @last_input = Time.now
84
+ debug 'Calling "exec_shell"...'
85
+ exec_shell do
86
+ begin
87
+ debug 'Running "before_init" hooks...'
88
+ run_hook :before_init
89
+ debug 'Calling "exec_prompt"...'
90
+ exec_prompt do
91
+ begin
92
+ debug 'Running "after_init" hooks...'
93
+ run_hook :after_init
94
+ debug 'Executing code block...'
95
+ block.call self
96
+ ensure
97
+ debug 'Running "before_term" hooks...'
98
+ run_hook :before_term
99
+ end
100
+ end
101
+ rescue QuitNow
102
+ debug 'Received QuitNow signal.'
103
+ nil
104
+ rescue Exception => ex
105
+ unless run_hook(:on_exception, ex)
106
+ raise
107
+ end
108
+ ensure
109
+ debug 'Running "after_term" hooks...'
110
+ run_hook :after_term
111
+ end
112
+ end
113
+
114
+ @session_complete = true
115
+ end
116
+
117
+ ##
118
+ # Sets the code to be run when debug messages are processed.
119
+ #
120
+ # The code will receive the debug message as an argument.
121
+ #
122
+ # on_debug do |msg|
123
+ # puts msg
124
+ # end
125
+ #
126
+ def self.on_debug(proc = nil, &block)
127
+ @on_debug =
128
+ if proc.respond_to?(:call)
129
+ proc
130
+ elsif proc && respond_to?(proc.to_s, true)
131
+ method(proc.to_s.to_sym)
132
+ elsif block
133
+ block
134
+ else
135
+ nil
136
+ end
137
+ end
138
+
139
+ ##
140
+ # Adds code to be run before the shell is fully initialized.
141
+ #
142
+ # This code would normally be used to navigate a menu or setup an environment.
143
+ # This method allows you to define that behavior without rewriting the connection code.
144
+ #
145
+ # before_init do |shell|
146
+ # ...
147
+ # end
148
+ #
149
+ # You can also pass the name of a static method.
150
+ #
151
+ # def self.some_init_function(shell)
152
+ # ...
153
+ # end
154
+ #
155
+ # before_init :some_init_function
156
+ #
157
+ def self.before_init(proc = nil, &block)
158
+ add_hook :before_init, proc, &block
159
+ end
160
+
161
+ ##
162
+ # Adds code to be run after the shell is fully initialized but before the session code executes.
163
+ #
164
+ # after_init do |shell|
165
+ # ...
166
+ # end
167
+ #
168
+ # You can also pass the name of a static method.
169
+ #
170
+ # def self.some_init_function(shell)
171
+ # ...
172
+ # end
173
+ #
174
+ # after_init :some_init_function
175
+ #
176
+ def self.after_init(proc = nil, &block)
177
+ add_hook :after_init, proc, &block
178
+ end
179
+
180
+
181
+ ##
182
+ # Adds code to be run before the shell is terminated immediately after executing the session code.
183
+ #
184
+ # This code might also be used to navigate a menu or clean up an environment.
185
+ # This method allows you to define that behavior without rewriting the connection code.
186
+ #
187
+ # This code is guaranteed to be called if the shell initializes correctly.
188
+ # That means if an error is raised in the session code, this will still fire before handling the error.
189
+ #
190
+ # before_term do |shell|
191
+ # ...
192
+ # end
193
+ #
194
+ # You can also pass the name of a static method.
195
+ #
196
+ # def self.some_term_function(shell)
197
+ # ...
198
+ # end
199
+ #
200
+ # before_term :some_term_function
201
+ #
202
+ def self.before_term(proc = nil, &block)
203
+ add_hook :before_term, proc, &block
204
+ end
205
+
206
+ ##
207
+ # Adds code to be run after the shell session is terminated but before closing the shell session.
208
+ #
209
+ # This code might also be used to navigate a menu or clean up an environment.
210
+ # This method allows you to define that behavior without rewriting the connection code.
211
+ #
212
+ # This code is guaranteed to be called if the shell connects correctly.
213
+ # That means if an error is raised in the session code or shell initialization code, this will still fire before
214
+ # closing the shell session.
215
+ #
216
+ # after_term do |shell|
217
+ # ...
218
+ # end
219
+ #
220
+ # You can also pass the name of a static method.
221
+ #
222
+ # def self.some_term_function(shell)
223
+ # ...
224
+ # end
225
+ #
226
+ # after_term :some_term_function
227
+ #
228
+ def self.after_term(proc = nil, &block)
229
+ add_hook :after_term, proc, &block
230
+ end
231
+
232
+
233
+ ##
234
+ # Adds code to be run when an exception occurs.
235
+ #
236
+ # This code will receive the shell as the first argument and the exception as the second.
237
+ # If it handles the exception it should return true, otherwise nil or false.
238
+ #
239
+ # on_exception do |shell, ex|
240
+ # if ex.is_a?(MyExceptionType)
241
+ # ...
242
+ # true
243
+ # else
244
+ # false
245
+ # end
246
+ # end
247
+ #
248
+ # You can also pass the name of a static method.
249
+ #
250
+ # def self.some_exception_handler(shell, ex)
251
+ # ...
252
+ # end
253
+ #
254
+ # on_exception :some_exception_handler
255
+ #
256
+ def self.on_exception(proc = nil, &block)
257
+ add_hook :on_exception, proc, &block
258
+ end
259
+
260
+ ##
261
+ # Defines the line ending used to terminate commands sent to the shell.
262
+ #
263
+ # The default is "\n". If you need "\r\n", "\r", or some other value, simply override this function.
264
+ def line_ending
265
+ "\n"
266
+ end
267
+
268
+ ##
269
+ # Has the session been completed?
270
+ def session_complete?
271
+ @session_complete
272
+ end
273
+
274
+ ##
275
+ # Gets the standard output from the session.
276
+ #
277
+ # The prompts are stripped from the standard ouput as they are encountered.
278
+ # So this will be a list of commands with their output.
279
+ #
280
+ # All line endings are converted to LF characters, so you will not
281
+ # encounter or need to search for CRLF or CR sequences.
282
+ #
283
+ def stdout
284
+ @stdout ||= ''
285
+ end
286
+
287
+ ##
288
+ # Gets the error output from the session.
289
+ #
290
+ # All line endings are converted to LF characters, so you will not
291
+ # encounter or need to search for CRLF or CR sequences.
292
+ #
293
+ def stderr
294
+ @stderr ||= ''
295
+ end
296
+
297
+ ##
298
+ # Gets both the standard output and error output from the session.
299
+ #
300
+ # The prompts will be included in the combined output.
301
+ # There is no attempt to differentiate error output from standard output.
302
+ #
303
+ # This is essentially the definitive log for the session.
304
+ #
305
+ # All line endings are converted to LF characters, so you will not
306
+ # encounter or need to search for CRLF or CR sequences.
307
+ #
308
+ def combined_output
309
+ @stdcomb ||= ''
310
+ end
311
+
312
+ ##
313
+ # Executes a command during the shell session.
314
+ #
315
+ # If called outside of the +new+ block, this will raise an error.
316
+ #
317
+ # The +command+ is the command to execute in the shell.
318
+ #
319
+ # The +options+ can be used to override the exit code behavior.
320
+ # In all cases, the :default option is the same as not providing the option and will cause +exec+
321
+ # to inherit the option from the shell's options.
322
+ #
323
+ # +retrieve_exit_code+::
324
+ # This can be one of :default, true, or false.
325
+ # +on_non_zero_exit_code+::
326
+ # This can be on ot :default, :ignore, or :raise.
327
+ # +silence_timeout+::
328
+ # This can be :default or the number of seconds to wait in silence before timing out.
329
+ # +command_timeout+::
330
+ # This can be :default or the maximum number of seconds to wait for a command to finish before timing out.
331
+ #
332
+ # If provided, the +block+ is a chunk of code that will be processed every time the
333
+ # shell receives output from the program. If the block returns a string, the string
334
+ # will be sent to the shell. This can be used to monitor processes or monitor and
335
+ # interact with processes. The +block+ is optional.
336
+ #
337
+ # shell.exec('sudo -p "password:" nginx restart') do |data,type|
338
+ # return 'super-secret' if /password:$/.match(data)
339
+ # nil
340
+ # end
341
+ #
342
+ def exec(command, options = {}, &block)
343
+ raise Shells::SessionCompleted if session_complete?
344
+
345
+ options ||= {}
346
+ options = { timeout_error: true }.merge(options)
347
+ options = self.options.merge(options.inject({}) { |m,(k,v)| m[k.to_sym] = v; m })
348
+ options[:retrieve_exit_code] = self.options[:retrieve_exit_code] if options[:retrieve_exit_code] == :default
349
+ options[:on_non_zero_exit_code] = self.options[:on_non_zero_exit_code] unless options[:on_non_zero_exit_code] == :default
350
+ options[:silence_timeout] = self.options[:silence_timeout] if options[:silence_timeout] == :default
351
+ options[:command_timeout] = self.options[:command_timeout] if options[:command_timeout] == :default
352
+ ret = ''
353
+
354
+ begin
355
+ push_buffer # store the current buffer and start a fresh buffer
356
+
357
+ # buffer while also passing data to the supplied block.
358
+ if block_given?
359
+ buffer_input(&block)
360
+ end
361
+
362
+ # send the command and wait for the prompt to return.
363
+ debug 'Sending command: ' + command
364
+ send_data command + line_ending
365
+ if wait_for_prompt(options[:silence_timeout], options[:command_timeout], options[:timeout_error])
366
+ # get the output of the command, minus the trailing prompt.
367
+ debug 'Reading output of command...'
368
+ ret = command_output command
369
+
370
+ if options[:retrieve_exit_code]
371
+ self.last_exit_code = get_exit_code
372
+ if options[:on_non_zero_exit_code] == :raise
373
+ raise NonZeroExitCode.new(last_exit_code) unless last_exit_code == 0
374
+ end
375
+ else
376
+ self.last_exit_code = nil
377
+ end
378
+ else
379
+ # A timeout occurred and timeout_error was set to false.
380
+ debug 'Command timed out...'
381
+ self.last_exit_code = :timeout
382
+ ret = combined_output
383
+ end
384
+
385
+ ensure
386
+ # return buffering to normal.
387
+ if block_given?
388
+ buffer_input
389
+ end
390
+
391
+ # restore the original buffer and merge the output from the command.
392
+ pop_merge_buffer
393
+ end
394
+ ret
395
+ end
396
+
397
+ ##
398
+ # Executes a command specifically for the exit code.
399
+ #
400
+ # Does not return the output of the command, only the exit code.
401
+ def exec_for_code(command, options = {}, &block)
402
+ options = (options || {}).merge(retrieve_exit_code: true, on_non_zero_exit_code: :ignore)
403
+ exec command, options, &block
404
+ last_exit_code
405
+ end
406
+
407
+ ##
408
+ # Reads from a file on the device.
409
+ def read_file(path)
410
+ raise ::NotImplementedError
411
+ end
412
+
413
+ ##
414
+ # Writes to a file on the device.
415
+ def write_file(path, data)
416
+ raise ::NotImplementedError
417
+ end
418
+
419
+
420
+ protected
421
+
422
+
423
+
424
+ ##
425
+ # Validates the options provided to the class.
426
+ #
427
+ # You should define this method in your subclass.
428
+ def validate_options #:doc:
429
+ warn "The validate_options() method is not defined on the #{self.class} class."
430
+ end
431
+
432
+ ##
433
+ # Executes a shell session.
434
+ #
435
+ # This method should connect to the shell and then yield.
436
+ # It should not initialize the prompt.
437
+ # When the yielded block returns this method should then disconnect from the shell.
438
+ #
439
+ # You must define this method in your subclass.
440
+ def exec_shell(&block) #:doc:
441
+ raise ::NotImplementedError
442
+ end
443
+
444
+ ##
445
+ # Runs all prompted commands.
446
+ #
447
+ # This method should initialize the shell prompt and then yield.
448
+ #
449
+ # You must define this method in your subclass.
450
+ def exec_prompt(&block) #:doc:
451
+ raise ::NotImplementedError
452
+ end
453
+
454
+ ##
455
+ # Sends data to the shell.
456
+ #
457
+ # You must define this method in your subclass.
458
+ def send_data(data) #:doc:
459
+ raise ::NotImplementedError
460
+ end
461
+
462
+ ##
463
+ # Loops while the block returns any true value.
464
+ #
465
+ # Inside the loop you should check for data being received from the shell and dispatch it to the appropriate
466
+ # hook method (see stdout_received() and stderr_received()). Once input has been cleared you should execute
467
+ # the block and exit unless the block returns a true value.
468
+ #
469
+ # You must define this method in your subclass.
470
+ def loop(&block) #:doc:
471
+ raise ::NotImplementedError
472
+ end
473
+
474
+ ##
475
+ # Register a callback to run when stdout data is received.
476
+ #
477
+ # The block will be passed the data received.
478
+ #
479
+ # You must define this method in your subclass and it should set a hook to be called when data is received.
480
+ def stdout_received(&block) #:doc:
481
+ raise ::NotImplementedError
482
+ end
483
+
484
+ ##
485
+ # Register a callback to run when stderr data is received.
486
+ #
487
+ # The block will be passed the data received.
488
+ #
489
+ # You must define this method in your subclass and it should set a hook to be called when data is received.
490
+ def stderr_received(&block) #:doc:
491
+ raise ::NotImplementedError
492
+ end
493
+
494
+ ##
495
+ # Gets the exit code from the last command.
496
+ #
497
+ # You must define this method in your subclass to utilize exit codes.
498
+ def get_exit_code #:doc:
499
+ raise ::NotImplementedError
500
+ end
501
+
502
+
503
+
504
+
505
+ ##
506
+ # Waits for the prompt to appear at the end of the output.
507
+ #
508
+ # Once the prompt appears, new input can be sent to the shell.
509
+ # This is automatically called in +exec+ so you would only need
510
+ # to call it directly if you were sending data manually to the
511
+ # shell.
512
+ #
513
+ # This method is used internally in the +exec+ method, but there may be legitimate use cases
514
+ # outside of that method as well.
515
+ def wait_for_prompt(silence_timeout = nil, command_timeout = nil, timeout_error = true) #:doc:
516
+ raise Shells::SessionCompleted if session_complete?
517
+
518
+ silence_timeout ||= options[:silence_timeout]
519
+ command_timeout ||= options[:command_timeout]
520
+
521
+ sent_nl_at = nil
522
+ sent_nl_times = 0
523
+ silence_timeout = silence_timeout.to_s.to_f unless silence_timeout.is_a?(Numeric)
524
+ nudge_timeout =
525
+ if silence_timeout > 0
526
+ (silence_timeout / 3) # we want to nudge twice before officially timing out.
527
+ else
528
+ 0
529
+ end
530
+
531
+ command_timeout = command_timeout.to_s.to_f unless command_timeout.is_a?(Numeric)
532
+ timeout =
533
+ if command_timeout > 0
534
+ Time.now + command_timeout
535
+ else
536
+ nil
537
+ end
538
+
539
+ loop do
540
+ last_input = @last_input
541
+
542
+ # Do we need to nudge the shell?
543
+ if nudge_timeout > 0 && (Time.now - last_input) > nudge_timeout
544
+
545
+ # Have we previously nudged the shell?
546
+ if sent_nl_times > 2
547
+ raise Shells::SilenceTimeout if timeout_error
548
+ return false
549
+ else
550
+ sent_nl_times = (sent_nl_at.nil? || sent_nl_at < last_input) ? 1 : (sent_nl_times + 1)
551
+ sent_nl_at = Time.now
552
+
553
+ send_data line_ending
554
+
555
+ # wait a bit longer...
556
+ @last_input = sent_nl_at
557
+ end
558
+ end
559
+
560
+ if timeout && Time.now > timeout
561
+ raise Shells::CommandTimeout if timeout_error
562
+ return false
563
+ end
564
+
565
+ !(combined_output =~ prompt_match)
566
+ end
567
+
568
+ pos = combined_output =~ prompt_match
569
+ if combined_output[pos - 1] != "\n"
570
+ # no newline before prompt, fix that.
571
+ self.combined_output = combined_output[0...pos] + "\n" + combined_output[pos..-1]
572
+ end
573
+ if stdout[-1] != "\n"
574
+ # no newline at end, fix that.
575
+ self.stdout <<= "\n"
576
+ end
577
+
578
+ true
579
+ end
580
+
581
+ ##
582
+ # Sets the block to call when data is received.
583
+ #
584
+ # If no block is provided, then the shell will simply log all output from the program.
585
+ # If a block is provided, it will be passed the data as it is received. If the block
586
+ # returns a string, then that string will be sent to the shell.
587
+ #
588
+ # This method is called internally in the +exec+ method, but there may be legitimate use
589
+ # cases outside of that method as well.
590
+ def buffer_input(&block) #:doc:
591
+ raise Shells::SessionCompleted if session_complete?
592
+ block ||= Proc.new { }
593
+ stdout_received do |data|
594
+ @last_input = Time.now
595
+ append_stdout strip_ansi_escape(data), &block
596
+ end
597
+ stderr_received do |data|
598
+ @last_input = Time.now
599
+ append_stderr strip_ansi_escap(data), &block
600
+ end
601
+ end
602
+
603
+ ##
604
+ # Pushes the buffers for output capture.
605
+ #
606
+ # This method is called internally in the +exec+ method, but there may be legitimate use
607
+ # cases outside of that method as well.
608
+ def push_buffer #:doc:
609
+ raise Shells::SessionCompleted if session_complete?
610
+ # push the buffer so we can get the output of a command.
611
+ debug 'Pushing buffer >>'
612
+ stdout_hist.push stdout
613
+ stderr_hist.push stderr
614
+ stdcomb_hist.push combined_output
615
+ self.stdout = ''
616
+ self.stderr = ''
617
+ self.combined_output = ''
618
+ end
619
+
620
+ ##
621
+ # Pops the buffers and merges the captured output.
622
+ #
623
+ # This method is called internally in the +exec+ method, but there may be legitimate use
624
+ # cases outside of that method as well.
625
+ def pop_merge_buffer #:doc:
626
+ raise Shells::SessionCompleted if session_complete?
627
+ # almost a standard pop, however we want to merge history with current.
628
+ debug 'Merging buffer <<'
629
+ if (hist = stdout_hist.pop)
630
+ self.stdout = hist + stdout
631
+ end
632
+ if (hist = stderr_hist.pop)
633
+ self.stderr = hist + stderr
634
+ end
635
+ if (hist = stdcomb_hist.pop)
636
+ self.combined_output = hist + combined_output
637
+ end
638
+ end
639
+
640
+ ##
641
+ # Pops the buffers and discards the captured output.
642
+ #
643
+ # This method is used internally in the +get_exit_code+ method, but there may be legitimate use
644
+ # cases outside of that method as well.
645
+ def pop_discard_buffer #:doc:
646
+ raise Shells::SessionCompleted if session_complete?
647
+ # a standard pop discarding current data and retrieving the history.
648
+ debug 'Discarding buffer <<'
649
+ if (hist = stdout_hist.pop)
650
+ @stdout = hist
651
+ end
652
+ if (hist = stderr_hist.pop)
653
+ @stderr = hist
654
+ end
655
+ if (hist = stdcomb_hist.pop)
656
+ @stdcomb = hist
657
+ end
658
+ end
659
+
660
+ ##
661
+ # Processes a debug message.
662
+ def self.debug(msg) #:doc:
663
+ @debug_proc ||= instance_variable_defined?(:@on_debug) ? (instance_variable_get(:@on_debug) || ->(_) { }) : ->(_){ }
664
+ @debug_proc.call(msg)
665
+ end
666
+
667
+ ##
668
+ # Processes a debug message for an instance.
669
+ def debug(msg) #:nodoc:
670
+ self.class.debug msg
671
+ end
672
+
673
+ ##
674
+ # Sets the prompt to the value temporarily for execution of the code block.
675
+ #
676
+ # The prompt is automatically reset after completion or failure of the code block.
677
+ #
678
+ # If no code block is provided this essentially only resets the prompt.
679
+ def temporary_prompt(prompt)
680
+ raise Shells::SessionCompleted if session_complete?
681
+ begin
682
+ @prompt_match = prompt.is_a?(Regexp) ? prompt : /#{prompt}[ \t]*$/
683
+
684
+ yield if block_given?
685
+ ensure
686
+ @prompt_match = nil
687
+ end
688
+ end
689
+
690
+ ##
691
+ # Allows you to change the :quit option inside of a session.
692
+ #
693
+ # This is useful if you need to change the quit command for some reason.
694
+ # e.g. - Changing the command to "reboot".
695
+ def change_quit(quit_command)
696
+ raise Shells::SessionCompleted if session_complete?
697
+ opts = options.dup
698
+ opts[:quit] = quit_command
699
+ opts.freeze
700
+ @options = opts
701
+ end
702
+
703
+
704
+ private
705
+
706
+
707
+ def self.add_hook(hook_name, proc, &block)
708
+ hooks[hook_name] ||= []
709
+
710
+ if proc.respond_to?(:call)
711
+ hooks[hook_name] << proc
712
+ elsif proc.is_a?(Symbol) || proc.is_a?(String)
713
+ if self.respond_to?(proc, true)
714
+ hooks[hook_name] << method(proc.to_sym)
715
+ end
716
+ elsif proc
717
+ raise ArgumentError, 'proc must respond to :call method or be the name of a static method in this class'
718
+ end
719
+
720
+ if block
721
+ hooks[hook_name] << block
722
+ end
723
+
724
+ end
725
+
726
+ def run_hook(hook_name, *args)
727
+ (self.class.hooks[hook_name] || []).each do |hook|
728
+ result = hook.call(self, *args)
729
+ return true if result.is_a?(TrueClass)
730
+ end
731
+ false
732
+ end
733
+
734
+ def self.hooks
735
+ @hooks ||= {}
736
+ end
737
+
738
+
739
+ def stdout=(value)
740
+ @stdout = value
741
+ end
742
+
743
+ def stderr=(value)
744
+ @stderr = value
745
+ end
746
+
747
+ def combined_output=(value)
748
+ @stdcomb = value
749
+ end
750
+
751
+ def append_stdout(data, &block)
752
+ # Combined output gets the prompts,
753
+ # but stdout will be without prompts.
754
+ data = reduce_newlines data
755
+ for_stdout = if (pos = (data =~ prompt_match))
756
+ data[0...pos]
757
+ else
758
+ data
759
+ end
760
+
761
+ self.stdout <<= for_stdout
762
+ self.combined_output <<= data
763
+
764
+ if block_given?
765
+ result = block.call(for_stdout, :stdout)
766
+ if result && result.is_a?(String)
767
+ send_data(result + line_ending)
768
+ end
769
+ end
770
+ end
771
+
772
+ def append_stderr(data, &block)
773
+ data = reduce_newlines data
774
+
775
+ self.stderr <<= data
776
+ self.combined_output <<= data
777
+
778
+ if block_given?
779
+ result = block.call(data, :stderr)
780
+ if result && result.is_a?(String)
781
+ send_data(result + line_ending)
782
+ end
783
+ end
784
+ end
785
+
786
+ def reduce_newlines(data)
787
+ data.gsub("\r\n", "\n").gsub(" \r", "").gsub("\r", "")
788
+ end
789
+
790
+ def command_output(command)
791
+ # get everything except for the ending prompt.
792
+ ret =
793
+ if (prompt_pos = combined_output =~ prompt_match)
794
+ combined_output[0...prompt_pos]
795
+ else
796
+ combined_output
797
+ end
798
+
799
+ possible_starts = [
800
+ command,
801
+ options[:prompt] + command,
802
+ options[:prompt] + ' ' + command
803
+ ]
804
+
805
+ # Go until we run out of data or we find one of the possible command starts.
806
+ # Note that we EXPECT the command to the first line of the output from the command because we expect the
807
+ # shell to echo it back to us.
808
+ result_cmd,_,result_data = ret.partition("\n")
809
+ until result_data.to_s.strip == '' || possible_starts.include?(result_cmd)
810
+ result_cmd,_,result_data = result_data.partition("\n")
811
+ end
812
+
813
+ result_data
814
+ end
815
+
816
+ def strip_ansi_escape(data)
817
+ data
818
+ .gsub(/\e\[(\d+;?)*[ABCDEFGHfu]/, "\n") # any of the "set cursor position" CSI commands.
819
+ .gsub(/\e\[=?(\d+;?)*[A-Za-z]/,'') # \e[#;#;#A or \e[=#;#;#A basically all the CSI commands except ...
820
+ .gsub(/\e\[(\d+;"[^"]+";?)+p/, '') # \e[#;"A"p
821
+ .gsub(/\e[NOc]./,'?') # any of the alternate character set commands.
822
+ .gsub(/\e[P_\]^X][^\e\a]*(\a|(\e\\))/,'') # any string command
823
+ .gsub(/[\x00\x08\x0B\x0C\x0E-\x1F]/, '') # any non-printable characters (notice \x0A (LF) and \x0D (CR) are left as is).
824
+ .gsub("\t", ' ') # turn tabs into spaces.
825
+ end
826
+
827
+ def stdout_hist
828
+ @stdout_hist ||= []
829
+ end
830
+
831
+ def stderr_hist
832
+ @stderr_hist ||= []
833
+ end
834
+
835
+ def stdcomb_hist
836
+ @stdcomb_hist ||= []
837
+ end
838
+
839
+ def prompt_match
840
+ # allow for trailing spaces or tabs, but no other whitespace.
841
+ @prompt_match ||= /#{@options[:prompt]}[ \t]*$/
842
+ end
843
+
844
+ end
845
+
846
+ end