shells 0.1.23 → 0.2.0

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