command-runner 0.6.1 → 0.7.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 35d339b7a9ec8370365504412a82dfb2e18523b3
4
+ data.tar.gz: 2ed783e009554db92e0374534ebe10dadc6e05de
5
+ SHA512:
6
+ metadata.gz: d038c4a0686d3dd7f484bc2a6f3f3eef464c6c1b1b2bab19d2a607ad21cc940d068229ce0af4f21380da9735f0d649f90dfee0ae88f2f8d7a212ab8f8dfff08f
7
+ data.tar.gz: 7578cae4033ef1491a4d9c3534507d5bda162dcb81711325de219e447ab060610d5fcb0f9852d75cb540cad1578632034205a229efd5f7061393a8c963e0d732
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
- # Command Runner [![Build Status](https://travis-ci.org/redjazz96/command-runner.png?branch=master)](https://travis-ci.org/redjazz96/command-runner)
1
+ # Command Runner
2
+ [![Build Status](https://travis-ci.org/redjazz96/command-runner.png?branch=master)](https://travis-ci.org/redjazz96/command-runner) [![Code Climate](https://codeclimate.com/github/redjazz96/command-runner.png)](https://codeclimate.com/github/redjazz96/command-runner)
3
+
2
4
  Runs commands.
3
5
 
4
6
  ```Ruby
@@ -34,6 +36,7 @@ unless you don't want it to.
34
36
 
35
37
  ```Ruby
36
38
  line = Command::Runner.new("echo", "{{interpolation}}")
39
+ line.force_unsafe!
37
40
  message = line.pass(:interpolation => "`uname -a`")
38
41
  message.stdout # => "Linux Hyperion 3.8.0-25-generic #37-Ubuntu SMP Thu Jun 6 20:47:07 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux\n"
39
42
  message.line # => "echo `uname -a`"
@@ -74,10 +77,16 @@ and return the value.
74
77
  ## Compatibility
75
78
  It works on
76
79
 
80
+ - 2.1.0
77
81
  - 2.0.0
78
82
  - 1.9.3
79
83
  - 1.8.7
80
- - REE
81
- - JRuby (1.8 Mode)
84
+ - JRuby (2.0 Mode)
85
+ - JRuby (1.9 Mode)
86
+
82
87
 
83
88
  unless the travis build fails.
89
+
90
+
91
+ [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/redjazz96/command-runner/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
92
+
@@ -23,10 +23,10 @@ module Command
23
23
  # Returns the best backend for messenger to use.
24
24
  #
25
25
  # @return [#call] a backend to use.
26
- def best_backend
27
- if Backends::PosixSpawn.available?
26
+ def best_backend(force_unsafe = false)
27
+ if Backends::PosixSpawn.available? && !force_unsafe
28
28
  Backends::PosixSpawn.new
29
- elsif Backends::Spawn.available?
29
+ elsif Backends::Spawn.available? && !force_unsafe
30
30
  Backends::Spawn.new
31
31
  elsif Backends::Backticks.available?
32
32
  Backends::Backticks.new
@@ -98,7 +98,7 @@ module Command
98
98
  # @return [Message, Object] message if no block was given, the
99
99
  # return value of the block otherwise.
100
100
  def pass!(interops = {}, options = {}, &block)
101
- backend.call(*[contents(interops), options.delete(:env) || {}, options].flatten, &block)
101
+ backend.call(*[contents(interops), options.delete(:env) || {}, options].flatten(1), &block)
102
102
 
103
103
  rescue Errno::ENOENT
104
104
  raise NoCommandError, @command
@@ -128,6 +128,16 @@ module Command
128
128
 
129
129
  alias_method :run, :pass
130
130
 
131
+ # Whether or not to force an unsafe backend. This should only be
132
+ # used in cases where the developer wants the arguments passed to
133
+ # a shell, so that the unsafe interpolated arguments can be
134
+ # shell'd.
135
+ #
136
+ # @return [void]
137
+ def force_unsafe!
138
+ @backend = self.class.best_backend(true)
139
+ end
140
+
131
141
  # The command line being run by the runner. Interpolates the
132
142
  # arguments with the given interpolations.
133
143
  #
@@ -147,27 +157,41 @@ module Command
147
157
  #
148
158
  # @param string [String] the string to interpolate.
149
159
  # @param interops [Hash] the interpolations to make.
150
- # @return [String] the interpolated string.
160
+ # @return [Array<String>] the interpolated string.
151
161
  def interpolate(string, interops = {})
152
162
  interops = interops.to_a.map { |(k, v)| { k.to_s => v } }.inject(&:merge) || {}
153
-
154
- string.gsub(/(\{{1,2})([0-9a-zA-Z_\-]+)(\}{1,2})/) do |m|
155
- if interops.key?($2) && $1.length == $3.length
156
- if $1.length < 2 then escape(interops[$2].to_s) else interops[$2] end
163
+ results = [*string.shellsplit]
164
+
165
+ results.map do |part|
166
+ if part =~ /(\{{1,2})([0-9a-zA-Z_\-]+)(\}{1,2})/
167
+ if interops.key?($2) && $1.length == $3.length
168
+ if $1.length == 1
169
+ escape interops[$2].to_s
170
+ else
171
+ interops[$2].to_s.shellsplit
172
+ end
173
+ else
174
+ part
175
+ end
157
176
  else
158
- m
177
+ part
159
178
  end
160
- end
179
+ end.flatten
161
180
  end
162
181
 
163
182
  private
164
183
 
165
- # Escape the given string for a shell.
184
+ # Escape the given string for a shell if the backend is unsafe,
185
+ # otherwise it just returns the string.
166
186
  #
167
187
  # @param string [String] the string to escape.
168
188
  # @return [String] the escaped string.
169
189
  def escape(string)
170
- Shellwords.escape(string)
190
+ if backend.unsafe?
191
+ Shellwords.escape(string)
192
+ else
193
+ string
194
+ end
171
195
  end
172
196
 
173
197
  end
@@ -1,3 +1,5 @@
1
+ require 'stringio'
2
+
1
3
  module Command
2
4
  class Runner
3
5
  module Backends
@@ -11,6 +13,10 @@ module Command
11
13
  true
12
14
  end
13
15
 
16
+ def self.unsafe?
17
+ true
18
+ end
19
+
14
20
  # Initialize the fake backend.
15
21
  def initialize
16
22
  super
@@ -36,10 +42,14 @@ module Command
36
42
 
37
43
  with_modified_env(env) do
38
44
  start_time = Time.now
39
- output << `#{command} #{arguments}`
45
+ output << `#{command} #{arguments.join(' ')}`
40
46
  end_time = Time.now
41
47
  end
42
48
 
49
+ if $?.exitstatus == 127
50
+ raise NoCommandError
51
+ end
52
+
43
53
  message = Message.new :process_id => $?.pid,
44
54
  :exit_code => $?.exitstatus,
45
55
  :finished => true,
@@ -47,7 +57,7 @@ module Command
47
57
  :env => env,
48
58
  :options => {},
49
59
  :stdout => output,
50
- :line => [command, arguments].join(' '),
60
+ :line => [command, arguments].flatten.join(' '),
51
61
  :executed => true,
52
62
  :status => $?
53
63
 
@@ -16,6 +16,17 @@ module Command
16
16
  true
17
17
  end
18
18
 
19
+ # A backend is considered unsafe when the arguments are
20
+ # exposed directly to the shell. This is a vulnerability, so
21
+ # we mark the class as unsafe and when we're about to pass
22
+ # the arguments to the backend, escape the safe
23
+ # interpolations.
24
+ #
25
+ # @return [Boolean]
26
+ def self.unsafe?
27
+ false
28
+ end
29
+
19
30
  # Initialize the fake backend.
20
31
  def initialize
21
32
  @ran = []
@@ -41,7 +52,7 @@ module Command
41
52
  @ran << [command, arguments]
42
53
 
43
54
  message = Message.new :env => env, :options => options, :line =>
44
- [command, arguments].join(' ')
55
+ [command, *arguments].join(' ')
45
56
  end
46
57
 
47
58
  # Determines whether or not the given command and arguments were
@@ -52,7 +63,12 @@ module Command
52
63
  # @param arguments [String]
53
64
  # @return [Boolean]
54
65
  def ran?(command, arguments)
55
- @ran.include?([command, arguments])
66
+ @ran.include?([command, *arguments])
67
+ end
68
+
69
+ # (see ::unsafe?)
70
+ def unsafe?
71
+ self.class.unsafe?
56
72
  end
57
73
 
58
74
  end
@@ -26,8 +26,8 @@ module Command
26
26
  #
27
27
  # @see Spawn#spawn
28
28
  # @return [Numeric]
29
- def spawn(env, line, options)
30
- POSIX::Spawn.spawn(env, line, options)
29
+ def spawn(env, command, arguments, options)
30
+ POSIX::Spawn.spawn(env, command, *[arguments, options].flatten)
31
31
  end
32
32
 
33
33
  end
@@ -44,10 +44,10 @@ module Command
44
44
 
45
45
  stdin_w.close
46
46
 
47
- line = [command, arguments].join(' ')
47
+ line = [command, *arguments].join(' ')
48
48
 
49
49
  start_time = Time.now
50
- process_id = spawn(env, line, new_options)
50
+ process_id = spawn(env, command, arguments, new_options)
51
51
 
52
52
  future do
53
53
  _, status = wait2(process_id)
@@ -80,8 +80,8 @@ module Command
80
80
  #
81
81
  # @see Process.spawn
82
82
  # @return [Numeric] the process id
83
- def spawn(env, line, options)
84
- Process.spawn(env, line, options)
83
+ def spawn(env, command, arguments, options)
84
+ Process.spawn(env, command, *[arguments, options].flatten)
85
85
  end
86
86
 
87
87
  # Waits for the given process, and returns the process id and the
@@ -19,6 +19,10 @@ module Command
19
19
  end
20
20
  end
21
21
 
22
+ def self.unsafe?
23
+ true
24
+ end
25
+
22
26
  # Initializes the backend.
23
27
  #
24
28
  # @param host [String] the host to connect to.
@@ -36,7 +40,8 @@ module Command
36
40
  channel = @net_ssh.open_channel do |ch|
37
41
 
38
42
 
39
- ch.exec "#{command} #{arguments}" do |sch, success|
43
+ ch.exec "#{command} " \
44
+ "#{arguments.join(" ").shellescape}" do |sch, success|
40
45
  raise Errno::ENOENT unless success
41
46
 
42
47
  env.each do |k, v|
@@ -3,7 +3,7 @@ module Command
3
3
  class Runner
4
4
 
5
5
  # The current version of Runner.
6
- VERSION = "0.6.1".freeze
6
+ VERSION = "0.7.1".freeze
7
7
 
8
8
  end
9
9
  end
@@ -4,13 +4,16 @@ describe Command::Runner::Backends::Backticks do
4
4
  Command::Runner::Backends::Backticks.should be_available
5
5
  end
6
6
 
7
+ its(:unsafe?) { should be_true }
8
+
7
9
  it "returns a message" do
8
- value = subject.call("echo", "hello")
10
+ value = subject.call("echo", ["hello"])
9
11
  value.should be_instance_of Command::Runner::Message
10
12
  value.should be_executed
11
13
  end
12
14
 
13
15
  it "gives the correct time" do
14
- subject.call("sleep", "0.5").time.should be_within(0.1).of(0.5)
16
+ subject.call("sleep", ["0.5"]).time.should be_within(0.1).of(0.5)
15
17
  end
18
+
16
19
  end
@@ -8,46 +8,58 @@ describe Command::Runner do
8
8
  Command::Runner.new(command, arguments)
9
9
  end
10
10
 
11
- context "interpolating strings" do
11
+ context "when interpolating" do
12
12
  let(:command) { "echo" }
13
13
  let(:arguments) { "some {interpolation}" }
14
14
 
15
15
  it "interpolates correctly" do
16
- subject.contents(:interpolation => "test").should == ["echo", "some test"]
16
+ expect(
17
+ subject.contents(:interpolation => "test")
18
+ ).to eq ["echo", ["some", "test"]]
17
19
  end
18
20
 
19
21
  it "escapes bad values" do
20
- subject.contents(:interpolation => "`bad value`").should == ["echo", "some \\`bad\\ value\\`"]
22
+ expect(
23
+ subject.contents(:interpolation => "`bad value`")
24
+ ).to eq ["echo", ["some", "`bad value`"]]
21
25
  end
22
26
 
23
27
  it "doesn't interpolate interpolation values" do
24
- subject.contents(:interpolation => "{other}", :other => "hi").should == ["echo", "some \\{other\\}"]
28
+ expect(
29
+ subject.contents(:interpolation => "{other}", :other => "hi")
30
+ ).to eq ["echo", ["some", "{other}"]]
25
31
  end
26
32
  end
27
33
 
28
- context "double interpolated strings" do
34
+ context "when interpolating double strings" do
29
35
  let(:command) { "echo" }
30
36
  let(:arguments) { "some {{interpolation}}" }
31
37
 
32
38
  it "interpolates correctly" do
33
- subject.contents(:interpolation => "test").should == ["echo", "some test"]
39
+ expect(
40
+ subject.contents(:interpolation => "test")
41
+ ).to eq ["echo", ["some", "test"]]
34
42
  end
35
43
 
36
44
  it "doesn't escape bad values" do
37
- subject.contents(:interpolation => "`bad value`").should == ["echo", "some `bad value`"]
45
+ expect(
46
+ subject.contents(:interpolation => "`bad value`")
47
+ ).to eq ["echo", ["some", "`bad", "value`"]]
38
48
  end
39
49
  end
40
50
 
41
- context "misinterpolated strings" do
51
+ context "when interpolating misinterpolated strings" do
42
52
  let(:command) { "echo" }
43
53
  let(:arguments) { "some {{interpolation}" }
44
54
 
45
55
  it "doesn't interpolate" do
46
- subject.contents(:interpolation => "test").should == ["echo", "some {{interpolation}"]
56
+ expect(
57
+ subject.contents(:interpolation => "test")
58
+ ).to eq ["echo", ["some", "{{interpolation}"]]
47
59
  end
48
60
  end
49
61
 
50
- context "selecting backends" do
62
+ context "when selecting backends" do
51
63
  it "selects the best backend" do
52
64
  Command::Runner::Backends::PosixSpawn.stub(:available?).and_return(false)
53
65
  Command::Runner::Backends::Spawn.stub(:available?).and_return(true)
@@ -56,9 +68,13 @@ describe Command::Runner do
56
68
  Command::Runner::Backends::PosixSpawn.stub(:available?).and_return(true)
57
69
  Command::Runner.best_backend.should be_instance_of Command::Runner::Backends::PosixSpawn
58
70
  end
71
+
72
+ it "takes into account unsafe backends" do
73
+ Command::Runner.best_backend(true).should be_unsafe
74
+ end
59
75
  end
60
76
 
61
- context "bad commands" do
77
+ context "when given bad commands" do
62
78
  let(:command) { "some-non-existant-command" }
63
79
  let(:arguments) { "" }
64
80
 
@@ -69,12 +85,37 @@ describe Command::Runner do
69
85
  its(:pass) { should be_no_command }
70
86
 
71
87
  it "calls the block given" do
72
- subject.backend = Command::Runner::Backends::Backticks.new
73
88
  subject.pass do |message|
74
- message.should be_no_command
75
89
 
76
- message.line.should == "some-non-existant-command "
90
+ expect(message.line).to eq "some-non-existant-command "
91
+ end
92
+ end
93
+ end
94
+
95
+ context "when passing commands" do
96
+ let(:command) { "echo" }
97
+ let(:arguments) { "{interpolation}" }
98
+
99
+ before :each do
100
+ subject.backend = Command::Runner::Backends::Backticks.new
101
+ end
102
+
103
+ it "escapes bad values" do
104
+ subject.pass(:interpolation => "`uname -a`") do |message|
105
+ expect(message.stdout).to eq "`uname -a`\n"
77
106
  end
78
107
  end
108
+
109
+ it "returns the last value in the block" do
110
+ expect(
111
+ subject.pass(:interpolation => "hi") { |m| m.stdout }
112
+ ).to eq "hi\n"
113
+ end
114
+
115
+ it "returns a message" do
116
+ expect(
117
+ subject.pass
118
+ ).to be_a Command::Runner::Message
119
+ end
79
120
  end
80
121
  end
data/spec/spawn_spec.rb CHANGED
@@ -2,23 +2,31 @@ 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
+ its(:unsafe?) { should be_false }
6
+
5
7
  it "is available" do
6
8
  Command::Runner::Backends::Spawn.should be_available
7
9
  end
8
10
 
9
11
  it "returns a message" do
10
- value = subject.call("echo", "hello")
12
+ value = subject.call("echo", ["hello"])
11
13
  value.should be_instance_of Command::Runner::Message
12
14
  value.should be_executed
13
15
  end
14
16
 
15
17
  it "doesn't block" do
16
18
  start_time = Time.now
17
- value = subject.call("sleep", "0.5")
19
+ value = subject.call("sleep", ["0.5"])
18
20
  end_time = Time.now
19
21
 
20
22
  (end_time - start_time).should be_within((1.0/100)).of(0)
21
- value.time.should be_within((2.0/100)).of(0.5)
23
+ value.time.should be_within((3.0/100)).of(0.5)
24
+ end
25
+
26
+ it "doesn't expose arguments to the shell" do
27
+ value = subject.call("echo", ["`uname -a`"])
28
+
29
+ expect(value.stdout).to eq "`uname -a`\n"
22
30
  end
23
31
 
24
32
  it "can not be available" do
@@ -28,4 +36,5 @@ describe Command::Runner::Backends::Spawn do
28
36
  Command::Runner::Backends::Spawn.new
29
37
  }.to raise_error(Command::Runner::NotAvailableBackendError)
30
38
  end
39
+
31
40
  end
metadata CHANGED
@@ -1,108 +1,99 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: command-runner
3
- version: !ruby/object:Gem::Version
4
- version: 0.6.1
5
- prerelease:
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.7.1
6
5
  platform: ruby
7
- authors:
6
+ authors:
8
7
  - Jeremy Rodi
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-07-07 00:00:00.000000000 Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
11
+
12
+ date: 2014-01-05 00:00:00 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
15
  name: promise
16
- requirement: !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
19
- - - ~>
20
- - !ruby/object:Gem::Version
21
- version: '0.3'
22
- type: :runtime
23
16
  prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
17
+ requirement: &id001 !ruby/object:Gem::Requirement
18
+ requirements:
27
19
  - - ~>
28
- - !ruby/object:Gem::Version
29
- version: '0.3'
30
- - !ruby/object:Gem::Dependency
20
+ - !ruby/object:Gem::Version
21
+ version: "0.3"
22
+ type: :runtime
23
+ version_requirements: *id001
24
+ - !ruby/object:Gem::Dependency
31
25
  name: rspec
32
- requirement: !ruby/object:Gem::Requirement
33
- none: false
34
- requirements:
35
- - - ! '>='
36
- - !ruby/object:Gem::Version
37
- version: '0'
38
- type: :development
39
26
  prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ! '>='
44
- - !ruby/object:Gem::Version
45
- version: '0'
46
- - !ruby/object:Gem::Dependency
27
+ requirement: &id002 !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - &id003
30
+ - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id002
35
+ - !ruby/object:Gem::Dependency
47
36
  name: yard
48
- requirement: !ruby/object:Gem::Requirement
49
- none: false
50
- requirements:
51
- - - ! '>='
52
- - !ruby/object:Gem::Version
53
- version: '0'
37
+ prerelease: false
38
+ requirement: &id004 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - *id003
54
41
  type: :development
42
+ version_requirements: *id004
43
+ - !ruby/object:Gem::Dependency
44
+ name: fuubar
55
45
  prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
- requirements:
59
- - - ! '>='
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- description: ! " Runs a command or two in the shell with arguments that can be\n
63
- \ interpolated with the interpolation syntax.\n"
46
+ requirement: &id005 !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - *id003
49
+ type: :development
50
+ version_requirements: *id005
51
+ description: " Runs a command or two in the shell with arguments that can be\n interpolated with the interpolation syntax.\n"
64
52
  email: redjazz96@gmail.com
65
53
  executables: []
54
+
66
55
  extensions: []
56
+
67
57
  extra_rdoc_files: []
68
- files:
58
+
59
+ files:
69
60
  - README.md
70
- - lib/command/runner/version.rb
71
- - lib/command/runner/exceptions.rb
72
- - lib/command/runner/backends.rb
61
+ - lib/command/runner.rb
73
62
  - lib/command/runner/message.rb
74
- - lib/command/runner/backends/fake.rb
75
- - lib/command/runner/backends/backticks.rb
76
- - lib/command/runner/backends/ssh.rb
77
63
  - lib/command/runner/backends/posix_spawn.rb
64
+ - lib/command/runner/backends/ssh.rb
65
+ - lib/command/runner/backends/backticks.rb
66
+ - lib/command/runner/backends/fake.rb
78
67
  - lib/command/runner/backends/spawn.rb
79
- - lib/command/runner.rb
80
- - spec/messenger_spec.rb
68
+ - lib/command/runner/exceptions.rb
69
+ - lib/command/runner/backends.rb
70
+ - lib/command/runner/version.rb
81
71
  - spec/spawn_spec.rb
72
+ - spec/messenger_spec.rb
82
73
  - spec/backticks_spec.rb
83
74
  homepage: http://github.com/redjazz96/command-runner
84
75
  licenses: []
76
+
77
+ metadata: {}
78
+
85
79
  post_install_message:
86
80
  rdoc_options: []
87
- require_paths:
81
+
82
+ require_paths:
88
83
  - lib
89
- required_ruby_version: !ruby/object:Gem::Requirement
90
- none: false
91
- requirements:
92
- - - ! '>='
93
- - !ruby/object:Gem::Version
94
- version: '0'
95
- required_rubygems_version: !ruby/object:Gem::Requirement
96
- none: false
97
- requirements:
98
- - - ! '>='
99
- - !ruby/object:Gem::Version
100
- version: '0'
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - *id003
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - *id003
101
90
  requirements: []
91
+
102
92
  rubyforge_project:
103
- rubygems_version: 1.8.25
93
+ rubygems_version: 2.0.14
104
94
  signing_key:
105
- specification_version: 3
95
+ specification_version: 4
106
96
  summary: Runs commands.
107
97
  test_files: []
98
+
108
99
  has_rdoc: false