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 +4 -4
- data/README.md +66 -15
- data/lib/backticks.rb +19 -6
- data/lib/backticks/cli.rb +20 -12
- data/lib/backticks/command.rb +15 -9
- data/lib/backticks/ext.rb +3 -3
- data/lib/backticks/runner.rb +19 -10
- data/lib/backticks/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8093e055335a1040855c560d8c94d9fb7a5cecd1
|
4
|
+
data.tar.gz: 2145905dfafbc820072d832f789b65d7c99b5638
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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
|
-
#
|
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.
|
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
|
44
|
-
#
|
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
|
54
|
-
stderr
|
55
|
-
|
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
|
-
|
58
|
-
|
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
|
-
|
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.
|
data/lib/backticks.rb
CHANGED
@@ -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(*
|
12
|
-
Backticks::Runner.new.command(*
|
13
|
+
def self.new(*cmd)
|
14
|
+
Backticks::Runner.new.command(*cmd)
|
13
15
|
end
|
14
16
|
|
15
|
-
# Run a command
|
17
|
+
# Run a command; return its stdout.
|
16
18
|
#
|
19
|
+
# @param [String] cmd
|
17
20
|
# @return [String] the command's output
|
18
|
-
def self.
|
19
|
-
command = self.new(*
|
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
|
data/lib/backticks/cli.rb
CHANGED
@@ -73,27 +73,35 @@ module Backticks
|
|
73
73
|
def self.options(**opts)
|
74
74
|
flags = []
|
75
75
|
|
76
|
-
# Transform opts into
|
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
|
-
|
81
|
-
|
82
|
-
|
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 << "
|
101
|
+
flags << "#{pre}#{kw}"
|
94
102
|
elsif arg
|
95
103
|
# truthey: option that has a value
|
96
|
-
flags << "
|
104
|
+
flags << "#{pre}#{kw}#{eql}#{arg}"
|
97
105
|
else
|
98
106
|
# falsey: omit boolean flag
|
99
107
|
end
|
data/lib/backticks/command.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
119
|
+
STDERR.write(data) if interactive?
|
120
|
+
fresh_error = data
|
117
121
|
end
|
118
122
|
end
|
119
|
-
|
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
|
data/lib/backticks/ext.rb
CHANGED
data/lib/backticks/runner.rb
CHANGED
@@ -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
|
-
@
|
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
|
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 =
|
77
|
-
stderr, stderr_w =
|
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
|
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
|
105
|
+
Command.new(thr.pid, stdin, stdout, stderr)
|
97
106
|
end
|
98
107
|
end
|
99
|
-
end
|
108
|
+
end
|
data/lib/backticks/version.rb
CHANGED
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.
|
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:
|
11
|
+
date: 2016-01-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|