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 +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
|