backticks 1.0.0rc1 → 1.0.0rc2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a446a43c0be0d08f7ad35f3f9a2fa7791f66229d
4
- data.tar.gz: 2fc8f0e3176dae7628cb08225c63f95d97002e8f
3
+ metadata.gz: 51c5bc30064cde134337b459ec829f6099a65c21
4
+ data.tar.gz: 950628c7a317d251eca8bc29c89cf474259b2f63
5
5
  SHA512:
6
- metadata.gz: 96d973bd4ef27a7bfcfecf7f8cb9e7ea0de2bb05f758aa42fb49c5438ba372230c5a77d07737da4e9492c0109533992ca9f1ad9ccf779fe442e7907d8bd5a7b7
7
- data.tar.gz: 378d42242e3efbc5817c89266a34e620ca59ddfe6fc92411b56671fe5aa7104088a5f934235f8b1f9dbcb66c589ef71f8c1c1904f5a755074392317817c59172
6
+ metadata.gz: 6887deef8f9ce8f4c1c998951b135a78e567a6244eaa9b80cd132fbb22d7700bc1c64470b010cfe9cd91b10a4dcc9432cf0bd4b9a5df5fbc9d73e365217f9b66
7
+ data.tar.gz: cdb8c7c4e9d7776592921ca11c29c930c3d1687ecf82a0058ffc8088d14c0482fcec31dfac9fa56853807984834fd2d9c093cf0c5f82ab6898a58955eff695c7
@@ -1,7 +1,8 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 1.9
4
+ - 2.0
5
+ - 2.1
5
6
  - 2.2
6
7
  before_install: gem install bundler -v '~> 1.10'
7
8
  script: bundle exec rake spec
@@ -0,0 +1,2 @@
1
+ 1.0
2
+ ===
data/Gemfile CHANGED
@@ -3,6 +3,11 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in backticks.gemspec
4
4
  gemspec
5
5
 
6
+ group :development do
7
+ gem 'pry'
8
+ gem 'pry-byebug'
9
+ end
10
+
6
11
  group :test do
7
12
  gem 'coveralls', require: false
8
13
  end
