pocketknife 0.1.0 → 0.2.0

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.
@@ -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