terrapin 0.6.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +6 -0
  4. data/GOALS +8 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE +26 -0
  7. data/NEWS.md +85 -0
  8. data/README.md +215 -0
  9. data/Rakefile +13 -0
  10. data/lib/terrapin.rb +12 -0
  11. data/lib/terrapin/command_line.rb +197 -0
  12. data/lib/terrapin/command_line/multi_pipe.rb +50 -0
  13. data/lib/terrapin/command_line/output.rb +12 -0
  14. data/lib/terrapin/command_line/runners.rb +7 -0
  15. data/lib/terrapin/command_line/runners/backticks_runner.rb +30 -0
  16. data/lib/terrapin/command_line/runners/fake_runner.rb +30 -0
  17. data/lib/terrapin/command_line/runners/popen_runner.rb +29 -0
  18. data/lib/terrapin/command_line/runners/posix_runner.rb +49 -0
  19. data/lib/terrapin/command_line/runners/process_runner.rb +41 -0
  20. data/lib/terrapin/exceptions.rb +8 -0
  21. data/lib/terrapin/os_detector.rb +27 -0
  22. data/lib/terrapin/version.rb +4 -0
  23. data/spec/spec_helper.rb +31 -0
  24. data/spec/support/fake_logger.rb +18 -0
  25. data/spec/support/have_output.rb +11 -0
  26. data/spec/support/nonblocking_examples.rb +14 -0
  27. data/spec/support/stub_os.rb +25 -0
  28. data/spec/support/unsetting_exitstatus.rb +7 -0
  29. data/spec/support/with_exitstatus.rb +12 -0
  30. data/spec/terrapin/command_line/output_spec.rb +14 -0
  31. data/spec/terrapin/command_line/runners/backticks_runner_spec.rb +24 -0
  32. data/spec/terrapin/command_line/runners/fake_runner_spec.rb +22 -0
  33. data/spec/terrapin/command_line/runners/popen_runner_spec.rb +24 -0
  34. data/spec/terrapin/command_line/runners/posix_runner_spec.rb +40 -0
  35. data/spec/terrapin/command_line/runners/process_runner_spec.rb +40 -0
  36. data/spec/terrapin/command_line_spec.rb +195 -0
  37. data/spec/terrapin/errors_spec.rb +62 -0
  38. data/spec/terrapin/os_detector_spec.rb +23 -0
  39. data/spec/terrapin/runners_spec.rb +97 -0
  40. data/terrapin.gemspec +28 -0
  41. metadata +209 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2f5aede86af5c29288c8f251af1dbf9bd1a179b90a416dd638ea02c3e29930a3
