backticks 0.1.1 → 0.3.0

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: b5b80eef1129c850eb5fc8ffb3dc816dcd7853d2
4
- data.tar.gz: e49b51b178cf49bdcb0c73dc0e77f44a0a1951cc
3
+ metadata.gz: 8093e055335a1040855c560d8c94d9fb7a5cecd1
4
+ data.tar.gz: 2145905dfafbc820072d832f789b65d7c99b5638
5
5
  SHA512:
6
- metadata.gz: 3e267a2020567274d2335bc848421de16736cd9aa370d103b5c561474f9a85a8851909cda40f114b298d59ad583336f66e3caf017e29c63f7711f53b56a82a14
7
- data.tar.gz: 728b478ae95cc2676fbf64c22809f4815e11a3b0b50031c7fb002eda006893309f56cb8201e4079e2046c806580f43bd43defc95723ce7834f81620a62e8e289
6
+ metadata.gz: fc987a7287c7662a5d9207eeebf429b74ead2b15c7ebaccfab73763ba73650b4197ef81cfc3654472bbf1bf818d56168b2e6d22994432502dd8defea1e75f1e8
7
+ data.tar.gz: d1aa0d1ae781ebed53753562d6c1799e091d7a91fad42f442a8c589efab60501af35618e9052a9bf86513b65f60b4691b5f3fc4c2a19f8991b3c4862b9d1f79c
data/README.md CHANGED
@@ -1,9 +1,28 @@
1
1
  # Backticks
2
2
 
3
3
  Backticks is an intuitive OOP wrapper for invoking command-line processes and