data/README.md CHANGED
@@ -1,15 +1,14 @@
1
- # Introduction
1
+ ![Build Status](https://travis-ci.org/xeger/backticks.svg) [![Coverage Status](https://coveralls.io/repos/xeger/backticks/badge.svg?branch=master&service=github)](https://coveralls.io/github/xeger/backticks?branch=master) [![Docs](https://img.shields.io/badge/docs-rubydoc-blue.svg)](http://www.rubydoc.info/gems/backticks)
2
2
 
3
3
  Backticks is a powerful, intuitive OOP wrapper for invoking command-line processes and
4
4
  interacting with them.
5
5
 
6
- ![Build Status](https://travis-ci.org/xeger/backticks.svg) [![Coverage Status](https://coveralls.io/repos/xeger/backticks/badge.svg?branch=master&service=github)](https://coveralls.io/github/xeger/backticks?branch=master)
7
-
8
6
  "Powerful" comes from features that make Backticks especially well suited for time-sensitive
9
7
  or record/playback applications:
10
8
  - Uses [pseudoterminals](https://en.wikipedia.org/wiki/Pseudoterminal) for realtime stdout/stdin
11
9
  - Captures input as well as output
12
10
  - Separates stdout from stderr
11
+ - Allows realtime monitoring and transformation of input/output
13
12
 
14
13
  "Intuitive" comes from a DSL that lets you provide command-line arguments as if they were
15
14
  Ruby method arguments:
@@ -105,6 +104,8 @@ require 'io/console'
105
104
  STDOUT.raw! ; Backticks::Runner.new(interactive:true).run('vi').join
106
105
  ```
107
106
 
107
+ ### Intercepting and Modifying I/O
108
+
108
109
  You can use the `Command#tap` method to intercept I/O streams in real time,
109
110
  with or without interactivity. To start the tap, pass a block to the method.
110
111
  Your block should accept two parameters: a Symbol stream name (:stdin,
@@ -112,14 +113,15 @@ Your block should accept two parameters: a Symbol stream name (:stdin,
112
113
  input or output.
113
114
 
114
115
  The result of your block is used to decide what to do with the input/output:
115
- nil means "discard" and a modified string is captured in place of the original.
116
+ nil means "discard," any String means "use this in place of the original input
117
+ or output.""
116
118
 
117
- Try loading the README in an editor with all of the vowels scrambled. Scroll
119
+ Try loading `README.md` in an editor with all of the vowels scrambled. Scroll
118
120
  around and notice how words change when they leave and enter the screen!
119
121
 
120
122
  ```ruby
123
+ vowels = 'aeiou'
121
124
  STDOUT.raw! ; cmd = Backticks::Runner.new(interactive:true).run('vi', 'README.md') ; cmd.tap do |io, bytes|
122
- vowels = 'aeiou'
123
125
  bytes.gsub!(/[#{vowels}]/) { vowels[rand(vowels.size)] } if io == :stdout
124
126
  bytes
125
127
  end ; cmd.join ; nil
@@ -6,13 +6,11 @@ module Backticks
6
6
  # Interactive commands print their output to Ruby's STDOUT and STDERR
7
7
  # in realtime, and also pass input from Ruby's STDIN to the command's stdin.
8
8
  class Command
9
- # Time value that is used internally when a user is willing to wait
10
- # "forever" for the command.
11
- #
12
- # Using a definite time-value helps simplify the looping logic internally,
13
- # but it does mean that this class will stop working in February of 2106.
14
- # You have been warned!
15
- FOREVER = Time.at(2**32-1).freeze
9
+ # Duration that we use when a caller is willing to wait "forever" for
10
+ # a command to finish. This means that `#join` is buggy when used with
11
+ # commands that take longer than a year to complete. You have been
12
+ # warned!
13
+ FOREVER = 86_400 * 365
16
14
 
17
15
  # Number of bytes to read from the command in one "chunk".
18
16
  CHUNK = 1_024
@@ -32,52 +30,71 @@ module Backticks
32
30
  # @return [String] all output to stderr that has been captured so far
33
31
  attr_reader :captured_error
34
32
 
35
- # Watch a running command.
36
- def initialize(pid, stdin, stdout, stderr)
33
+ # Watch a running command by taking ownership of the IO objects that
34
+ # are passed in.
35
+ #
36
+ # @param [Integer] pid
37
+ # @param [IO] stdin
38
+ # @param [IO] stdout
39
+ # @param [IO] stderr
40
+ def initialize(pid, stdin, stdout, stderr, interactive:false)
37
41
  @pid = pid
38
42
  @stdin = stdin
39
43
  @stdout = stdout
40
44
  @stderr = stderr
45
+ @interactive = !!interactive
41
46
 
42
47
  @captured_input = String.new.force_encoding(Encoding::BINARY)
43
48
  @captured_output = String.new.force_encoding(Encoding::BINARY)
44
49
  @captured_error = String.new.force_encoding(Encoding::BINARY)
45
50
  end
46
51
 
47
- # @return [String]
52
+ # @return [String] a basic string representation of this command
48
53
  def to_s
49
54
  "#<Backticks::Command(@pid=#{pid},@status=#{@status || 'nil'})>"
50
55
  end
51
56
 
57
+ # @return [Boolean] true if this command is tied to STDIN/STDOUT
52
58
  def interactive?
53
- !@stdin.nil?
59
+ @interactive
60
+ end
61
+
62
+ # Block until the command completes; return true if its status
63
+ # was zero, false if nonzero.
64
+ #
65
+ # @return [Boolean]
66
+ def success?
67
+ join
68
+ status.success?
54
69
  end
55
70
 
56
- # Provide a callback to monitor input and output in real time.
71
+ # Provide a callback to monitor input and output in real time. This method
72
+ # saves a reference to block for later use; whenever the command generates
73
+ # output or receives input, the block is called back with the name of the
74
+ # stream on which I/O occurred and the actual data that was read or written.
57
75
  # @yield
58
- # @yieldparam
76
+ # @yieldparam [Symbol] stream one of :stdin, :stdout or :stderr
77
+ # @yieldparam [String] data fresh input from the designated stream
59
78
  def tap(&block)
60
79
  raise StandardError.new("Tap is already set (#{@tap}); cannot set twice") if @tap && @tap != block
61
80
  @tap = block
62
81
  end
63
82
 
64
83
  # Block until the command exits, or until limit seconds have passed. If
65
- # interactive is true, pass user input to the command and print its output
66
- # to Ruby's output streams. If the time limit expires, return `nil`;
67
- # otherwise, return self.
84
+ # interactive is true, proxy STDIN to the command and print its output
85
+ # to STDOUT. If the time limit expires, return `nil`; otherwise, return
86
+ # self.
87
+ #
88
+ # If the command has already exited when this method is called, return
89
+ # self immediately.
68
90
  #
69
91
  # @param [Float,Integer] limit number of seconds to wait before returning
70
- def join(limit=nil)
92
+ def join(limit=FOREVER)
71
93
  return self if @status
72
94
 
73
- if limit
74
- tf = Time.now + limit
75
- else
76
- tf = FOREVER
77
- end
78
-
95
+ tf = Time.now + limit
79
96
  until (t = Time.now) >= tf
80
- capture(tf - t)
97
+ capture(tf-t)
81
98
  res = Process.waitpid(@pid, Process::WNOHANG)
82
99
  if res
83
100
  @status = $?
@@ -103,13 +120,7 @@ module Backticks
103
120
  streams = [@stdout, @stderr]
104
121
  streams << STDIN if interactive?
105
122
 
106
- if limit
107
- tf = Time.now + limit
108
- else
109
- tf = FOREVER
110
- end
111
-
112
- ready, _, _ = IO.select(streams, [], [], 0)
123
+ ready, _, _ = IO.select(streams, [], [], limit)
113
124
 
114
125
  # proxy STDIN to child's stdin
115
126
  if ready && ready.include?(STDIN)
@@ -24,19 +24,23 @@ module Backticks
24
24
  # List of I/O streams that should be captured using a pipe instead of
25
25
  # a pseudoterminal.
26
26
  #
27
- # This may be a Boolean, or it may be an Array of stream names from the
28
- # set [:stdin, :stdout, :stderr].
27
+ # When read, this attribute is always an Array of stream names from the
28
+ # set `[:stdin, :stdout, :stderr]`.
29
29
  #
30
- # Note: if you set `interactive` to true, then stdin and stdout will be
30
+ # **Note**: if you set `interactive` to true, then stdin and stdout are
31
31
  # unbuffered regardless of how you have set `buffered`!
32
32
  #
33
33
  # @return [Array] list of symbolic stream names
34
34
  attr_reader :buffered
35
35
 
36
+ # @return [String,nil] PWD for new child processes, default is Dir.pwd
37
+ attr_accessor :chdir
38
+
36
39
  # @return [#parameters] the CLI-translation object used by this runner
37
40
  attr_reader :cli
38
41
 
39
42
  # Create an instance of Runner.
43
+ #
40
44
  # @option [#include?,Boolean] buffered list of names; true/false for all/none
41
45
  # @option [#parameters] cli command-line parameter translator
42
46
  # @option [Boolean] interactive true to tie parent stdout/stdin to child
@@ -55,6 +59,13 @@ module Backticks
55
59
  self.interactive = options[:interactive]
56
60
  end
57
61
 
62
+ # Control which streams are buffered (i.e. use a pipe) and which are
63
+ # unbuffered (i.e. use a pseudo-TTY).
64
+ #
65
+ # If you pass a Boolean argument, it is converted to an Array; therefore,
66
+ # the reader for this attribute always returns a list even if you wrote
67
+ # a boolean value.
68
+ #
58
69
  # @param [Array,Boolean] buffered list of symbolic stream names; true/false for all/none
59
70
  def buffered=(b)
60
71
  @buffered = case b
@@ -109,7 +120,8 @@ module Backticks
109
120
  PTY.open
110
121
  end
111
122
 
112
- pid = spawn(*argv, in: stdin_r, out: stdout_w, err: stderr_w)
123
+ dir = @chdir || Dir.pwd
124
+ pid = spawn(*argv, in: stdin_r, out: stdout_w, err: stderr_w, chdir: dir)
113
125
  stdin_r.close
114
126
  stdout_w.close
115
127
  stderr_w.close
@@ -118,7 +130,7 @@ module Backticks
118
130
  stdin = nil
119
131
  end
120
132
 
121
- Command.new(pid, stdin, stdout, stderr)
133
+ Command.new(pid, stdin, stdout, stderr, interactive:interactive)
122
134
  end
123
135
  end
124
136
  end
@@ -1,3 +1,3 @@
1
1
  module Backticks
2
- VERSION = "1.0.0rc1"
2
+ VERSION = "1.0.0rc2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: backticks
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0rc1
4
+ version: 1.0.0rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Spataro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-07-29 00:00:00.000000000 Z
11
+ date: 2016-09-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -62,6 +62,7 @@ files:
62
62
  - ".gitignore"
63
63
  - ".rspec"
64
64
  - ".travis.yml"
65
+ - CHANGELOG.md
65
66
  - CODE_OF_CONDUCT.md
66
67
  - Gemfile
67
68
  - README.md