Skip to content

Commit 3bc7ade

Browse files
committed
support commands/users/groups with spaces or other strange characters
1 parent baff92c commit 3bc7ade

File tree

3 files changed

+45
-22
lines changed

3 files changed

+45
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ appear at the top.
66
## [Unreleased][]
77

88
* Your contribution here!
9+
* [#451](https://github.com/capistrano/sshkit/pull/451): support commands/users/groups/directories with spaces or other shell characters - [@grosser](https://github.com/grosser)
910

1011
## [1.18.2][] (2019-02-03)
1112

lib/sshkit/command.rb

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'digest/sha1'
22
require 'securerandom'
3+
require 'shellwords'
34

45
# @author Lee Hambley
56
module SSHKit
@@ -145,7 +146,7 @@ def should_map?
145146

146147
def within(&_block)
147148
return yield unless options[:in]
148-
sprintf("cd #{options[:in]} && %s", yield)
149+
"cd #{options[:in].shellescape} && #{yield}"
149150
end
150151

151152
def environment_hash
@@ -155,8 +156,7 @@ def environment_hash
155156
def environment_string
156157
environment_hash.collect do |key,value|
157158
key_string = key.is_a?(Symbol) ? key.to_s.upcase : key.to_s
158-
escaped_value = value.to_s.gsub(/"/, '\"')
159-
%{#{key_string}="#{escaped_value}"}
159+
"#{key_string}=#{value.to_s.shellescape}"
160160
end.join(' ')
161161
end
162162

@@ -167,22 +167,22 @@ def with(&_block)
167167

168168
def user(&_block)
169169
return yield unless options[:user]
170-
"sudo -u #{options[:user]} #{environment_string + " " unless environment_string.empty?}-- sh -c '#{yield}'"
170+
"sudo -u #{options[:user].to_s.shellescape} #{environment_string + " " unless environment_string.empty?}-- sh -c #{yield.shellescape}"
171171
end
172172

173173
def in_background(&_block)
174174
return yield unless options[:run_in_background]
175-
sprintf("( nohup %s > /dev/null & )", yield)
175+
"( nohup #{yield} > /dev/null & )"
176176
end
177177

178178
def umask(&_block)
179179
return yield unless SSHKit.config.umask
180-
sprintf("umask #{SSHKit.config.umask} && %s", yield)
180+
"umask #{SSHKit.config.umask} && #{yield}"
181181
end
182182

183183
def group(&_block)
184184
return yield unless options[:group]
185-
%Q(sg #{options[:group]} -c "#{yield}")
185+
"sg #{options[:group].to_s.shellescape} -c #{yield.shellescape}"
186186
# We could also use the so-called heredoc format perhaps:
187187
#"newgrp #{options[:group]} <<EOC \\\"%s\\\" EOC" % %Q{#{yield}}
188188
end
@@ -213,7 +213,9 @@ def with_redaction
213213

214214
def to_s
215215
if should_map?
216-
[SSHKit.config.command_map[command.to_sym], *Array(args)].join(' ')
216+
arguments = Array(args)
217+
arguments = (arguments.any? ? arguments.shelljoin : [])
218+
[SSHKit.config.command_map[command.to_sym], *arguments].join(" ")
217219
else
218220
command.to_s
219221
end

test/unit/test_command.rb

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ def test_maps_a_command
99
assert_equal '/usr/bin/env example', c.to_command
1010
end
1111

12+
def test_maps_a_command_with_arguments
13+
c = Command.new('example', 'hello world')
14+
assert_equal '/usr/bin/env example hello\\ world', c.to_command
15+
end
16+
1217
def test_not_mapping_a_builtin
1318
%w{if test time}.each do |builtin|
1419
c = Command.new(builtin)
@@ -33,60 +38,65 @@ def test_leading_and_trailing_space_is_stripped
3338
def test_including_the_env
3439
SSHKit.config = nil
3540
c = Command.new(:rails, 'server', env: {rails_env: :production})
36-
assert_equal %{( export RAILS_ENV="production" ; /usr/bin/env rails server )}, c.to_command
41+
assert_equal %{( export RAILS_ENV=production ; /usr/bin/env rails server )}, c.to_command
3742
end
3843

3944
def test_including_the_env_with_multiple_keys
4045
SSHKit.config = nil
4146
c = Command.new(:rails, 'server', env: {rails_env: :production, foo: 'bar'})
42-
assert_equal %{( export RAILS_ENV="production" FOO="bar" ; /usr/bin/env rails server )}, c.to_command
47+
assert_equal %{( export RAILS_ENV=production FOO=bar ; /usr/bin/env rails server )}, c.to_command
4348
end
4449

4550
def test_including_the_env_with_string_keys
4651
SSHKit.config = nil
4752
c = Command.new(:rails, 'server', env: {'FACTER_env' => :production, foo: 'bar'})
48-
assert_equal %{( export FACTER_env="production" FOO="bar" ; /usr/bin/env rails server )}, c.to_command
53+
assert_equal %{( export FACTER_env=production FOO=bar ; /usr/bin/env rails server )}, c.to_command
4954
end
5055

5156
def test_double_quotes_are_escaped_in_env
5257
SSHKit.config = nil
5358
c = Command.new(:rails, 'server', env: {foo: 'asdf"hjkl'})
54-
assert_equal %{( export FOO="asdf\\\"hjkl" ; /usr/bin/env rails server )}, c.to_command
59+
assert_equal %{( export FOO=asdf\\\"hjkl ; /usr/bin/env rails server )}, c.to_command
5560
end
5661

5762
def test_percentage_symbol_handled_in_env
5863
SSHKit.config = nil
5964
c = Command.new(:rails, 'server', env: {foo: 'asdf%hjkl'}, user: "anotheruser")
60-
assert_equal %{( export FOO="asdf%hjkl" ; sudo -u anotheruser FOO=\"asdf%hjkl\" -- sh -c '/usr/bin/env rails server' )}, c.to_command
65+
assert_equal %{( export FOO=asdf\\%hjkl ; sudo -u anotheruser FOO=asdf\\%hjkl -- sh -c /usr/bin/env\\ rails\\ server )}, c.to_command
6166
end
6267

6368
def test_including_the_env_doesnt_addressively_escape
6469
SSHKit.config = nil
6570
c = Command.new(:rails, 'server', env: {path: '/example:$PATH'})
66-
assert_equal %{( export PATH="/example:$PATH" ; /usr/bin/env rails server )}, c.to_command
71+
assert_equal %{( export PATH=/example:\\$PATH ; /usr/bin/env rails server )}, c.to_command
6772
end
6873

6974
def test_global_env
7075
SSHKit.config = nil
7176
SSHKit.config.default_env = { default: 'env' }
7277
c = Command.new(:rails, 'server', env: {})
73-
assert_equal %{( export DEFAULT="env" ; /usr/bin/env rails server )}, c.to_command
78+
assert_equal %{( export DEFAULT=env ; /usr/bin/env rails server )}, c.to_command
7479
end
7580

7681
def test_default_env_is_overwritten_with_locally_defined
7782
SSHKit.config.default_env = { foo: 'bar', over: 'under' }
7883
c = Command.new(:rails, 'server', env: { over: 'write'})
79-
assert_equal %{( export FOO="bar" OVER="write" ; /usr/bin/env rails server )}, c.to_command
84+
assert_equal %{( export FOO=bar OVER=write ; /usr/bin/env rails server )}, c.to_command
8085
end
8186

8287
def test_working_in_a_given_directory
8388
c = Command.new(:ls, '-l', in: "/opt/sites")
8489
assert_equal "cd /opt/sites && /usr/bin/env ls -l", c.to_command
8590
end
8691

92+
def test_working_in_a_given_weird_directory
93+
c = Command.new(:ls, '-l', in: "/opt/sites and stuff")
94+
assert_equal "cd /opt/sites\\ and\\ stuff && /usr/bin/env ls -l", c.to_command
95+
end
96+
8797
def test_working_in_a_given_directory_with_env
8898
c = Command.new(:ls, '-l', in: "/opt/sites", env: {a: :b})
89-
assert_equal %{cd /opt/sites && ( export A="b" ; /usr/bin/env ls -l )}, c.to_command
99+
assert_equal %{cd /opt/sites && ( export A=b ; /usr/bin/env ls -l )}, c.to_command
90100
end
91101

92102
def test_having_a_host_passed
@@ -97,17 +107,27 @@ def test_having_a_host_passed
97107

98108
def test_working_as_a_given_user
99109
c = Command.new(:whoami, user: :anotheruser)
100-
assert_equal "sudo -u anotheruser -- sh -c '/usr/bin/env whoami'", c.to_command
110+
assert_equal "sudo -u anotheruser -- sh -c /usr/bin/env\\ whoami", c.to_command
111+
end
112+
113+
def test_working_as_a_given_weird_user
114+
c = Command.new(:whoami, user: "mr space |")
115+
assert_equal "sudo -u mr\\ space\\ \\| -- sh -c /usr/bin/env\\ whoami", c.to_command
101116
end
102117

103118
def test_working_as_a_given_group
104119
c = Command.new(:whoami, group: :devvers)
105-
assert_equal 'sg devvers -c "/usr/bin/env whoami"', c.to_command
120+
assert_equal 'sg devvers -c /usr/bin/env\\ whoami', c.to_command
121+
end
122+
123+
def test_working_as_a_given_weird_group
124+
c = Command.new(:whoami, group: "space | group")
125+
assert_equal "sg space\\ \\|\\ group -c /usr/bin/env\\ whoami", c.to_command
106126
end
107127

108128
def test_working_as_a_given_user_and_group
109129
c = Command.new(:whoami, user: :anotheruser, group: :devvers)
110-
assert_equal %Q(sudo -u anotheruser -- sh -c 'sg devvers -c "/usr/bin/env whoami"'), c.to_command
130+
assert_equal %Q(sudo -u anotheruser -- sh -c sg\\ devvers\\ -c\\ /usr/bin/env\\\\\\ whoami), c.to_command
111131
end
112132

113133
def test_umask
@@ -125,13 +145,13 @@ def test_umask_with_working_directory
125145
def test_umask_with_working_directory_and_user
126146
SSHKit.config.umask = '007'
127147
c = Command.new(:touch, 'somefile', in: '/var', user: 'alice')
128-
assert_equal "cd /var && umask 007 && sudo -u alice -- sh -c '/usr/bin/env touch somefile'", c.to_command
148+
assert_equal "cd /var && umask 007 && sudo -u alice -- sh -c /usr/bin/env\\ touch\\ somefile", c.to_command
129149
end
130150

131151
def test_umask_with_env_and_working_directory_and_user
132152
SSHKit.config.umask = '007'
133153
c = Command.new(:touch, 'somefile', user: 'bob', env: {a: 'b'}, in: '/var')
134-
assert_equal %{cd /var && umask 007 && ( export A="b" ; sudo -u bob A="b" -- sh -c '/usr/bin/env touch somefile' )}, c.to_command
154+
assert_equal %{cd /var && umask 007 && ( export A=b ; sudo -u bob A=b -- sh -c /usr/bin/env\\ touch\\ somefile )}, c.to_command
135155
end
136156

137157
def test_verbosity_defaults_to_logger_info

0 commit comments

Comments
 (0)