pocketknife 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 name of the node.
6
+ # @return [String] Name of the node.
7
7
  attr_accessor :name
8
8
 
9
- # Instance of a {Pocketknife}.
9
+ # @return [Pocketknife] The Pocketknife this node is associated with.
10
10
  attr_accessor :pocketknife
11
11
 
12
- # Instance of Rye::Box connection, cached by {#connection}.
12
+ # @return [Rye::Box] The Rye::Box connection, cached by {#connection}.
13
13
  attr_accessor :connection_cache
14
14
 
15
- # Hash with information about platform, cached by {#platform}.
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 Rye::Box connection.
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 "#{executable}" && test -x `which "#{executable}"`})
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<String, Object] Return a hash describing the node, see above.
78
- # @raise [UnsupportedInstallationPlatform] Raised if there's no installation information for this platform.
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
- # @raise [NotInstalling] Raised if Chef isn't installed, but user didn't allow installation.
104
- # @raise [UnsupportedInstallationPlatform] Raised if there's no installation information for this platform.
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 [] Prepares the upload, executes the block, and cleans up the upload when done.
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 "#{ETC_CHEF}" "#{VAR_POCKETKNIFE}" "#{VAR_POCKETKNIFE_CACHE}" "#{CHEF_SOLO_APPLY}" "#{CHEF_SOLO_APPLY_ALIAS}" &&
240
- mkdir -p "#{ETC_CHEF}" "#{VAR_POCKETKNIFE}" "#{VAR_POCKETKNIFE_CACHE}" "#{CHEF_SOLO_APPLY.dirname}"
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.say("Uploading new files...", false)
244
- self.connection.file_upload(self.local_node_json_pathname.to_s, NODE_JSON.to_s)
245
- self.connection.file_upload(TMP_TARBALL.to_s, VAR_POCKETKNIFE_TARBALL.to_s)
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
- self.say("Installing new files...", false)
248
- self.execute <<-HERE, true
249
- cd "#{VAR_POCKETKNIFE_CACHE}" &&
250
- tar xf "#{VAR_POCKETKNIFE_TARBALL}" &&
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 "#{TMP_SOLO_RB}" "#{SOLO_RB}" &&
254
- mv "#{TMP_CHEF_SOLO_APPLY}" "#{CHEF_SOLO_APPLY}" &&
255
- chmod u+x "#{CHEF_SOLO_APPLY}" &&
256
- ln -s "#{CHEF_SOLO_APPLY.basename}" "#{CHEF_SOLO_APPLY_ALIAS}" &&
257
- rm "#{VAR_POCKETKNIFE_TARBALL}" &&
258
- mv * "#{VAR_POCKETKNIFE}"
259
- HERE
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] Raised if something goes wrong with execution.
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
- # Instance of a Pocketknife.
6
+ # @return [Pocketknife] The Pocketknife instance to manage.
7
7
  attr_accessor :pocketknife
8
8
 
9
- # Hash of Node instances by their name.
9
+ # @return [Hash{String => Pocketknife::Node}] Node instances by their name.
10
10
  attr_accessor :nodes
11
11
 
12
- # Array of known nodes, used as cache by {#known_nodes}.
12
+ # @return [Array<Pocketknife::Node>] Known nodes, cached by {#known_nodes}.
13
13
  attr_accessor :known_nodes_cache
14
14
 
15
- # Instantiate a new manager.
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
- # Return a node. Uses cached value in {#known_nodes_cache} if available.
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. "henrietta".
42
- # @return [String] The complete node name, e.g. "henrietta.swa.gov.it"
43
- # @raise [NoSuchNode] A hostname could not be found for this node, either because the node doesn't exist or the abbreviated form isn't unique enough.
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>] nodes A list of node names.
63
- # @raise [Pocketknife::NoSuchNode] Raised if there's an unknown node.
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] Raised if can't find the +nodes+ directory.
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")
@@ -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
- MINOR = 1
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
@@ -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