command-runner 0.6.1 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
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