pocketknife 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +5 -0
- data/CHANGES.md +14 -0
- data/Gemfile +24 -9
- data/README.md +48 -18
- data/Rakefile +44 -4
- data/lib/pocketknife.rb +64 -15
- data/lib/pocketknife/errors.rb +101 -61
- data/lib/pocketknife/node.rb +131 -40
- data/lib/pocketknife/node_manager.rb +12 -11
- data/lib/pocketknife/version.rb +6 -1
- data/lib/shellwords.rb +153 -0
- data/pocketknife.gemspec +40 -33
- data/spec/pocketknife_node_spec.rb +64 -20
- data/spec/pocketknife_spec.rb +5 -1
- data/spec/spec_helper.rb +8 -1
- data/spec/support/silence_stream.rb +11 -0
- metadata +97 -43
- data/Gemfile.lock +0 -52
data/lib/pocketknife/node.rb
CHANGED
@@ -3,16 +3,16 @@ class Pocketknife
|
|
3
3
|
#
|
4
4
|
# A node represents a remote computer that will be managed with Pocketknife and <tt>chef-solo</tt>. It can connect to a node, execute commands on it, install the stack, and upload and apply configurations to it.
|
5
5
|
class Node
|
6
|
-
# String
|
6
|
+
# @return [String] Name of the node.
|
7
7
|
attr_accessor :name
|
8
8
|
|
9
|
-
#
|
9
|
+
# @return [Pocketknife] The Pocketknife this node is associated with.
|
10
10
|
attr_accessor :pocketknife
|
11
11
|
|
12
|
-
#
|
12
|
+
# @return [Rye::Box] The Rye::Box connection, cached by {#connection}.
|
13
13
|
attr_accessor :connection_cache
|
14
14
|
|
15
|
-
# Hash
|
15
|
+
# @return [Hash{Symbol => String, Numeric}] Information about platform, cached by {#platform}.
|
16
16
|
attr_accessor :platform_cache
|
17
17
|
|
18
18
|
# Initialize a new node.
|
@@ -25,9 +25,11 @@ class Pocketknife
|
|
25
25
|
self.connection_cache = nil
|
26
26
|
end
|
27
27
|
|
28
|
-
# Returns a
|
28
|
+
# Returns a connection.
|
29
29
|
#
|
30
30
|
# Caches result to {#connection_cache}.
|
31
|
+
#
|
32
|
+
# @return [Rye::Box]
|
31
33
|
def connection
|
32
34
|
return self.connection_cache ||= begin
|
33
35
|
rye = Rye::Box.new(self.name, :user => "root")
|
@@ -40,6 +42,7 @@ class Pocketknife
|
|
40
42
|
#
|
41
43
|
# @param [String] message The message to display.
|
42
44
|
# @param [Boolean] importance How important is this? +true+ means important, +nil+ means normal, +false+ means unimportant.
|
45
|
+
# @return [void]
|
43
46
|
def say(message, importance=nil)
|
44
47
|
self.pocketknife.say("* #{self.name}: #{message}", importance)
|
45
48
|
end
|
@@ -57,7 +60,7 @@ class Pocketknife
|
|
57
60
|
# @return [Boolean] Has executable?
|
58
61
|
def has_executable?(executable)
|
59
62
|
begin
|
60
|
-
self.connection.execute(%{which
|
63
|
+
self.connection.execute(%{which #{executable.shellescape} && test -x `which #{executable.shellescape}`})
|
61
64
|
return true
|
62
65
|
rescue Rye::Err
|
63
66
|
return false
|
@@ -68,14 +71,14 @@ class Pocketknife
|
|
68
71
|
#
|
69
72
|
# The information is formatted similar to this:
|
70
73
|
# {
|
71
|
-
# :distributor=>"Ubuntu", # String with distributor name
|
72
|
-
# :codename=>"maverick", # String with release codename
|
73
|
-
# :release=>"10.10", # String with release number
|
74
|
-
# :version=>10.1 # Float with release number
|
74
|
+
# :distributor => "Ubuntu", # String with distributor name
|
75
|
+
# :codename => "maverick", # String with release codename
|
76
|
+
# :release => "10.10", # String with release number
|
77
|
+
# :version => 10.1 # Float with release number
|
75
78
|
# }
|
76
79
|
#
|
77
|
-
# @return [Hash
|
78
|
-
# @raise [UnsupportedInstallationPlatform]
|
80
|
+
# @return [Hash{Symbol => String, Numeric}] Return a hash describing the node, see above.
|
81
|
+
# @raise [UnsupportedInstallationPlatform] Don't know how to install on this platform.
|
79
82
|
def platform
|
80
83
|
return self.platform_cache ||= begin
|
81
84
|
lsb_release = "/etc/lsb-release"
|
@@ -100,8 +103,9 @@ class Pocketknife
|
|
100
103
|
|
101
104
|
# Installs Chef and its dependencies on a node if needed.
|
102
105
|
#
|
103
|
-
# @
|
104
|
-
# @raise [
|
106
|
+
# @return [void]
|
107
|
+
# @raise [NotInstalling] Can't install because user Chef isn't already present and user forbade automatic installation.
|
108
|
+
# @raise [UnsupportedInstallationPlatform] Don't know how to install on this platform.
|
105
109
|
def install
|
106
110
|
unless self.has_executable?("chef-solo")
|
107
111
|
case self.pocketknife.can_install
|
@@ -136,6 +140,8 @@ class Pocketknife
|
|
136
140
|
end
|
137
141
|
|
138
142
|
# Installs Chef on the remote node.
|
143
|
+
#
|
144
|
+
# @return [void]
|
139
145
|
def install_chef
|
140
146
|
self.say("Installing chef...")
|
141
147
|
self.execute("gem install --no-rdoc --no-ri chef", true)
|
@@ -143,6 +149,8 @@ class Pocketknife
|
|
143
149
|
end
|
144
150
|
|
145
151
|
# Installs Rubygems on the remote node.
|
152
|
+
#
|
153
|
+
# @return [void]
|
146
154
|
def install_rubygems
|
147
155
|
self.say("Installing rubygems...")
|
148
156
|
self.execute(<<-HERE, true)
|
@@ -158,6 +166,8 @@ cd /root &&
|
|
158
166
|
end
|
159
167
|
|
160
168
|
# Installs Ruby on the remote node.
|
169
|
+
#
|
170
|
+
# @return [void]
|
161
171
|
def install_ruby
|
162
172
|
command = \
|
163
173
|
case self.platform[:distributor].downcase
|
@@ -184,21 +194,26 @@ cd /root &&
|
|
184
194
|
# mynode.upload
|
185
195
|
# end
|
186
196
|
#
|
187
|
-
# @yield
|
197
|
+
# @yield to execute the block, will prepare upload before block is invoked, and cleanup the temporary files afterwards.
|
198
|
+
# @return [void]
|
188
199
|
def self.prepare_upload(&block)
|
200
|
+
# TODO make this an instance method so it can avoid creating a tarball if using :rsync
|
189
201
|
begin
|
190
202
|
# TODO either do this in memory or scope this to the PID to allow concurrency
|
191
203
|
TMP_SOLO_RB.open("w") {|h| h.write(SOLO_RB_CONTENT)}
|
192
204
|
TMP_CHEF_SOLO_APPLY.open("w") {|h| h.write(CHEF_SOLO_APPLY_CONTENT)}
|
193
205
|
TMP_TARBALL.open("w") do |handle|
|
206
|
+
items = [
|
207
|
+
VAR_POCKETKNIFE_COOKBOOKS.basename,
|
208
|
+
VAR_POCKETKNIFE_SITE_COOKBOOKS.basename,
|
209
|
+
VAR_POCKETKNIFE_ROLES.basename,
|
210
|
+
VAR_POCKETKNIFE_DATA_BAGS.basename,
|
211
|
+
TMP_SOLO_RB,
|
212
|
+
TMP_CHEF_SOLO_APPLY
|
213
|
+
].reject { |o| not File.exist?(o) }.map {|o| o.to_s}
|
214
|
+
|
194
215
|
Archive::Tar::Minitar.pack(
|
195
|
-
|
196
|
-
VAR_POCKETKNIFE_COOKBOOKS.basename.to_s,
|
197
|
-
VAR_POCKETKNIFE_SITE_COOKBOOKS.basename.to_s,
|
198
|
-
VAR_POCKETKNIFE_ROLES.basename.to_s,
|
199
|
-
TMP_SOLO_RB.to_s,
|
200
|
-
TMP_CHEF_SOLO_APPLY.to_s
|
201
|
-
],
|
216
|
+
items,
|
202
217
|
handle
|
203
218
|
)
|
204
219
|
end
|
@@ -217,7 +232,10 @@ cd /root &&
|
|
217
232
|
end
|
218
233
|
|
219
234
|
# Cleans up cache of shared files uploaded to all nodes. This cache is created by the {prepare_upload} method.
|
235
|
+
#
|
236
|
+
# @return [void]
|
220
237
|
def self.cleanup_upload
|
238
|
+
# TODO make this an instance method so it can avoid creating a tarball if using :rsync
|
221
239
|
[
|
222
240
|
TMP_TARBALL,
|
223
241
|
TMP_SOLO_RB,
|
@@ -227,52 +245,120 @@ cd /root &&
|
|
227
245
|
end
|
228
246
|
end
|
229
247
|
|
248
|
+
# Rsync files to a node.
|
249
|
+
#
|
250
|
+
# @param [Array] args Arguments to sent to +rsync+, e.g. options, filenames, and target.
|
251
|
+
# @return [void]
|
252
|
+
# @raise [RsyncError] Something went wrong with the rsync.
|
253
|
+
def rsync(*args)
|
254
|
+
command = ['rsync', *args.map{|o| o.shellescape}]
|
255
|
+
unless system *command
|
256
|
+
raise Pocketknife::RsyncError.new(command.join(' '), self.name)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Rsync a file to a node with options: <tt>--update --copy-links</tt>
|
261
|
+
#
|
262
|
+
# @param [Array] args Arguments to sent to +rsync+, e.g. options, filename, and target.
|
263
|
+
# @return [void]
|
264
|
+
# @raise [RsyncError] Something went wrong with the rsync.
|
265
|
+
def rsync_file(*args)
|
266
|
+
self.rsync("-uL", *args)
|
267
|
+
end
|
268
|
+
|
269
|
+
# Rsync directory to a node with options: <tt>--recursive --update --copy-links --delete</tt>
|
270
|
+
#
|
271
|
+
# @param [Array] args Arguments to sent to +rsync+, e.g. options, directory name, and target.
|
272
|
+
# @return [void]
|
273
|
+
# @raise [RsyncError] Something went wrong with the rsync.
|
274
|
+
def rsync_directory(*args)
|
275
|
+
self.rsync("-ruL", "--delete", *args)
|
276
|
+
end
|
277
|
+
|
230
278
|
# Uploads configuration information to node.
|
231
279
|
#
|
232
280
|
# IMPORTANT: You must first call {prepare_upload} to create the shared files that will be uploaded.
|
281
|
+
#
|
282
|
+
# @return [void]
|
233
283
|
def upload
|
234
284
|
self.say("Uploading configuration...")
|
235
285
|
|
236
286
|
self.say("Removing old files...", false)
|
237
287
|
self.execute <<-HERE
|
238
288
|
umask 0377 &&
|
239
|
-
rm -rf
|
240
|
-
mkdir -p
|
289
|
+
rm -rf #{ETC_CHEF.shellescape} #{VAR_POCKETKNIFE.shellescape} #{VAR_POCKETKNIFE_CACHE.shellescape} #{CHEF_SOLO_APPLY.shellescape} #{CHEF_SOLO_APPLY_ALIAS.shellescape} &&
|
290
|
+
mkdir -p #{ETC_CHEF.shellescape} #{VAR_POCKETKNIFE.shellescape} #{VAR_POCKETKNIFE_CACHE.shellescape} #{CHEF_SOLO_APPLY.dirname.shellescape}
|
241
291
|
HERE
|
242
292
|
|
243
|
-
self.
|
244
|
-
|
245
|
-
|
293
|
+
case self.pocketknife.transfer_mechanism
|
294
|
+
when :tar
|
295
|
+
self.say("Uploading new files...", false)
|
296
|
+
self.connection.file_upload(self.local_node_json_pathname.to_s, NODE_JSON.to_s)
|
297
|
+
self.connection.file_upload(TMP_TARBALL.to_s, VAR_POCKETKNIFE_TARBALL.to_s)
|
246
298
|
|
247
|
-
|
248
|
-
|
249
|
-
cd
|
250
|
-
tar xf
|
299
|
+
self.say("Installing new files...", false)
|
300
|
+
self.execute <<-HERE, true
|
301
|
+
cd #{VAR_POCKETKNIFE_CACHE.shellescape} &&
|
302
|
+
tar xf #{VAR_POCKETKNIFE_TARBALL.shellescape} &&
|
251
303
|
chmod -R u+rwX,go= . &&
|
252
304
|
chown -R root:root . &&
|
253
|
-
mv
|
254
|
-
mv
|
255
|
-
chmod u+x
|
256
|
-
ln -s
|
257
|
-
rm
|
258
|
-
mv *
|
259
|
-
|
305
|
+
mv #{TMP_SOLO_RB.shellescape} #{SOLO_RB.shellescape} &&
|
306
|
+
mv #{TMP_CHEF_SOLO_APPLY.shellescape} #{CHEF_SOLO_APPLY.shellescape} &&
|
307
|
+
chmod u+x #{CHEF_SOLO_APPLY.shellescape} &&
|
308
|
+
ln -s #{CHEF_SOLO_APPLY.basename.shellescape} #{CHEF_SOLO_APPLY_ALIAS.shellescape} &&
|
309
|
+
rm #{VAR_POCKETKNIFE_TARBALL.shellescape} &&
|
310
|
+
mv * #{VAR_POCKETKNIFE.shellescape}
|
311
|
+
HERE
|
312
|
+
|
313
|
+
when :rsync
|
314
|
+
self.say("Uploading new files...", false)
|
315
|
+
|
316
|
+
self.rsync_file("#{self.local_node_json_pathname}", "root@#{self.name}:#{NODE_JSON}")
|
317
|
+
|
318
|
+
%w[SOLO_RB CHEF_SOLO_APPLY].each do |fragment|
|
319
|
+
source = self.class.const_get("TMP_#{fragment}")
|
320
|
+
target = self.class.const_get(fragment)
|
321
|
+
self.rsync_file("#{source}", "root@#{self.name}:#{target}")
|
322
|
+
end
|
323
|
+
|
324
|
+
%w[COOKBOOKS SITE_COOKBOOKS ROLES DATA_BAGS].each do |fragment|
|
325
|
+
target = self.class.const_get("VAR_POCKETKNIFE_#{fragment}")
|
326
|
+
source = target.basename
|
327
|
+
next unless source.exist?
|
328
|
+
self.rsync_directory("#{source}/", "root@#{self.name}:#{target}")
|
329
|
+
end
|
330
|
+
|
331
|
+
self.say("Modifying new files...", false)
|
332
|
+
self.execute <<-HERE, true
|
333
|
+
cd #{VAR_POCKETKNIFE_CACHE.shellescape} &&
|
334
|
+
chmod u+x #{CHEF_SOLO_APPLY.shellescape} &&
|
335
|
+
ln -s #{CHEF_SOLO_APPLY.basename.shellescape} #{CHEF_SOLO_APPLY_ALIAS.shellescape}
|
336
|
+
HERE
|
337
|
+
|
338
|
+
else
|
339
|
+
raise InvalidTransferMechanism.new(self.pocketknife.transfer_mechanism)
|
340
|
+
end
|
260
341
|
|
261
342
|
self.say("Finished uploading!", false)
|
262
343
|
end
|
263
344
|
|
264
345
|
# Applies the configuration to the node. Installs Chef, Ruby and Rubygems if needed.
|
346
|
+
#
|
347
|
+
# @return [void]
|
265
348
|
def apply
|
266
349
|
self.install
|
267
350
|
|
268
351
|
self.say("Applying configuration...", true)
|
269
|
-
command = "chef-solo -j #{NODE_JSON}"
|
352
|
+
command = "chef-solo -j #{NODE_JSON.shellescape}"
|
353
|
+
command << " -o #{self.pocketknife.runlist}" if self.pocketknife.runlist
|
270
354
|
command << " -l debug" if self.pocketknife.verbosity == true
|
271
355
|
self.execute(command, true)
|
272
356
|
self.say("Finished applying!")
|
273
357
|
end
|
274
358
|
|
275
359
|
# Deploys the configuration to the node, which calls {#upload} and {#apply}.
|
360
|
+
#
|
361
|
+
# @return [void]
|
276
362
|
def deploy
|
277
363
|
self.upload
|
278
364
|
self.apply
|
@@ -283,7 +369,7 @@ cd "#{VAR_POCKETKNIFE_CACHE}" &&
|
|
283
369
|
# @param [String] commands Shell commands to execute.
|
284
370
|
# @param [Boolean] immediate Display execution information immediately to STDOUT, rather than returning it as an object when done.
|
285
371
|
# @return [Rye::Rap] A result object describing the completed execution.
|
286
|
-
# @raise [ExecutionError]
|
372
|
+
# @raise [ExecutionError] Something went wrong with the execution, the cause is described in the exception.
|
287
373
|
def execute(commands, immediate=false)
|
288
374
|
self.say("Executing:\n#{commands}", false)
|
289
375
|
if immediate
|
@@ -323,12 +409,17 @@ cd "#{VAR_POCKETKNIFE_CACHE}" &&
|
|
323
409
|
# Remote path to pocketknife's roles
|
324
410
|
# @private
|
325
411
|
VAR_POCKETKNIFE_ROLES = VAR_POCKETKNIFE + "roles"
|
412
|
+
# Remote path to pocketknife's databags
|
413
|
+
# @private
|
414
|
+
VAR_POCKETKNIFE_DATA_BAGS = VAR_POCKETKNIFE + "data_bags"
|
415
|
+
|
326
416
|
# Content of the solo.rb file
|
327
417
|
# @private
|
328
418
|
SOLO_RB_CONTENT = <<-HERE
|
329
419
|
file_cache_path "#{VAR_POCKETKNIFE_CACHE}"
|
330
420
|
cookbook_path ["#{VAR_POCKETKNIFE_COOKBOOKS}", "#{VAR_POCKETKNIFE_SITE_COOKBOOKS}"]
|
331
421
|
role_path "#{VAR_POCKETKNIFE_ROLES}"
|
422
|
+
data_bag_path "#{VAR_POCKETKNIFE_DATA_BAGS}"
|
332
423
|
HERE
|
333
424
|
# Remote path to chef-solo-apply
|
334
425
|
# @private
|
@@ -3,16 +3,16 @@ class Pocketknife
|
|
3
3
|
#
|
4
4
|
# This class finds, validates and manages {Pocketknife::Node} instances for a {Pocketknife}.
|
5
5
|
class NodeManager
|
6
|
-
#
|
6
|
+
# @return [Pocketknife] The Pocketknife instance to manage.
|
7
7
|
attr_accessor :pocketknife
|
8
8
|
|
9
|
-
# Hash
|
9
|
+
# @return [Hash{String => Pocketknife::Node}] Node instances by their name.
|
10
10
|
attr_accessor :nodes
|
11
11
|
|
12
|
-
# Array
|
12
|
+
# @return [Array<Pocketknife::Node>] Known nodes, cached by {#known_nodes}.
|
13
13
|
attr_accessor :known_nodes_cache
|
14
14
|
|
15
|
-
#
|
15
|
+
# Instantiates a new manager.
|
16
16
|
#
|
17
17
|
# @param [Pocketknife] pocketknife
|
18
18
|
def initialize(pocketknife)
|
@@ -21,7 +21,7 @@ class Pocketknife
|
|
21
21
|
self.known_nodes_cache = nil
|
22
22
|
end
|
23
23
|
|
24
|
-
#
|
24
|
+
# Returns a node. Uses cached value in {#known_nodes_cache} if available.
|
25
25
|
#
|
26
26
|
# @param [String] name A node name to find, can be an abbrevation.
|
27
27
|
# @return [Pocketknife::Node]
|
@@ -38,9 +38,9 @@ class Pocketknife
|
|
38
38
|
#
|
39
39
|
# The abbreviated node name given must match only one node exactly. For example, you'll get a {Pocketknife::NoSuchNode} if you ask for an abbreviated node by the name of <tt>giovanni</tt> when there are nodes called <tt>giovanni.boldini.it</tt> and <tt>giovanni.bellini.it</tt> -- you'd need to ask using a more specific name, such as <tt>giovanni.boldini</tt>.
|
40
40
|
#
|
41
|
-
# @param [String] abbreviated_name A node name, which may be abbreviated, e.g.
|
42
|
-
# @return [String] The complete node name, e.g.
|
43
|
-
# @raise [NoSuchNode]
|
41
|
+
# @param [String] abbreviated_name A node name, which may be abbreviated, e.g. <tt>henrietta</tt>.
|
42
|
+
# @return [String] The complete node name, e.g. <tt>henrietta.swa.gov.it</tt>.
|
43
|
+
# @raise [NoSuchNode] Couldn't find this node, either because it doesn't exist or the abbreviation isn't unique.
|
44
44
|
def hostname_for(abbreviated_name)
|
45
45
|
if self.known_nodes.include?(abbreviated_name)
|
46
46
|
return abbreviated_name
|
@@ -59,8 +59,9 @@ class Pocketknife
|
|
59
59
|
|
60
60
|
# Asserts that the specified nodes are known to Pocketknife.
|
61
61
|
#
|
62
|
-
# @param [Array<String>]
|
63
|
-
# @
|
62
|
+
# @param [Array<String>] names A list of node names.
|
63
|
+
# @return [void]
|
64
|
+
# @raise [Pocketknife::NoSuchNode] Couldn't find a node.
|
64
65
|
def assert_known(names)
|
65
66
|
for name in names
|
66
67
|
# This will raise a NoSuchNode exception if there's a problem.
|
@@ -73,7 +74,7 @@ class Pocketknife
|
|
73
74
|
# Caches results to {#known_nodes_cache}.
|
74
75
|
#
|
75
76
|
# @return [Array<String>] The node names.
|
76
|
-
# @raise [Errno::ENOENT]
|
77
|
+
# @raise [Errno::ENOENT] Can't find the +nodes+ directory.
|
77
78
|
def known_nodes
|
78
79
|
return(self.known_nodes_cache ||= begin
|
79
80
|
dir = Pathname.new("nodes")
|
data/lib/pocketknife/version.rb
CHANGED
@@ -3,11 +3,16 @@ class Pocketknife
|
|
3
3
|
#
|
4
4
|
# Information about the Pocketknife version.
|
5
5
|
module Version
|
6
|
+
# @return [Integer] Major version.
|
6
7
|
MAJOR = 0
|
7
|
-
|
8
|
+
# @return [Integer] Minor version.
|
9
|
+
MINOR = 2
|
10
|
+
# @return [Integer] Patch version.
|
8
11
|
PATCH = 0
|
12
|
+
# @return [Integer] Build version.
|
9
13
|
BUILD = nil
|
10
14
|
|
15
|
+
# @return [String] The version as a string.
|
11
16
|
STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
|
12
17
|
end
|
13
18
|
end
|
data/lib/shellwords.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
#
|
2
|
+
# shellwords.rb: Manipulates strings a la UNIX Bourne shell
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# This module manipulates strings according to the word parsing rules
|
7
|
+
# of the UNIX Bourne shell.
|
8
|
+
#
|
9
|
+
# The shellwords() function was originally a port of shellwords.pl,
|
10
|
+
# but modified to conform to POSIX / SUSv3 (IEEE Std 1003.1-2001).
|
11
|
+
#
|
12
|
+
# Authors:
|
13
|
+
# - Wakou Aoyama
|
14
|
+
# - Akinori MUSHA <knu@iDaemons.org>
|
15
|
+
#
|
16
|
+
# Contact:
|
17
|
+
# - Akinori MUSHA <knu@iDaemons.org> (current maintainer)
|
18
|
+
#
|
19
|
+
module Shellwords
|
20
|
+
# Splits a string into an array of tokens in the same way the UNIX
|
21
|
+
# Bourne shell does.
|
22
|
+
#
|
23
|
+
# argv = Shellwords.split('here are "two words"')
|
24
|
+
# argv #=> ["here", "are", "two words"]
|
25
|
+
#
|
26
|
+
# String#shellsplit is a shorthand for this function.
|
27
|
+
#
|
28
|
+
# argv = 'here are "two words"'.shellsplit
|
29
|
+
# argv #=> ["here", "are", "two words"]
|
30
|
+
def shellsplit(line)
|
31
|
+
words = []
|
32
|
+
field = ''
|
33
|
+
line.scan(/\G\s*(?>([^\s\\\'\"]+)|'([^\']*)'|"((?:[^\"\\]|\\.)*)"|(\\.?)|(\S))(\s|\z)?/m) do
|
34
|
+
|word, sq, dq, esc, garbage, sep|
|
35
|
+
raise ArgumentError, "Unmatched double quote: #{line.inspect}" if garbage
|
36
|
+
field << (word || sq || (dq || esc).gsub(/\\(.)/, '\\1'))
|
37
|
+
if sep
|
38
|
+
words << field
|
39
|
+
field = ''
|
40
|
+
end
|
41
|
+
end
|
42
|
+
words
|
43
|
+
end
|
44
|
+
|
45
|
+
alias shellwords shellsplit
|
46
|
+
|
47
|
+
module_function :shellsplit, :shellwords
|
48
|
+
|
49
|
+
class << self
|
50
|
+
alias split shellsplit
|
51
|
+
end
|
52
|
+
|
53
|
+
# Escapes a string so that it can be safely used in a Bourne shell
|
54
|
+
# command line.
|
55
|
+
#
|
56
|
+
# Note that a resulted string should be used unquoted and is not
|
57
|
+
# intended for use in double quotes nor in single quotes.
|
58
|
+
#
|
59
|
+
# open("| grep #{Shellwords.escape(pattern)} file") { |pipe|
|
60
|
+
# # ...
|
61
|
+
# }
|
62
|
+
#
|
63
|
+
# String#shellescape is a shorthand for this function.
|
64
|
+
#
|
65
|
+
# open("| grep #{pattern.shellescape} file") { |pipe|
|
66
|
+
# # ...
|
67
|
+
# }
|
68
|
+
#
|
69
|
+
# It is caller's responsibility to encode the string in the right
|
70
|
+
# encoding for the shell environment where this string is used.
|
71
|
+
# Multibyte characters are treated as multibyte characters, not
|
72
|
+
# bytes.
|
73
|
+
def shellescape(str)
|
74
|
+
# An empty argument will be skipped, so return empty quotes.
|
75
|
+
return "''" if str.empty?
|
76
|
+
|
77
|
+
str = str.dup
|
78
|
+
|
79
|
+
# Treat multibyte characters as is. It is caller's responsibility
|
80
|
+
# to encode the string in the right encoding for the shell
|
81
|
+
# environment.
|
82
|
+
str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1")
|
83
|
+
|
84
|
+
# A LF cannot be escaped with a backslash because a backslash + LF
|
85
|
+
# combo is regarded as line continuation and simply ignored.
|
86
|
+
str.gsub!(/\n/, "'\n'")
|
87
|
+
|
88
|
+
return str
|
89
|
+
end
|
90
|
+
|
91
|
+
module_function :shellescape
|
92
|
+
|
93
|
+
class << self
|
94
|
+
alias escape shellescape
|
95
|
+
end
|
96
|
+
|
97
|
+
# Builds a command line string from an argument list +array+ joining
|
98
|
+
# all elements escaped for Bourne shell and separated by a space.
|
99
|
+
#
|
100
|
+
# open('|' + Shellwords.join(['grep', pattern, *files])) { |pipe|
|
101
|
+
# # ...
|
102
|
+
# }
|
103
|
+
#
|
104
|
+
# Array#shelljoin is a shorthand for this function.
|
105
|
+
#
|
106
|
+
# open('|' + ['grep', pattern, *files].shelljoin) { |pipe|
|
107
|
+
# # ...
|
108
|
+
# }
|
109
|
+
#
|
110
|
+
def shelljoin(array)
|
111
|
+
array.map { |arg| shellescape(arg) }.join(' ')
|
112
|
+
end
|
113
|
+
|
114
|
+
module_function :shelljoin
|
115
|
+
|
116
|
+
class << self
|
117
|
+
alias join shelljoin
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# @!visibility private
|
122
|
+
class String
|
123
|
+
# call-seq:
|
124
|
+
# str.shellsplit => array
|
125
|
+
#
|
126
|
+
# Splits +str+ into an array of tokens in the same way the UNIX
|
127
|
+
# Bourne shell does. See Shellwords::shellsplit for details.
|
128
|
+
def shellsplit
|
129
|
+
Shellwords.split(self)
|
130
|
+
end
|
131
|
+
|
132
|
+
# call-seq:
|
133
|
+
# str.shellescape => string
|
134
|
+
#
|
135
|
+
# Escapes +str+ so that it can be safely used in a Bourne shell
|
136
|
+
# command line. See Shellwords::shellescape for details.
|
137
|
+
def shellescape
|
138
|
+
Shellwords.escape(self)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# @!visibility private
|
143
|
+
class Array
|
144
|
+
# call-seq:
|
145
|
+
# array.shelljoin => string
|
146
|
+
#
|
147
|
+
# Builds a command line string from an argument list +array+ joining
|
148
|
+
# all elements escaped for Bourne shell and separated by a space.
|
149
|
+
# See Shellwords::shelljoin for details.
|
150
|
+
def shelljoin
|
151
|
+
Shellwords.join(self)
|
152
|
+
end
|
153
|
+
end
|