4
+ data.tar.gz: 4dd46fea24d0a8a78c0c39959d5c08dcfeec5a4429a1f18acfc87323811c18bb
5
+ SHA512:
6
+ metadata.gz: 28e367c3deb7c6b6064a6dd2cfd1600646261302d135805ba47e3fbba18a2ee2256dbb2b9a379378c20bc1a770bcfdfc5b95fe668e5d499d3f2eda2bf1c49677
7
+ data.tar.gz: 873cb7a8046310942c2533003ae09a865fde8288fe64e97484c69d588d042b818e24ffd3b2e06e38308cffee1de99dbc86490231daf24b1cd8bade289e64f1ea
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.rbc
2
+ .rbx
3
+ *.gem
4
+ Gemfile.lock
5
+ *.swp
6
+ *.swo
7
+ .bundle
8
+ bin
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ rvm:
2
+ - 1.9.3
3
+ - 2.0.0
4
+ - 2.1.5
5
+ - jruby-19mode
6
+ - rbx-2
data/GOALS ADDED
@@ -0,0 +1,8 @@
1
+ Terrapin hits 1.0.0 when:
2
+
3
+ [*] It can run command lines across all major unix platforms.
4
+ [*] It can run command lines on Windows.
5
+ [*] It handles quoting.
6
+ [*] It takes advantage of OS-specific functionality to be thread-safe,
7
+ when possible.
8
+ [ ] It has a consistent and functioning API that pleases us.
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ platforms :ruby do
6
+ gem "posix-spawn"
7
+ end
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+
2
+ LICENSE
3
+
4
+ The MIT License
5
+
6
+ Copyright (c) 2011-2014 Jon Yurek and thoughtbot, inc.
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in
16
+ all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
+ THE SOFTWARE.
25
+
26
+
data/NEWS.md ADDED
@@ -0,0 +1,85 @@
1
+ New for 0.5.7:
2
+
3
+ * Feature: Allow collection of both STDOUT and STDERR.
4
+ * Improvement: Convert arguments to strings when possible
5
+
6
+ New for 0.5.6:
7
+
8
+ * Bug Fix: Java does not need to run commands with `env`
9
+ * Bug Fix: Found out we were rescuing the wrong error
10
+
11
+ New for 0.5.5:
12
+
13
+ * Bug Fix: Posix- and ProcessRunner respect paths *and* are thread safe!
14
+ * Bug Fix: `exitstatus` should always be set, even if command doesn't run.
15
+ * Test Fix: Do not try to test Runners if they don't run on this system.
16
+ * Improvement: Pass the Errno::ENOENT message through to the exception.
17
+ * Improvement: Improve documentation
18
+
19
+ New for 0.5.4:
20
+
21
+ * Bug Fix: PosixRunner and ProcessRunner respect supplemental paths now.
22
+
23
+ New for 0.5.3:
24
+
25
+ * SECURITY: Fix exploitable bug that could allow arbitrary command execution.
26
+ See CVE-2013-4457 for more details. Thanks to Holger Just for report and fix!
27
+ * Bug fix: Sub-word interpolations can be confused for the longer version
28
+
29
+ New for 0.5.2:
30
+
31
+ * Improvement: Close all the IO objects!
32
+ * Feature: Add an Runner that uses IO.popen, so JRuby can play
33
+ * Improvement: Officially drop Ruby 1.8 support, add Ruby 2.0 support
34
+ * Bug fix: Prevent a crash if no command was actually run
35
+ * Improvement: Add security cautions to the README
36
+
37
+ New for 0.5.1:
38
+
39
+ * Fixed a bug preventing running on 1.8.7 for no good reason.
40
+
41
+ New for 0.5.0:
42
+
43
+ * Updated the copyrights to 2013
44
+ * Added UTF encoding markers on code files to ensure they're interpreted as
45
+ UTF-8 instead of ASCII.
46
+ * Swapped the ordering of the PATH and supplemental path. A binary in the
47
+ supplemental path will take precedence, now.
48
+ * Errors contain the output of the erroring command, for inspection.
49
+ * Use climate_control instead for environment management.
50
+
51
+ New for 0.4.2:
52
+
53
+ * Loggers that don't understand `tty?`, like `ActiveSupport::BufferedLogger`
54
+ will still work.
55
+
56
+ New for 0.4.1:
57
+
58
+ * Introduce FakeRunner for testing, so you don't really run commands.
59
+ * Fix logging: output the actual command, not the un-interpolated pattern.
60
+ * Prevent color codes from being output if log destination isn't a TTY.
61
+
62
+ New for 0.4.0:
63
+
64
+ * Moved interpolation to the `run` method, instead of interpolating on `new`.
65
+ * Remove official support for REE.
66
+
67
+ New for 0.3.2:
68
+
69
+ * Fix a hang when processes wait for IO.
70
+
71
+ New for 0.3.1:
72
+
73
+ * Made the `Runner` manually swappable, in case `ProcessRunner` doesn't work
74
+ for some reason.
75
+ * Fixed copyright years.
76
+
77
+ New for 0.3.0:
78
+
79
+ * Support blank arguments.
80
+ * Add `CommandLine#unix?`.
81
+ * Add `CommandLine#exit_status`.
82
+ * Automatically use `POSIX::Spawn` if available.
83
+ * Add `CommandLine#environment` as a hash of extra `ENV` data..
84
+ * Add `CommandLine#runner` which produces an object that responds to `#call`.
85
+ * Fix a race condition but only on Ruby 1.9.
data/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # Terrapin [![Build Status](https://secure.travis-ci.org/thoughtbot/terrapin.png?branch=master)](http://travis-ci.org/thoughtbot/terrapin)
2
+
3
+ A small library for doing (command) lines.
4
+
5
+ [API reference](http://rubydoc.info/gems/terrapin/)
6
+
7
+ ## Usage
8
+
9
+ The basic, normal stuff:
10
+
11
+ ```ruby
12
+ line = Terrapin::CommandLine.new("echo", "hello 'world'")
13
+ line.command # => "echo hello 'world'"
14
+ line.run # => "hello world\n"
15
+ ```
16
+
17
+ Interpolated arguments:
18
+
19
+ ```ruby
20
+ line = Terrapin::CommandLine.new("convert", ":in -scale :resolution :out")
21
+ line.command(in: "omg.jpg",
22
+ resolution: "32x32",
23
+ out: "omg_thumb.jpg")
24
+ # => "convert 'omg.jpg' -scale '32x32' 'omg_thumb.jpg'"
25
+ ```
26
+
27
+ It prevents attempts at being bad:
28
+
29
+ ```ruby
30
+ line = Terrapin::CommandLine.new("cat", ":file")
31
+ line.command(file: "haha`rm -rf /`.txt") # => "cat 'haha`rm -rf /`.txt'"
32
+
33
+ line = Terrapin::CommandLine.new("cat", ":file")
34
+ line.command(file: "ohyeah?'`rm -rf /`.ha!") # => "cat 'ohyeah?'\\''`rm -rf /`.ha!'"
35
+ ```
36
+
37
+ NOTE: It only does that for arguments interpolated via `run`, NOT arguments
38
+ passed into `new` (see 'Security' below):
39
+
40
+ ```ruby
41
+ line = Terrapin::CommandLine.new("echo", "haha`whoami`")
42
+ line.command # => "echo haha`whoami`"
43
+ line.run # => "hahawebserver"
44
+ ```
45
+
46
+ You can ignore the result:
47
+
48
+ ```ruby
49
+ line = Terrapin::CommandLine.new("noisy", "--extra-verbose", swallow_stderr: true)
50
+ line.command # => "noisy --extra-verbose 2>/dev/null"
51
+
52
+ # ... and on Windows...
53
+ line.command # => "noisy --extra-verbose 2>NUL"
54
+ ```
55
+
56
+ If your command errors, you get an exception:
57
+
58
+ ```ruby
59
+ line = Terrapin::CommandLine.new("git", "commit")
60
+ begin
61
+ line.run
62
+ rescue Terrapin::ExitStatusError => e
63
+ e.message # => "Command 'git commit' returned 1. Expected 0"
64
+ end
65
+ ```
66
+
67
+ If your command might return something non-zero, and you expect that, it's cool:
68
+
69
+ ```ruby
70
+ line = Terrapin::CommandLine.new("/usr/bin/false", "", expected_outcodes: [0, 1])
71
+ begin
72
+ line.run
73
+ rescue Terrapin::ExitStatusError => e
74
+ # => You never get here!
75
+ end
76
+ ```
77
+
78
+ You don't have the command? You get an exception:
79
+
80
+ ```ruby
81
+ line = Terrapin::CommandLine.new("lolwut")
82
+ begin
83
+ line.run
84
+ rescue Terrapin::CommandNotFoundError => e
85
+ e # => the command isn't in the $PATH for this process.
86
+ end
87
+ ```
88
+
89
+ But don't fear, you can specify where to look for the command:
90
+
91
+ ```ruby
92
+ Terrapin::CommandLine.path = "/opt/bin"
93
+ line = Terrapin::CommandLine.new("lolwut")
94
+ line.command # => "lolwut", but it looks in /opt/bin for it.
95
+ ```
96
+
97
+ You can even give it a bunch of places to look:
98
+
99
+ ```ruby
100
+ FileUtils.rm("/opt/bin/lolwut")
101
+ File.open('/usr/local/bin/lolwut') {|f| f.write('echo Hello') }
102
+ Terrapin::CommandLine.path = ["/opt/bin", "/usr/local/bin"]
103
+ line = Terrapin::CommandLine.new("lolwut")
104
+ line.run # => prints 'Hello', because it searches the path
105
+ ```
106
+
107
+ Or just put it in the command:
108
+
109
+ ```ruby
110
+ line = Terrapin::CommandLine.new("/opt/bin/lolwut")
111
+ line.command # => "/opt/bin/lolwut"
112
+ ```
113
+
114
+ You can see what's getting run. The 'Command' part it logs is in green for visibility!
115
+
116
+ ```ruby
117
+ line = Terrapin::CommandLine.new("echo", ":var", logger: Logger.new(STDOUT))
118
+ line.run(var: "LOL!") # => Logs this with #info -> Command :: echo 'LOL!'
119
+ ```
120
+
121
+ Or log every command:
122
+
123
+ ```ruby
124
+ Terrapin::CommandLine.logger = Logger.new(STDOUT)
125
+ Terrapin::CommandLine.new("date").run # => Logs this -> Command :: date
126
+ ```
127
+
128
+ ## Security
129
+
130
+ Short version: Only pass user-generated data into the `run` method and NOT
131
+ `new`.
132
+
133
+ As shown in examples above, Terrapin will only shell-escape what is passed in as
134
+ interpolations to the `run` method. It WILL NOT escape what is passed in to the
135
+ second argument of `new`. Terrapin assumes that you will not be manually
136
+ passing user-generated data to that argument and will be using it as a template
137
+ for your command line's structure.
138
+
139
+ ## POSIX Spawn
140
+
141
+ You can potentially increase performance by installing [the posix-spawn
142
+ gem](https://rubygems.org/gems/posix-spawn). This gem can keep your
143
+ application's heap from being copied when forking command line
144
+ processes. For applications with large heaps the gain can be
145
+ significant. To include `posix-spawn`, simply add it to your `Gemfile` or,
146
+ if you don't use bundler, install the gem.
147
+
148
+ ## Runners
149
+
150
+ Terrapin will attempt to choose from among 3 different ways of running commands.
151
+ The simplest is using backticks, and is the default in 1.8. In Ruby 1.9, it
152
+ will attempt to use `Process.spawn`. And, as mentioned above, if the
153
+ `posix-spawn` gem is installed, it will attempt to use that. If for some reason
154
+ one of the `.spawn` runners don't work for you, you can override them manually
155
+ by setting a new runner, like so:
156
+
157
+ ```ruby
158
+ Terrapin::CommandLine.runner = Terrapin::CommandLine::BackticksRunner.new
159
+ ```
160
+
161
+ And if you really want to, you can define your own Runner, though I can't
162
+ imagine why you would.
163
+
164
+ ### JRuby issues
165
+
166
+ #### Caveat
167
+
168
+ If you get `Error::ECHILD` errors and are using JRuby, there is a very good
169
+ chance that the error is actually in JRuby. This was brought to our attention
170
+ in https://github.com/thoughtbot/terrapin/issues/24 and probably fixed in
171
+ http://jira.codehaus.org/browse/JRUBY-6162. You *will* want to use the
172
+ `BackticksRunner` if you are unable to update JRuby.
173
+
174
+ #### Spawn warning
175
+
176
+ If you get `unsupported spawn option: out` warning (like in [issue 38](https://github.com/thoughtbot/terrapin/issues/38)),
177
+ try to use `PopenRunner`:
178
+
179
+ ```ruby
180
+ Terrapin::CommandLine.runner = Terrapin::CommandLine::PopenRunner.new
181
+ ```
182
+
183
+ ## Thread Safety
184
+
185
+ Terrapin should be thread safe. As discussed [here, in this climate_control
186
+ thread](https://github.com/thoughtbot/climate_control/pull/11), climate_control,
187
+ which modifies the environment under which commands are run for the
188
+ BackticksRunner and PopenRunner, is thread-safe but not reentrant. Please let us
189
+ know if you find this is ever not the case.
190
+
191
+ ## Feedback
192
+
193
+ *Security* concerns must be privately emailed to
194
+ [security@thoughtbot.com](security@thoughtbot.com).
195
+
196
+ Question? Idea? Problem? Bug? Comment? Concern? Like using question marks?
197
+
198
+ [GitHub Issues For All!](https://github.com/thoughtbot/terrapin/issues)
199
+
200
+ ## Credits
201
+
202
+ Thank you to all [the contributors](https://github.com/thoughtbot/terrapin/graphs/contributors)!
203
+
204
+ ![thoughtbot](http://thoughtbot.com/logo.png)
205
+
206
+ Terrapin is maintained and funded by [thoughtbot, inc](http://thoughtbot.com/community)
207
+
208
+ The names and logos for thoughtbot are trademarks of thoughtbot, inc.
209
+
210
+ ## License
211
+
212
+ Copyright 2011-2014 Jon Yurek and thoughtbot, inc. This is free software, and
213
+ may be redistributed under the terms specified in the
214
+ [LICENSE](https://github.com/thoughtbot/terrapin/blob/master/LICENSE)
215
+ file.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc 'Default: Run specs.'
6
+ task :default => 'spec:unit'
7
+
8
+ namespace :spec do
9
+ desc "Run unit specs"
10
+ RSpec::Core::RakeTask.new('unit') do |t|
11
+ t.pattern = 'spec/terrapin/**/*_spec.rb'
12
+ end
13
+ end
data/lib/terrapin.rb ADDED
@@ -0,0 +1,12 @@
1
+ # coding: UTF-8
2
+
3
+ require 'rbconfig'
4
+ require 'terrapin/os_detector'
5
+ require 'terrapin/command_line'
6
+ require 'terrapin/command_line/output'
7
+ require 'terrapin/command_line/multi_pipe'
8
+ require 'terrapin/command_line/runners'
9
+ require 'terrapin/exceptions'
10
+
11
+ module Terrapin
12
+ end
@@ -0,0 +1,197 @@
1
+ # coding: UTF-8
2
+
3
+ module Terrapin
4
+ class CommandLine
5
+ class << self
6
+ attr_accessor :logger, :runner
7
+
8
+ def path
9
+ @supplemental_path
10
+ end
11
+
12
+ def path=(supplemental_path)
13
+ @supplemental_path = Array(supplemental_path).
14
+ flatten.
15
+ join(OS.path_separator)
16
+ end
17
+
18
+ def environment
19
+ @supplemental_environment ||= {}
20
+ end
21
+
22
+ def runner
23
+ @runner || best_runner
24
+ end
25
+
26
+ def runner_options
27
+ @default_runner_options ||= {}
28
+ end
29
+
30
+ def fake!
31
+ @runner = FakeRunner.new
32
+ end
33
+
34
+ def unfake!
35
+ @runner = nil
36
+ end
37
+
38
+ private
39
+
40
+ def best_runner
41
+ [PosixRunner, ProcessRunner, BackticksRunner].detect do |runner|
42
+ runner.supported?
43
+ end.new
44
+ end
45
+ end
46
+
47
+ @environment = {}
48
+
49
+ attr_reader :exit_status, :runner
50
+
51
+ def initialize(binary, params = "", options = {})
52
+ @binary = binary.dup
53
+ @params = params.dup
54
+ @options = options.dup
55
+ @runner = @options.delete(:runner) || self.class.runner
56
+ @logger = @options.delete(:logger) || self.class.logger
57
+ @swallow_stderr = @options.delete(:swallow_stderr)
58
+ @expected_outcodes = @options.delete(:expected_outcodes) || [0]
59
+ @environment = @options.delete(:environment) || {}
60
+ @runner_options = @options.delete(:runner_options) || {}
61
+ end
62
+
63
+ def command(interpolations = {})
64
+ cmd = [path_prefix, @binary, interpolate(@params, interpolations)]
65
+ cmd << bit_bucket if @swallow_stderr
66
+ cmd.join(" ").strip
67
+ end
68
+
69
+ def run(interpolations = {})
70
+ @exit_status = nil
71
+ begin
72
+ full_command = command(interpolations)
73
+ log("#{colored("Command")} :: #{full_command}")
74
+ @output = execute(full_command)
75
+ rescue Errno::ENOENT => e
76
+ raise Terrapin::CommandNotFoundError, e.message
77
+ ensure
78
+ @exit_status = $?.respond_to?(:exitstatus) ? $?.exitstatus : 0
79
+ end
80
+
81
+ if @exit_status == 127
82
+ raise Terrapin::CommandNotFoundError
83
+ end
84
+
85
+ unless @expected_outcodes.include?(@exit_status)
86
+ message = [
87
+ "Command '#{full_command}' returned #{@exit_status}. Expected #{@expected_outcodes.join(", ")}",
88
+ "Here is the command output: STDOUT:\n", command_output,
89
+ "\nSTDERR:\n", command_error_output
90
+ ].join("\n")
91
+ raise Terrapin::ExitStatusError, message
92
+ end
93
+ command_output
94
+ end
95
+
96
+ def command_output
97
+ output.output
98
+ end
99
+
100
+ def command_error_output
101
+ output.error_output
102
+ end
103
+
104
+ def output
105
+ @output || Output.new
106
+ end
107
+
108
+ private
109
+
110
+ def colored(text, ansi_color = "\e[32m")
111
+ if @logger && @logger.respond_to?(:tty?) && @logger.tty?
112
+ "#{ansi_color}#{text}\e[0m"
113
+ else
114
+ text
115
+ end
116
+ end
117
+
118
+ def log(text)
119
+ if @logger
120
+ @logger.info(text)
121
+ end
122
+ end
123
+
124
+ def path_prefix
125
+ if !self.class.path.nil? && !self.class.path.empty?
126
+ os_path_prefix
127
+ end
128
+ end
129
+
130
+ def os_path_prefix
131
+ if OS.unix?
132
+ unix_path_prefix
133
+ else
134
+ windows_path_prefix
135
+ end
136
+ end
137
+
138
+ def unix_path_prefix
139
+ "PATH=#{self.class.path}#{OS.path_separator}$PATH;"
140
+ end
141
+
142
+ def windows_path_prefix
143
+ "SET PATH=#{self.class.path}#{OS.path_separator}%PATH% &"
144
+ end
145
+
146
+ def execute(command)
147
+ runner.call(command, environment, runner_options)
148
+ end
149
+
150
+ def environment
151
+ self.class.environment.merge(@environment)
152
+ end
153
+
154
+ def runner_options
155
+ self.class.runner_options.merge(@runner_options)
156
+ end
157
+
158
+ def interpolate(pattern, interpolations)
159
+ interpolations = stringify_keys(interpolations)
160
+ pattern.gsub(/:\{?(\w+)\b\}?/) do |match|
161
+ key = match.tr(":{}", "")
162
+ if interpolations.key?(key)
163
+ shell_quote_all_values(interpolations[key])
164
+ else
165
+ match
166
+ end
167
+ end
168
+ end
169
+
170
+ def stringify_keys(hash)
171
+ Hash[hash.map{ |k, v| [k.to_s, v] }]
172
+ end
173
+
174
+ def shell_quote_all_values(values)
175
+ Array(values).map(&method(:shell_quote)).join(" ")
176
+ end
177
+
178
+ def shell_quote(string)
179
+ return "" if string.nil?
180
+ string = string.to_s if string.respond_to? :to_s
181
+
182
+ if OS.unix?
183
+ if string.empty?
184
+ "''"
185
+ else
186
+ string.split("'", -1).map{|m| "'#{m}'" }.join("\\'")
187
+ end
188
+ else
189
+ %{"#{string}"}
190
+ end
191
+ end
192
+
193
+ def bit_bucket
194
+ OS.unix? ? "2>/dev/null" : "2>NUL"
195
+ end
196
+ end
197
+ end