4
- interacting with them. It uses PTYs
4
+ interacting with them. It improves on Ruby's built-in invocation methods in a
5
+ few ways:
6
+ - Uses [pseudoterminals](https://en.wikipedia.org/wiki/Pseudoterminal) for unbuffered I/O
7
+ - Captures input as well as output
8
+ - Intuitive API that accepts CLI parameters as Ruby positional and keyword args
5
9
 
6
- By default, processes that you invoke
10
+ If you want to write a record/playback application for the terminal, or write
11
+ functional tests that verify your program's output in real time, Backticks is
12
+ exactly what you've been looking for!
13
+
14
+ For an example of the intuitive API, let's consider how we list a bunch of
15
+ files or search for some text with Backticks:
16
+
17
+ ```ruby
18
+ # invokes "ls -l -R"
19
+ Backticks.run 'ls', l:true, R:true
20
+
21
+ # invokes "grep -H --context=2 --regexp=needle haystack.txt"
22
+ Backticks.run 'grep', {H:true, context:2, regexp:'needle'}, 'haystack.txt'
23
+ ```
24
+
25
+ Notice how running commands feels like a plain old Ruby method call.
7
26
 
8
27
  ## Installation
9
28
 
@@ -26,22 +45,23 @@ Or install it yourself as:
26
45
  ```ruby
27
46
  require 'backticks'
28
47
 
29
- # The lazy way; provides no CLI sugar, but benefits from unbuffered output.
30
- # Many Unix utilities produce colorized output when stdout is a TTY; be
31
- # prepared to handle escape codes in the output.
48
+ # The lazy way; provides no CLI sugar, but benefits from unbuffered output,
49
+ # and allows you to override Ruby's built-in backticks method.
32
50
  shell = Object.new ; shell.extend(Backticks::Ext)
33
51
  shell.instance_eval do
34
52
  puts `ls -l`
35
53
  raise 'Oh no!' unless $?.success?
36
54
  end
55
+ # The just-as-lazy but less-magical way.
56
+ Backticks.system('ls -l') || raise('Oh no!')
37
57
 
38
- # The easy way.
39
- output = Backticks.command('ls', R:true, '*.rb')
58
+ # The easy way. Uses default options; returns the command's output as a String.
59
+ output = Backticks.run('ls', R:true, '*.rb')
40
60
  puts "Exit status #{$?.to_i}. Output:"
41
61
  puts output
42
62
 
43
- # The hard way; allows customization such as interactive mode, which proxies
44
- # the child process's stdin, stdout and stderr to the parent process.
63
+ # The hard way. Allows customized behavior; returns a Command object that
64
+ # allows you to interact with the running command.
45
65
  command = Backticks::Runner.new(interactive:true).command('ls', R:true, '*.rb')
46
66
  command.join
47
67
  puts "Exit status: #{command.status.to_i}. Output:"
@@ -50,17 +70,48 @@ puts command.captured_output
50
70
 
51
71
  ### Buffering
52
72
 
53
- By default, Backticks allocates a pseudo-TTY for stdout and two Unix pipes for
54
- stderr/stdin; this captures stdout in real-time, but stderr and
55
- stdin are subject to unavoidable Unix pipe buffering.
73
+ By default, Backticks allocates a pseudo-TTY for stdin/stdout and a Unix pipe
74
+ for stderr; this captures the program's output and the user's input in realtime,
75
+ but stderr is buffered according to the whim of the kernel's pipe subsystem.
76
+
77
+ To use pipes for all I/O streams, enable buffering on the Runner:
78
+
79
+ ```ruby
80
+ # at initialize-time
81
+ r = Backticks::Runner.new(buffered:true)
82
+
83
+ # or later on
84
+ r.buffered = false
85
+ ```
86
+
87
+ ### Interactivity
88
+
89
+ If you set `interactive:true` on the Runner, the console of the calling (Ruby)
90
+ process is "tied" to the child's I/O streams, allowing the user to interact
91
+ with the child process even as its input and output are captured for later use.
92
+
93
+ If the child process will use raw input, you need to set the parent's console
94
+ accordingly:
95
+
96
+ ```ruby
97
+ require 'io/console'
98
+ # In IRB, call raw! on same line as command; IRB prompt uses raw I/O
99
+ STDOUT.raw! ; Backticks::Runner.new(interactive:true).command('vi').join
100
+ ```
101
+
102
+ ### Literally Overriding Ruby's Backticks
56
103
 
57
- To use pipes for all io streams, enable buffering when you construct your
58
- Runner:
104
+ It's a terrible idea, but you can use this gem to change the behavior of
105
+ backticks system-wide by mixing it into Kernel.
59
106
 
60
107
  ```ruby
61
- Backticks::Runner.new(buffered:true)
108
+ require 'backticks'
109
+ include Backticks::Ext
110
+ `echo Ruby lets me shoot myself in the foot`
62
111
  ```
63
112
 
113
+ If you do this, I will hunt you down and scoff at you. You have been warned!
114
+
64
115
  ## Development
65
116
 
66
117
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -5,19 +5,32 @@ require_relative 'backticks/runner'
5
5
  require_relative 'backticks/ext'
6
6
 
7
7
  module Backticks
8
- # Run a command.
8
+ # Run a command; return a Command object that can be used to interact with
9
+ # the running process. Method parameters are passed through to the Runner.
9
10
  #
11
+ # @see Backticks::Runner#command
10
12
  # @return [Backticks::Command] a running command
11
- def self.new(*argv)
12
- Backticks::Runner.new.command(*argv)
13
+ def self.new(*cmd)
14
+ Backticks::Runner.new.command(*cmd)
13
15
  end
14
16
 
15
- # Run a command and return its stdout.
17
+ # Run a command; return its stdout.
16
18
  #
19
+ # @param [String] cmd
17
20
  # @return [String] the command's output
18
- def self.command(*argv)
19
- command = self.new(*argv)
21
+ def self.run(*cmd)
22
+ command = self.new(*cmd)
20
23
  command.join
21
24
  command.captured_output
22
25
  end
26
+
27
+ # Run a command; return its success or failure.
28
+ #
29
+ # @param [String] cmd
30
+ # @return [Boolean] true if the command succeeded; false otherwise
31
+ def self.system(*cmd)
32
+ command = self.new(*cmd)
33
+ command.join
34
+ $?.success?
35
+ end
23
36
  end
@@ -73,27 +73,35 @@ module Backticks
73
73
  def self.options(**opts)
74
74
  flags = []
75
75
 
76
- # Transform opts into golang flags-style command line parameters;
76
+ # Transform opts into getopt-style command line parameters;
77
77
  # append them to the command.
78
78
  opts.each do |kw, arg|
79
79
  if kw.length == 1
80
- if arg == true
81
- # true: boolean flag
82
- flags << "-#{kw}"
83
- elsif arg
84
- # truthey: option that has a value
85
- flags << "-#{kw}" << arg.to_s
86
- else
87
- # falsey: omit boolean flag
88
- end
80
+ # short-form option e.g. "-a" or "-x hello"
81
+ pre='-'
82
+ eql=' '
89
83
  else
84
+ # long-form option e.g. "--ask" or "--extra=hello"
90
85
  kw = kw.to_s.gsub('_','-')
86
+ pre='--'
87
+ eql='='
88
+ end
89
+
90
+ # options can be single- or multi-valued; normalize the processing
91
+ # of both by "upconverting" single options to an array or values.
92
+ if arg.kind_of?(Array)
93
+ values = arg
94
+ else
95
+ values = [arg]
96
+ end
97
+
98
+ values.each do |arg|
91
99
  if arg == true
92
100
  # true: boolean flag
93
- flags << "--#{kw}"
101
+ flags << "#{pre}#{kw}"
94
102
  elsif arg
95
103
  # truthey: option that has a value
96
- flags << "--#{kw}=#{arg}"
104
+ flags << "#{pre}#{kw}#{eql}#{arg}"
97
105
  else
98
106
  # falsey: omit boolean flag
99
107
  end
@@ -24,20 +24,21 @@ module Backticks
24
24
  attr_reader :captured_input, :captured_output, :captured_error, :status
25
25
 
26
26
  # Watch a running command.
27
- def initialize(pid, stdin, stdout, stderr, interactive:false)
27
+ def initialize(pid, stdin, stdout, stderr)
28
28
  @pid = pid
29
29
  @stdin = stdin
30
30
  @stdout = stdout
31
31
  @stderr = stderr
32
- @interactive = interactive
33
-
34
- stdin.close unless @interactive
35
32
 
36
33
  @captured_input = String.new.force_encoding(Encoding::BINARY)
37
34
  @captured_output = String.new.force_encoding(Encoding::BINARY)
38
35
  @captured_error = String.new.force_encoding(Encoding::BINARY)
39
36
  end
40
37
 
38
+ def interactive?
39
+ !@stdin.nil?
40
+ end
41
+
41
42
  # Block until the command exits, or until limit seconds have passed. If
42
43
  # interactive is true, pass user input to the command and print its output
43
44
  # to Ruby's output streams. If the time limit expires, return `nil`;
@@ -45,6 +46,8 @@ module Backticks
45
46
  #
46
47
  # @param [Float,Integer] limit number of seconds to wait before returning
47
48
  def join(limit=nil)
49
+ return self if @status # preserve idempotency
50
+
48
51
  if limit
49
52
  tf = Time.now + limit
50
53
  else
@@ -76,7 +79,7 @@ module Backticks
76
79
  # @return [String,nil] fresh bytes from stdout/stderr, or nil if no output
77
80
  private def capture(limit=nil)
78
81
  streams = [@stdout, @stderr]
79
- streams << STDIN if @interactive
82
+ streams << STDIN if interactive?
80
83
 
81
84
  if limit
82
85
  tf = Time.now + limit
@@ -103,7 +106,7 @@ module Backticks
103
106
  data = @stdout.readpartial(CHUNK) rescue nil
104
107
  if data
105
108
  @captured_output << data
106
- STDOUT.write(data) if @interactive
109
+ STDOUT.write(data) if interactive?
107
110
  fresh_output = data
108
111
  end
109
112
  end
@@ -113,14 +116,17 @@ module Backticks
113
116
  data = @stderr.readpartial(CHUNK) rescue nil
114
117
  if data
115
118
  @captured_error << data
116
- STDERR.write(data) if @interactive
119
+ STDERR.write(data) if interactive?
120
+ fresh_error = data
117
121
  end
118
122
  end
119
- fresh_output
123
+
124
+ # return freshly-captured text(if any)
125
+ fresh_output || fresh_error
120
126
  rescue Interrupt
121
127
  # Proxy Ctrl+C to the child
122
128
  (Process.kill('INT', @pid) rescue nil) if @interactive
123
129
  raise
124
130
  end
125
131
  end
126
- end
132
+ end
@@ -1,7 +1,7 @@
1
1
  module Backticks
2
2
  module Ext
3
- def `(str)
4
- Backticks.command(str)
3
+ def `(cmd)
4
+ Backticks.run(cmd)
5
5
  end
6
6
  end
7
- end
7
+ end
@@ -33,10 +33,10 @@ module Backticks
33
33
 
34
34
  # Create an instance of Runner.
35
35
  # @param [#parameters] cli object used to convert Ruby method parameters into command-line parameters
36
- def initialize(cli:Backticks::CLI::Getopt)
37
- @interactive = false
38
- @buffered = false
36
+ def initialize(buffered:false, cli:Backticks::CLI::Getopt, interactive:false)
37
+ @buffered = buffered
39
38
  @cli = cli
39
+ @interactive = interactive
40
40
  end
41
41
 
42
42
  # Run a command whose parameters are expressed using some Rubyish sugar.
@@ -56,9 +56,8 @@ module Backticks
56
56
  #
57
57
  # @example Run docker-compose with complex parameters
58
58
  # command('docker-compose', {file: 'joe.yml'}, 'up', {d:true}, 'mysvc')
59
- def command(*args)
59
+ def run(*args)
60
60
  argv = @cli.parameters(*args)
61
-
62
61
  if self.buffered
63
62
  run_buffered(argv)
64
63
  else
@@ -66,6 +65,8 @@ module Backticks
66
65
  end
67
66
  end
68
67
 
68
+ alias command run
69
+
69
70
  # Run a command. Use a pty to capture the unbuffered output.
70
71
  #
71
72
  # @param [Array] argv command to run; argv[0] is program name and the
@@ -73,14 +74,18 @@ module Backticks
73
74
  # @return [Command] the running command
74
75
  private def run_unbuffered(argv)
75
76
  stdout, stdout_w = PTY.open
76
- stdin_r, stdin = IO.pipe
77
- stderr, stderr_w = IO.pipe
77
+ stdin_r, stdin = PTY.open
78
+ stderr, stderr_w = PTY.open
78
79
  pid = spawn(*argv, in: stdin_r, out: stdout_w, err: stderr_w)
79
80
  stdin_r.close
80
81
  stdout_w.close
81
82
  stderr_w.close
83
+ unless @interactive
84
+ stdin.close
85
+ stdin = nil
86
+ end
82
87
 
83
- Command.new(pid, stdin, stdout, stderr, interactive:@interactive)
88
+ Command.new(pid, stdin, stdout, stderr)
84
89
  end
85
90
 
86
91
  # Run a command. Perform no translation or substitution. Use a pipe
@@ -92,8 +97,12 @@ module Backticks
92
97
  # @return [Command] the running command
93
98
  private def run_buffered(argv)
94
99
  stdin, stdout, stderr, thr = Open3.popen3(*argv)
100
+ unless @interactive
101
+ stdin.close
102
+ stdin = nil
103
+ end
95
104
 
96
- Command.new(thr.pid, stdin, stdout, stderr, interactive:@interactive)
105
+ Command.new(thr.pid, stdin, stdout, stderr)
97
106
  end
98
107
  end
99
- end
108
+ end
@@ -1,3 +1,3 @@
1
1
  module Backticks
2
- VERSION = "0.1.1"
2
+ VERSION = "0.3.0"
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: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Spataro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-12-24 00:00:00.000000000 Z
11
+ date: 2016-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler