command-runner 0.9.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -4
- data/lib/command/runner/backends/backticks.rb +1 -1
- data/lib/command/runner/backends/fake.rb +12 -1
- data/lib/command/runner/backends/posix_spawn.rb +9 -3
- data/lib/command/runner/backends/spawn.rb +6 -2
- data/lib/command/runner/backends/ssh.rb +11 -3
- data/lib/command/runner/backends/unsafe_fake.rb +19 -0
- data/lib/command/runner/backends.rb +1 -0
- data/lib/command/runner/version.rb +1 -1
- data/lib/command/runner.rb +33 -19
- data/spec/backticks_spec.rb +5 -5
- data/spec/interpolation_spec.rb +19 -0
- data/spec/messenger_spec.rb +18 -15
- data/spec/spawn_spec.rb +13 -7
- data/spec/spec_helper.rb +4 -0
- metadata +20 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b5636d13fb538ac969b67e6eb4fdc7ad5ddaa27
|
4
|
+
data.tar.gz: fa207be281ffb8b56279bbe7d50e76c1f52ef225
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 037f29024cedc32f6b469288a2c7229f7d74d9035df02894b9b8685834f867eab990fc3c9f69a9ba305ae38eb33a7a141b284dbb46396603fde613083aaddaf3
|
7
|
+
data.tar.gz: 5804940205201d61ab9ff50164e75dc9e5b18c415d46a38747345b9a990e52c3d731f3e0754eb1f60253df69373cde1a514f9644f09605c6d34d7d61f6fbf3fc
|
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# Command Runner
|
2
|
-
[![Build Status](https://travis-ci.org/
|
2
|
+
[![Build Status](https://travis-ci.org/medcat/command-runner.png?branch=master)](https://travis-ci.org/medcat/command-runner) [![Code Climate](https://codeclimate.com/github/medcat/command-runner.png)](https://codeclimate.com/github/medcat/command-runner)
|
3
3
|
|
4
4
|
Runs commands.
|
5
5
|
|
@@ -46,7 +46,7 @@ It can also use different methods to run commands...
|
|
46
46
|
|
47
47
|
```Ruby
|
48
48
|
line = Command::Runner.new("echo", "something")
|
49
|
-
line.
|
49
|
+
line.backend = Command::Runner::Backends::Spawn.new
|
50
50
|
line.pass
|
51
51
|
```
|
52
52
|
|
@@ -80,7 +80,6 @@ It works on
|
|
80
80
|
- 2.1.0
|
81
81
|
- 2.0.0
|
82
82
|
- 1.9.3
|
83
|
-
- 1.8.7
|
84
83
|
- JRuby (2.0 Mode)
|
85
84
|
- JRuby (1.9 Mode)
|
86
85
|
|
@@ -88,5 +87,5 @@ It works on
|
|
88
87
|
unless the travis build fails.
|
89
88
|
|
90
89
|
|
91
|
-
[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/
|
90
|
+
[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/medcat/command-runner/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
|
92
91
|
|
@@ -11,8 +11,10 @@ module Command
|
|
11
11
|
# Returns whether or not this backend is avialable on this
|
12
12
|
# platform.
|
13
13
|
#
|
14
|
+
# @param force_unsafe [Boolean] if the backend needs to be
|
15
|
+
# able to handle unsafe execution, then this will be true.
|
14
16
|
# @abstract
|
15
|
-
def self.available?
|
17
|
+
def self.available?(force_unsafe = false)
|
16
18
|
true
|
17
19
|
end
|
18
20
|
|
@@ -27,6 +29,15 @@ module Command
|
|
27
29
|
false
|
28
30
|
end
|
29
31
|
|
32
|
+
# Whether or not it can handle unsafe execution. This is in
|
33
|
+
# case the developer wants to force unsafe execution on a
|
34
|
+
# safe backend.
|
35
|
+
#
|
36
|
+
# @return [Boolean]
|
37
|
+
def self.unsafe_execution?
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
30
41
|
# Initialize the fake backend.
|
31
42
|
def initialize
|
32
43
|
@ran = []
|
@@ -13,10 +13,10 @@ module Command
|
|
13
13
|
#
|
14
14
|
# @see Fake.available?
|
15
15
|
# @return [Boolean]
|
16
|
-
def self.available?
|
16
|
+
def self.available?(_ = false)
|
17
17
|
@_available ||= begin
|
18
18
|
require 'posix/spawn'
|
19
|
-
|
19
|
+
super
|
20
20
|
rescue LoadError => e
|
21
21
|
false
|
22
22
|
end
|
@@ -27,7 +27,13 @@ module Command
|
|
27
27
|
# @see Spawn#spawn
|
28
28
|
# @return [Numeric]
|
29
29
|
def spawn(env, command, arguments, options)
|
30
|
-
|
30
|
+
if options.delete(:unsafe)
|
31
|
+
POSIX::Spawn.spawn(env,
|
32
|
+
"#{command} #{arguments.join(' ')}", options)
|
33
|
+
else
|
34
|
+
POSIX::Spawn.spawn(env, command,
|
35
|
+
*[arguments, options].flatten)
|
36
|
+
end
|
31
37
|
end
|
32
38
|
|
33
39
|
end
|
@@ -11,7 +11,7 @@ module Command
|
|
11
11
|
# that platform.
|
12
12
|
#
|
13
13
|
# @return [Boolean]
|
14
|
-
def self.available?
|
14
|
+
def self.available?(_ = false)
|
15
15
|
Process.respond_to?(:spawn) && !(RUBY_PLATFORM == "java" && RUBY_VERSION =~ /\A1\.9/)
|
16
16
|
end
|
17
17
|
|
@@ -81,7 +81,11 @@ module Command
|
|
81
81
|
# @see Process.spawn
|
82
82
|
# @return [Numeric] the process id
|
83
83
|
def spawn(env, command, arguments, options)
|
84
|
-
|
84
|
+
if options.delete(:unsafe)
|
85
|
+
Process.spawn(env, "#{command} #{arguments.join(' ')}", options)
|
86
|
+
else
|
87
|
+
Process.spawn(env, command, *arguments, options)
|
88
|
+
end
|
85
89
|
end
|
86
90
|
|
87
91
|
# Waits for the given process, and returns the process id and the
|
@@ -10,10 +10,14 @@ module Command
|
|
10
10
|
class SSH < Fake
|
11
11
|
|
12
12
|
# (see Fake.available?)
|
13
|
-
def self.available?
|
13
|
+
def self.available?(force_unsafe = false)
|
14
14
|
begin
|
15
15
|
require 'net/ssh'
|
16
|
-
|
16
|
+
if force_unsafe
|
17
|
+
false
|
18
|
+
else
|
19
|
+
true
|
20
|
+
end
|
17
21
|
rescue LoadError
|
18
22
|
false
|
19
23
|
end
|
@@ -23,6 +27,10 @@ module Command
|
|
23
27
|
true
|
24
28
|
end
|
25
29
|
|
30
|
+
def self.unsafe_execution?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
26
34
|
# Initializes the backend.
|
27
35
|
#
|
28
36
|
# @param host [String] the host to connect to.
|
@@ -41,7 +49,7 @@ module Command
|
|
41
49
|
|
42
50
|
|
43
51
|
ch.exec "#{command} " \
|
44
|
-
"#{arguments.join(" ")
|
52
|
+
"#{arguments.join(" ")}" do |sch, success|
|
45
53
|
raise Errno::ENOENT unless success
|
46
54
|
|
47
55
|
env.each do |k, v|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Command
|
2
|
+
class Runner
|
3
|
+
module Backends
|
4
|
+
class UnsafeFake < Fake
|
5
|
+
|
6
|
+
# A backend is considered unsafe when the arguments are
|
7
|
+
# exposed directly to the shell. This is a vulnerability, so
|
8
|
+
# we mark the class as unsafe and when we're about to pass
|
9
|
+
# the arguments to the backend, escape the safe
|
10
|
+
# interpolations.
|
11
|
+
#
|
12
|
+
# @return [Boolean]
|
13
|
+
def self.unsafe?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -11,6 +11,7 @@ module Command
|
|
11
11
|
autoload :Spawn, "command/runner/backends/spawn"
|
12
12
|
autoload :Backticks, "command/runner/backends/backticks"
|
13
13
|
autoload :PosixSpawn, "command/runner/backends/posix_spawn"
|
14
|
+
autoload :UnsafeFake, "command/runner/backends/unsafe_fake"
|
14
15
|
|
15
16
|
end
|
16
17
|
|
data/lib/command/runner.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'shellwords'
|
2
2
|
require 'future'
|
3
|
+
require 'hashie'
|
3
4
|
|
4
5
|
require 'command/runner/version'
|
5
6
|
require 'command/runner/message'
|
@@ -24,11 +25,11 @@ module Command
|
|
24
25
|
#
|
25
26
|
# @return [#call] a backend to use.
|
26
27
|
def best_backend(force_unsafe = false)
|
27
|
-
if Backends::PosixSpawn.available?
|
28
|
+
if Backends::PosixSpawn.available?(force_unsafe)
|
28
29
|
Backends::PosixSpawn.new
|
29
|
-
elsif Backends::Spawn.available?
|
30
|
+
elsif Backends::Spawn.available?(force_unsafe)
|
30
31
|
Backends::Spawn.new
|
31
|
-
elsif Backends::Backticks.available?
|
32
|
+
elsif Backends::Backticks.available?(force_unsafe)
|
32
33
|
Backends::Backticks.new
|
33
34
|
else
|
34
35
|
Backends::Fake.new
|
@@ -98,7 +99,9 @@ module Command
|
|
98
99
|
# @return [Message, Object] message if no block was given, the
|
99
100
|
# return value of the block otherwise.
|
100
101
|
def pass!(interops = {}, options = {}, &block)
|
101
|
-
|
102
|
+
options[:unsafe] = @unsafe
|
103
|
+
env = options.delete(:env) || {}
|
104
|
+
backend.call(*contents(interops), env, options, &block)
|
102
105
|
|
103
106
|
rescue Errno::ENOENT
|
104
107
|
raise NoCommandError, @command
|
@@ -135,7 +138,7 @@ module Command
|
|
135
138
|
#
|
136
139
|
# @return [void]
|
137
140
|
def force_unsafe!
|
138
|
-
@
|
141
|
+
@unsafe = true
|
139
142
|
end
|
140
143
|
|
141
144
|
# The command line being run by the runner. Interpolates the
|
@@ -159,24 +162,35 @@ module Command
|
|
159
162
|
# @param interops [Hash] the interpolations to make.
|
160
163
|
# @return [Array<String>] the interpolated string.
|
161
164
|
def interpolate(string, interops = {})
|
162
|
-
interops =
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
if
|
168
|
-
|
169
|
-
|
165
|
+
interops = Hashie::Mash.new(interops)
|
166
|
+
args = string.shellsplit
|
167
|
+
|
168
|
+
args.map do |arg|
|
169
|
+
arg.gsub(/(\{{1,2})([0-9a-zA-Z_\-.]+)(\}{1,2})/) do |m|
|
170
|
+
if $1.length != $3.length
|
171
|
+
next m
|
172
|
+
end
|
173
|
+
|
174
|
+
ops = interops
|
175
|
+
parts = $2.split('.')
|
176
|
+
parts.each do |part|
|
177
|
+
if ops.key?(part)
|
178
|
+
ops = ops[part]
|
170
179
|
else
|
171
|
-
|
180
|
+
ops = nil
|
181
|
+
break
|
172
182
|
end
|
183
|
+
end
|
184
|
+
|
185
|
+
if ops == nil
|
186
|
+
m
|
187
|
+
elsif $1.length == 1
|
188
|
+
escape(ops)
|
173
189
|
else
|
174
|
-
|
190
|
+
ops
|
175
191
|
end
|
176
|
-
else
|
177
|
-
part
|
178
192
|
end
|
179
|
-
end
|
193
|
+
end
|
180
194
|
end
|
181
195
|
|
182
196
|
private
|
@@ -187,7 +201,7 @@ module Command
|
|
187
201
|
# @param string [String] the string to escape.
|
188
202
|
# @return [String] the escaped string.
|
189
203
|
def escape(string)
|
190
|
-
if backend.unsafe?
|
204
|
+
if backend.unsafe? || @unsafe
|
191
205
|
Shellwords.escape(string)
|
192
206
|
else
|
193
207
|
string
|
data/spec/backticks_spec.rb
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
describe Command::Runner::Backends::Backticks do
|
2
2
|
|
3
3
|
it "is available" do
|
4
|
-
Command::Runner::Backends::Backticks.
|
4
|
+
expect(Command::Runner::Backends::Backticks).to be_available
|
5
5
|
end
|
6
6
|
|
7
|
-
|
7
|
+
it("is unsafe") { expect(described_class).to be_unsafe }
|
8
8
|
|
9
9
|
it "returns a message" do
|
10
10
|
value = subject.call("echo", ["hello"])
|
11
|
-
value.
|
12
|
-
value.
|
11
|
+
expect(value).to be_instance_of Command::Runner::Message
|
12
|
+
expect(value).to be_executed
|
13
13
|
end
|
14
14
|
|
15
15
|
it "gives the correct time" do
|
16
|
-
subject.call("sleep", ["0.5"]).time.
|
16
|
+
expect(subject.call("sleep", ["0.5"]).time).to be_within(0.1).of(0.5)
|
17
17
|
end
|
18
18
|
|
19
19
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
describe Command::Runner do
|
2
|
+
|
3
|
+
before :each do
|
4
|
+
Command::Runner.backend = Command::Runner::Backends::Fake.new
|
5
|
+
end
|
6
|
+
|
7
|
+
subject { Command::Runner.new("gcc", "{env.flags} -o {output} {input} -l{lib}") }
|
8
|
+
let(:data) do
|
9
|
+
{ env: { flags: "-g3" },
|
10
|
+
output: "test",
|
11
|
+
input: "test.c",
|
12
|
+
lib: "m" }
|
13
|
+
end
|
14
|
+
|
15
|
+
it "interpolates correctly" do
|
16
|
+
value = subject.contents(data).last
|
17
|
+
expect(value).to eq ["-g3", "-o", "test", "test.c", "-lm"]
|
18
|
+
end
|
19
|
+
end
|
data/spec/messenger_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
describe Command::Runner do
|
2
2
|
|
3
3
|
before :each do
|
4
|
-
Command::Runner.backend = Command::Runner::Backends::
|
4
|
+
Command::Runner.backend = Command::Runner::Backends::UnsafeFake.new
|
5
5
|
end
|
6
6
|
|
7
7
|
subject do
|
@@ -21,13 +21,13 @@ describe Command::Runner do
|
|
21
21
|
it "escapes bad values" do
|
22
22
|
expect(
|
23
23
|
subject.contents(:interpolation => "`bad value`")
|
24
|
-
).to eq ["echo", ["some", "
|
24
|
+
).to eq ["echo", ["some", "\\`bad\\ value\\`"]]
|
25
25
|
end
|
26
26
|
|
27
27
|
it "doesn't interpolate interpolation values" do
|
28
28
|
expect(
|
29
29
|
subject.contents(:interpolation => "{other}", :other => "hi")
|
30
|
-
).to eq ["echo", ["some", "{other}"]]
|
30
|
+
).to eq ["echo", ["some", "\\{other\\}"]]
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -44,7 +44,7 @@ describe Command::Runner do
|
|
44
44
|
it "doesn't escape bad values" do
|
45
45
|
expect(
|
46
46
|
subject.contents(:interpolation => "`bad value`")
|
47
|
-
).to eq ["echo", ["some", "`bad
|
47
|
+
).to eq ["echo", ["some", "`bad value`"]]
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
@@ -61,16 +61,17 @@ describe Command::Runner do
|
|
61
61
|
|
62
62
|
context "when selecting backends" do
|
63
63
|
it "selects the best backend" do
|
64
|
-
Command::Runner::Backends::PosixSpawn
|
65
|
-
|
66
|
-
Command::Runner
|
67
|
-
|
68
|
-
Command::Runner
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
Command::Runner.best_backend
|
64
|
+
allow(Command::Runner::Backends::PosixSpawn).
|
65
|
+
to receive(:available?).and_return(false)
|
66
|
+
allow(Command::Runner::Backends::Spawn).
|
67
|
+
to receive(:available?).and_return(true)
|
68
|
+
expect(Command::Runner.best_backend).
|
69
|
+
to be_instance_of Command::Runner::Backends::Spawn
|
70
|
+
|
71
|
+
allow(Command::Runner::Backends::PosixSpawn).
|
72
|
+
to receive(:available?).and_return(true)
|
73
|
+
expect(Command::Runner.best_backend).
|
74
|
+
to be_instance_of Command::Runner::Backends::PosixSpawn
|
74
75
|
end
|
75
76
|
end
|
76
77
|
|
@@ -82,7 +83,9 @@ describe Command::Runner do
|
|
82
83
|
subject.backend = Command::Runner::Backends::Backticks.new
|
83
84
|
end
|
84
85
|
|
85
|
-
|
86
|
+
it "should result in no command" do
|
87
|
+
expect(subject.pass).to be_no_command
|
88
|
+
end
|
86
89
|
|
87
90
|
it "calls the block given" do
|
88
91
|
subject.pass do |message|
|
data/spec/spawn_spec.rb
CHANGED
@@ -2,16 +2,16 @@ describe Command::Runner::Backends::Spawn do
|
|
2
2
|
|
3
3
|
next unless Process.respond_to?(:spawn) && !(RUBY_PLATFORM == "java" && RUBY_VERSION =~ /\A1\.9/)
|
4
4
|
|
5
|
-
|
5
|
+
it("is safe") { expect(described_class).to_not be_unsafe }
|
6
6
|
|
7
7
|
it "is available" do
|
8
|
-
Command::Runner::Backends::Spawn.
|
8
|
+
expect(Command::Runner::Backends::Spawn).to be_available
|
9
9
|
end
|
10
10
|
|
11
11
|
it "returns a message" do
|
12
12
|
value = subject.call("echo", ["hello"])
|
13
|
-
value.
|
14
|
-
value.
|
13
|
+
expect(value).to be_instance_of Command::Runner::Message
|
14
|
+
expect(value).to be_executed
|
15
15
|
end
|
16
16
|
|
17
17
|
it "doesn't block" do
|
@@ -19,8 +19,8 @@ describe Command::Runner::Backends::Spawn do
|
|
19
19
|
value = subject.call("sleep", ["0.5"])
|
20
20
|
end_time = Time.now
|
21
21
|
|
22
|
-
(end_time - start_time).
|
23
|
-
value.time.
|
22
|
+
expect(end_time - start_time).to be_within((1.0/100)).of(0)
|
23
|
+
expect(value.time).to be_within((3.0/100)).of(0.5)
|
24
24
|
end
|
25
25
|
|
26
26
|
it "doesn't expose arguments to the shell" do
|
@@ -29,8 +29,14 @@ describe Command::Runner::Backends::Spawn do
|
|
29
29
|
expect(value.stdout).to eq "`uname -a`\n"
|
30
30
|
end
|
31
31
|
|
32
|
+
it "can be unsafe" do
|
33
|
+
value = subject.call("echo", ["`uname -a`"], {}, unsafe: true)
|
34
|
+
|
35
|
+
expect(value.stdout).to_not eq "`uname -a`\n"
|
36
|
+
end
|
37
|
+
|
32
38
|
it "can not be available" do
|
33
|
-
Command::Runner::Backends::Spawn.
|
39
|
+
allow(Command::Runner::Backends::Spawn).to receive(:available?).and_return(false)
|
34
40
|
|
35
41
|
expect {
|
36
42
|
Command::Runner::Backends::Spawn.new
|
data/spec/spec_helper.rb
ADDED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: command-runner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Rodi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-09-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: promise
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: hashie
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.3'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: rspec
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -83,13 +97,16 @@ files:
|
|
83
97
|
- lib/command/runner/backends/posix_spawn.rb
|
84
98
|
- lib/command/runner/backends/spawn.rb
|
85
99
|
- lib/command/runner/backends/ssh.rb
|
100
|
+
- lib/command/runner/backends/unsafe_fake.rb
|
86
101
|
- lib/command/runner/exceptions.rb
|
87
102
|
- lib/command/runner/message.rb
|
88
103
|
- lib/command/runner/version.rb
|
89
104
|
- spec/backticks_spec.rb
|
105
|
+
- spec/interpolation_spec.rb
|
90
106
|
- spec/messenger_spec.rb
|
91
107
|
- spec/spawn_spec.rb
|
92
|
-
|
108
|
+
- spec/spec_helper.rb
|
109
|
+
homepage: http://github.com/medcat/command-runner
|
93
110
|
licenses:
|
94
111
|
- MIT
|
95
112
|
metadata: {}
|