shells 0.1.5

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