backticks 0.1.1 → 0.3.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.
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