shells 0.1.23 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,5 @@
1
1
  require 'shells/errors'
2
+ require 'thread'
2
3
 
3
4
  module Shells
4
5
 
@@ -6,878 +7,27 @@ module Shells
6
7
  # Provides a base interface for all shells to build on.
7
8
  #
8
9
  # Instantiating this class will raise an error.
9
- # All shell sessions should inherit this class.
10
+ # All shell sessions should inherit this class and override the necessary interface methods.
10
11
  class ShellBase
11
12
 
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
13
+ def inspect
14
+ "#<#{self.class}:0x#{object_id.to_s(16).rjust(12,'0')} #{options.reject{|k,v| k == :password}.inspect}>"
178
15
  end
179
16
 
17
+ end
18
+ end
180
19
 
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 [:raise, :ignore].include?(options[:on_non_zero_exit_code])
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
- options[:command_is_echoed] = true if options[:command_is_echoed].nil?
353
- ret = ''
354
-
355
- begin
356
- push_buffer # store the current buffer and start a fresh buffer
357
-
358
- # buffer while also passing data to the supplied block.
359
- if block_given?
360
- buffer_input(&block)
361
- end
362
-
363
- # send the command and wait for the prompt to return.
364
- debug 'Sending command: ' + command
365
- send_data command + line_ending
366
- if wait_for_prompt(options[:silence_timeout], options[:command_timeout], options[:timeout_error])
367
- # get the output of the command, minus the trailing prompt.
368
- debug 'Reading output of command...'
369
- ret = command_output command, options[:command_is_echoed]
370
-
371
- if options[:retrieve_exit_code]
372
- self.last_exit_code = get_exit_code
373
- if options[:on_non_zero_exit_code] == :raise
374
- raise NonZeroExitCode.new(last_exit_code) unless last_exit_code == 0
375
- end
376
- else
377
- self.last_exit_code = nil
378
- end
379
- else
380
- # A timeout occurred and timeout_error was set to false.
381
- debug 'Command timed out...'
382
- self.last_exit_code = :timeout
383
- ret = combined_output
384
- end
385
-
386
- ensure
387
- # return buffering to normal.
388
- if block_given?
389
- buffer_input
390
- end
391
-
392
- # restore the original buffer and merge the output from the command.
393
- pop_merge_buffer
394
- end
395
- ret
396
- end
397
-
398
- ##
399
- # Executes a command specifically for the exit code.
400
- #
401
- # Does not return the output of the command, only the exit code.
402
- def exec_for_code(command, options = {}, &block)
403
- options = (options || {}).merge(retrieve_exit_code: true, on_non_zero_exit_code: :ignore)
404
- exec command, options, &block
405
- last_exit_code
406
- end
407
-
408
- ##
409
- # Executes a command ignoring any exit code.
410
- #
411
- # Returns the output of the command and does not even retrieve the exit code.
412
- def exec_ignore_code(command, options = {}, &block)
413
- options = (options || {}).merge(retrieve_exit_code: false, on_non_zero_exit_code: :ignore)
414
- exec command, options, &block
415
- end
416
-
417
- ##
418
- # Reads from a file on the device.
419
- def read_file(path)
420
- raise ::NotImplementedError
421
- end
422
-
423
- ##
424
- # Writes to a file on the device.
425
- def write_file(path, data)
426
- raise ::NotImplementedError
427
- end
428
-
429
-
430
- protected
431
-
432
- ##
433
- # Validates the options provided to the class.
434
- #
435
- # You should define this method in your subclass.
436
- def validate_options #:doc:
437
- warn "The validate_options() method is not defined on the #{self.class} class."
438
- end
439
-
440
- ##
441
- # Executes a shell session.
442
- #
443
- # This method should connect to the shell and then yield.
444
- # It should not initialize the prompt.
445
- # When the yielded block returns this method should then disconnect from the shell.
446
- #
447
- # You must define this method in your subclass.
448
- def exec_shell(&block) #:doc:
449
- raise ::NotImplementedError
450
- end
451
-
452
- ##
453
- # Runs all prompted commands.
454
- #
455
- # This method should initialize the shell prompt and then yield.
456
- #
457
- # You must define this method in your subclass.
458
- def exec_prompt(&block) #:doc:
459
- raise ::NotImplementedError
460
- end
461
-
462
- ##
463
- # Sends data to the shell.
464
- #
465
- # You must define this method in your subclass.
466
- def send_data(data) #:doc:
467
- raise ::NotImplementedError
468
- end
469
-
470
- ##
471
- # Loops while the block returns any true value.
472
- #
473
- # Inside the loop you should check for data being received from the shell and dispatch it to the appropriate
474
- # hook method (see stdout_received() and stderr_received()). Once input has been cleared you should execute
475
- # the block and exit unless the block returns a true value.
476
- #
477
- # You must define this method in your subclass.
478
- def loop(&block) #:doc:
479
- raise ::NotImplementedError
480
- end
481
-
482
- ##
483
- # Register a callback to run when stdout data is received.
484
- #
485
- # The block will be passed the data received.
486
- #
487
- # You must define this method in your subclass and it should set a hook to be called when data is received.
488
- def stdout_received(&block) #:doc:
489
- raise ::NotImplementedError
490
- end
491
-
492
- ##
493
- # Register a callback to run when stderr data is received.
494
- #
495
- # The block will be passed the data received.
496
- #
497
- # You must define this method in your subclass and it should set a hook to be called when data is received.
498
- def stderr_received(&block) #:doc:
499
- raise ::NotImplementedError
500
- end
501
-
502
- ##
503
- # Gets the exit code from the last command.
504
- #
505
- # You must define this method in your subclass to utilize exit codes.
506
- def get_exit_code #:doc:
507
- raise ::NotImplementedError
508
- end
509
-
510
-
511
-
512
-
513
- ##
514
- # Waits for the prompt to appear at the end of the output.
515
- #
516
- # Once the prompt appears, new input can be sent to the shell.
517
- # This is automatically called in +exec+ so you would only need
518
- # to call it directly if you were sending data manually to the
519
- # shell.
520
- #
521
- # This method is used internally in the +exec+ method, but there may be legitimate use cases
522
- # outside of that method as well.
523
- def wait_for_prompt(silence_timeout = nil, command_timeout = nil, timeout_error = true) #:doc:
524
- raise Shells::SessionCompleted if session_complete?
525
-
526
- silence_timeout ||= options[:silence_timeout]
527
- command_timeout ||= options[:command_timeout]
528
-
529
- sent_nl_at = nil
530
- sent_nl_times = 0
531
- silence_timeout = silence_timeout.to_s.to_f unless silence_timeout.is_a?(Numeric)
532
- nudge_timeout =
533
- if silence_timeout > 0
534
- (silence_timeout / 3) # we want to nudge twice before officially timing out.
535
- else
536
- 0
537
- end
538
-
539
- command_timeout = command_timeout.to_s.to_f unless command_timeout.is_a?(Numeric)
540
- timeout =
541
- if command_timeout > 0
542
- Time.now + command_timeout
543
- else
544
- nil
545
- end
546
-
547
- loop do
548
- Thread.pass
549
-
550
- last_input = @last_input
551
-
552
- # Do we need to nudge the shell?
553
- if nudge_timeout > 0 && (Time.now - last_input) > nudge_timeout
554
-
555
- # Have we previously nudged the shell?
556
- if sent_nl_times > 2
557
- raise Shells::SilenceTimeout if timeout_error
558
- return false
559
- else
560
- sent_nl_times = (sent_nl_at.nil? || sent_nl_at < last_input) ? 1 : (sent_nl_times + 1)
561
- sent_nl_at = Time.now
562
-
563
- send_data line_ending
564
-
565
- # wait a bit longer...
566
- @last_input = sent_nl_at
567
- end
568
- end
569
-
570
- if timeout && Time.now > timeout
571
- raise Shells::CommandTimeout if timeout_error
572
- return false
573
- end
574
-
575
- !(combined_output =~ prompt_match)
576
- end
577
-
578
- pos = (combined_output =~ prompt_match)
579
- if combined_output[pos - 1] != "\n"
580
- # no newline before prompt, fix that.
581
- self.combined_output = combined_output[0...pos] + "\n" + combined_output[pos..-1]
582
- end
583
- if stdout[-1] != "\n"
584
- # no newline at end, fix that.
585
- self.stdout <<= "\n"
586
- end
587
-
588
- true
589
- end
590
-
591
- ##
592
- # Sets the block to call when data is received.
593
- #
594
- # If no block is provided, then the shell will simply log all output from the program.
595
- # If a block is provided, it will be passed the data as it is received. If the block
596
- # returns a string, then that string will be sent to the shell.
597
- #
598
- # This method is called internally in the +exec+ method, but there may be legitimate use
599
- # cases outside of that method as well.
600
- def buffer_input(&block) #:doc:
601
- raise Shells::SessionCompleted if session_complete?
602
- block ||= Proc.new { }
603
- stdout_received do |data|
604
- @last_input = Time.now
605
- append_stdout strip_ansi_escape(data), &block
606
- end
607
- stderr_received do |data|
608
- @last_input = Time.now
609
- append_stderr strip_ansi_escape(data), &block
610
- end
611
- end
612
-
613
- ##
614
- # Pushes the buffers for output capture.
615
- #
616
- # This method is called internally in the +exec+ method, but there may be legitimate use
617
- # cases outside of that method as well.
618
- def push_buffer #:doc:
619
- raise Shells::SessionCompleted if session_complete?
620
- # push the buffer so we can get the output of a command.
621
- debug 'Pushing buffer >>'
622
- stdout_hist.push stdout
623
- stderr_hist.push stderr
624
- stdcomb_hist.push combined_output
625
- self.stdout = ''
626
- self.stderr = ''
627
- self.combined_output = ''
628
- end
629
-
630
- ##
631
- # Pops the buffers and merges the captured output.
632
- #
633
- # This method is called internally in the +exec+ method, but there may be legitimate use
634
- # cases outside of that method as well.
635
- def pop_merge_buffer #:doc:
636
- raise Shells::SessionCompleted if session_complete?
637
- # almost a standard pop, however we want to merge history with current.
638
- debug 'Merging buffer <<'
639
- if (hist = stdout_hist.pop)
640
- self.stdout = hist + stdout
641
- end
642
- if (hist = stderr_hist.pop)
643
- self.stderr = hist + stderr
644
- end
645
- if (hist = stdcomb_hist.pop)
646
- self.combined_output = hist + combined_output
647
- end
648
- end
649
-
650
- ##
651
- # Pops the buffers and discards the captured output.
652
- #
653
- # This method is used internally in the +get_exit_code+ method, but there may be legitimate use
654
- # cases outside of that method as well.
655
- def pop_discard_buffer #:doc:
656
- raise Shells::SessionCompleted if session_complete?
657
- # a standard pop discarding current data and retrieving the history.
658
- debug 'Discarding buffer <<'
659
- if (hist = stdout_hist.pop)
660
- @stdout = hist
661
- end
662
- if (hist = stderr_hist.pop)
663
- @stderr = hist
664
- end
665
- if (hist = stdcomb_hist.pop)
666
- @stdcomb = hist
667
- end
668
- end
669
-
670
- ##
671
- # Processes a debug message.
672
- def self.debug(msg) #:doc:'
673
- @on_debug&.call(msg)
674
- end
675
-
676
- ##
677
- # Processes a debug message for an instance.
678
- def debug(msg) #:nodoc:
679
- self.class.debug msg
680
- end
681
-
682
- ##
683
- # Sets the prompt to the value temporarily for execution of the code block.
684
- #
685
- # The prompt is automatically reset after completion or failure of the code block.
686
- #
687
- # If no code block is provided this essentially only resets the prompt.
688
- def temporary_prompt(prompt)
689
- raise Shells::SessionCompleted if session_complete?
690
- begin
691
- @prompt_match = prompt.is_a?(Regexp) ? prompt : /#{prompt}[ \t]*$/
692
-
693
- yield if block_given?
694
- ensure
695
- @prompt_match = nil
696
- end
697
- end
698
-
699
- ##
700
- # Allows you to change the :quit option inside of a session.
701
- #
702
- # This is useful if you need to change the quit command for some reason.
703
- # e.g. - Changing the command to "reboot".
704
- def change_quit(quit_command)
705
- raise Shells::SessionCompleted if session_complete?
706
- opts = options.dup
707
- opts[:quit] = quit_command
708
- opts.freeze
709
- @options = opts
710
- end
711
-
712
-
713
- private
714
-
715
-
716
- def self.add_hook(hook_name, proc, &block)
717
- hooks[hook_name] ||= []
718
-
719
- if proc.respond_to?(:call)
720
- hooks[hook_name] << proc
721
- elsif proc.is_a?(Symbol) || proc.is_a?(String)
722
- if self.respond_to?(proc, true)
723
- hooks[hook_name] << method(proc.to_sym)
724
- end
725
- elsif proc
726
- raise ArgumentError, 'proc must respond to :call method or be the name of a static method in this class'
727
- end
728
-
729
- if block
730
- hooks[hook_name] << block
731
- end
732
-
733
- end
734
-
735
- def run_hook(hook_name, *args)
736
- (self.class.hooks[hook_name] || []).each do |hook|
737
- result = hook.call(self, *args)
738
- return true if result.is_a?(TrueClass)
739
- end
740
- false
741
- end
742
-
743
- def self.hooks
744
- @hooks ||= {}
745
- end
746
-
747
-
748
- def stdout=(value)
749
- @stdout = value
750
- end
751
-
752
- def stderr=(value)
753
- @stderr = value
754
- end
755
-
756
- def combined_output=(value)
757
- @stdcomb = value
758
- end
759
-
760
- def append_stdout(data, &block)
761
- # Combined output gets the prompts,
762
- # but stdout will be without prompts.
763
- data = reduce_newlines data
764
- for_stdout = if (pos = (data =~ prompt_match))
765
- data[0...pos]
766
- else
767
- data
768
- end
769
-
770
- self.stdout <<= for_stdout
771
- self.combined_output <<= data
772
-
773
- if block_given?
774
- result = block.call(for_stdout, :stdout)
775
- if result && result.is_a?(String)
776
- send_data(result + line_ending)
777
- end
778
- end
779
- end
780
-
781
- def append_stderr(data, &block)
782
- data = reduce_newlines data
783
-
784
- self.stderr <<= data
785
- self.combined_output <<= data
786
-
787
- if block_given?
788
- result = block.call(data, :stderr)
789
- if result && result.is_a?(String)
790
- send_data(result + line_ending)
791
- end
792
- end
793
- end
794
-
795
- def reduce_newlines(data)
796
- data.gsub("\r\n", "\n").gsub(" \r", "").gsub("\r", "")
797
- end
798
-
799
- def command_output(command, expect_command = true)
800
- # get everything except for the ending prompt.
801
- ret =
802
- if (prompt_pos = (combined_output =~ prompt_match))
803
- combined_output[0...prompt_pos]
804
- else
805
- combined_output
806
- end
807
-
808
-
809
- if expect_command
810
- command_regex = command_match(command)
811
-
812
- # Go until we run out of data or we find one of the possible command starts.
813
- # Note that we EXPECT the command to the first line of the output from the command because we expect the
814
- # shell to echo it back to us.
815
- result_cmd,_,result_data = ret.partition("\n")
816
- until result_data.to_s.strip == '' || result_cmd.strip =~ command_regex
817
- result_cmd,_,result_data = result_data.partition("\n")
818
- end
819
-
820
- if result_cmd.nil? || !(result_cmd =~ command_regex)
821
- STDERR.puts "SHELL WARNING: Failed to match #{command_regex.inspect}."
822
- end
823
-
824
- result_data
825
- else
826
- ret
827
- end
828
- end
829
-
830
- def strip_ansi_escape(data)
831
- data
832
- .gsub(/\e\[(\d+;?)*[ABCDEFGHfu]/, "\n") # any of the "set cursor position" CSI commands.
833
- .gsub(/\e\[=?(\d+;?)*[A-Za-z]/,'') # \e[#;#;#A or \e[=#;#;#A basically all the CSI commands except ...
834
- .gsub(/\e\[(\d+;"[^"]+";?)+p/, '') # \e[#;"A"p
835
- .gsub(/\e[NOc]./,'?') # any of the alternate character set commands.
836
- .gsub(/\e[P_\]^X][^\e\a]*(\a|(\e\\))/,'') # any string command
837
- .gsub(/[\x00\x08\x0B\x0C\x0E-\x1F]/, '') # any non-printable characters (notice \x0A (LF) and \x0D (CR) are left as is).
838
- .gsub("\t", ' ') # turn tabs into spaces.
839
- end
840
-
841
- def stdout_hist
842
- @stdout_hist ||= []
843
- end
844
-
845
- def stderr_hist
846
- @stderr_hist ||= []
847
- end
848
-
849
- def stdcomb_hist
850
- @stdcomb_hist ||= []
851
- end
852
-
853
- def regex_escape(text)
854
- text
855
- .gsub('\\', '\\\\')
856
- .gsub('[', '\\[')
857
- .gsub(']', '\\]')
858
- .gsub('(', '\\(')
859
- .gsub(')', '\\)')
860
- .gsub('.', '\\.')
861
- .gsub('*', '\\*')
862
- .gsub('+', '\\+')
863
- .gsub('?', '\\?')
864
- .gsub('{', '\\{')
865
- .gsub('}', '\\}')
866
- .gsub('$', '\\$')
867
- .gsub('^', '\\^')
868
- end
869
-
870
- def command_match(command)
871
- p = regex_escape @options[:prompt]
872
- c = regex_escape command
873
- /\A(?:#{p}\s*)?#{c}[ \t]*\z/
874
- end
875
20
 
876
- def prompt_match
877
- # allow for trailing spaces or tabs, but no other whitespace.
878
- @prompt_match ||= /#{regex_escape @options[:prompt]}[ \t]*$/
879
- end
21
+ require 'shells/shell_base/hooks'
880
22
 
881
- end
23
+ require 'shells/shell_base/sync'
24
+ require 'shells/shell_base/debug'
25
+ require 'shells/shell_base/options'
26
+ require 'shells/shell_base/interface' # methods to override in derived classes.
27
+ require 'shells/shell_base/input'
28
+ require 'shells/shell_base/output'
29
+ require 'shells/shell_base/regex_escape'
30
+ require 'shells/shell_base/prompt'
31
+ require 'shells/shell_base/exec'
32
+ require 'shells/shell_base/run'
882
33
 
883
- end