berkshelf 3.1.5 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/berkshelf.gemspec +6 -5
- data/features/commands/search.feature +2 -2
- data/features/commands/vendor.feature +6 -2
- data/features/commands/verify.feature +29 -0
- data/features/config.feature +13 -48
- data/features/step_definitions/filesystem_steps.rb +2 -2
- data/features/step_definitions/gem_steps.rb +3 -1
- data/features/step_definitions/utility_steps.rb +2 -2
- data/generator_files/Vagrantfile.erb +30 -30
- data/generator_files/metadata.rb.erb +0 -1
- data/lib/berkshelf.rb +5 -2
- data/lib/berkshelf/berksfile.rb +41 -41
- data/lib/berkshelf/cli.rb +11 -1
- data/lib/berkshelf/community_rest.rb +1 -0
- data/lib/berkshelf/config.rb +18 -4
- data/lib/berkshelf/cookbook_store.rb +1 -1
- data/lib/berkshelf/downloader.rb +4 -0
- data/lib/berkshelf/errors.rb +0 -1
- data/lib/berkshelf/file_syncer.rb +134 -0
- data/lib/berkshelf/locations/base.rb +6 -1
- data/lib/berkshelf/locations/git.rb +2 -2
- data/lib/berkshelf/lockfile.rb +14 -2
- data/lib/berkshelf/uploader.rb +10 -17
- data/lib/berkshelf/validator.rb +37 -0
- data/lib/berkshelf/version.rb +1 -1
- data/lib/berkshelf/visualizer.rb +13 -6
- data/spec/spec_helper.rb +1 -1
- data/spec/support/kitchen.rb +3 -1
- data/spec/support/matchers/file_system_matchers.rb +1 -1
- data/spec/support/matchers/filepath_matchers.rb +38 -2
- data/spec/support/shared_examples/formatter.rb +7 -7
- data/spec/unit/berkshelf/berksfile_spec.rb +51 -21
- data/spec/unit/berkshelf/cached_cookbook_spec.rb +5 -5
- data/spec/unit/berkshelf/cli_spec.rb +1 -1
- data/spec/unit/berkshelf/community_rest_spec.rb +12 -12
- data/spec/unit/berkshelf/config_spec.rb +4 -4
- data/spec/unit/berkshelf/cookbook_generator_spec.rb +2 -2
- data/spec/unit/berkshelf/cookbook_store_spec.rb +6 -6
- data/spec/unit/berkshelf/core_ext/file_utils_spec.rb +3 -3
- data/spec/unit/berkshelf/core_ext/pathname_spec.rb +23 -6
- data/spec/unit/berkshelf/dependency_spec.rb +4 -4
- data/spec/unit/berkshelf/downloader_spec.rb +5 -1
- data/spec/unit/berkshelf/errors_spec.rb +1 -1
- data/spec/unit/berkshelf/file_syncer_spec.rb +206 -0
- data/spec/unit/berkshelf/init_generator_spec.rb +19 -22
- data/spec/unit/berkshelf/installer_spec.rb +6 -6
- data/spec/unit/berkshelf/locations/base_spec.rb +17 -8
- data/spec/unit/berkshelf/locations/git_spec.rb +34 -34
- data/spec/unit/berkshelf/locations/path_spec.rb +3 -3
- data/spec/unit/berkshelf/lockfile_parser_spec.rb +1 -1
- data/spec/unit/berkshelf/lockfile_spec.rb +50 -36
- data/spec/unit/berkshelf/packager_spec.rb +6 -4
- data/spec/unit/berkshelf/resolver/graph_spec.rb +3 -3
- data/spec/unit/berkshelf/resolver_spec.rb +3 -3
- data/spec/unit/berkshelf/shell_spec.rb +30 -24
- data/spec/unit/berkshelf/uploader_spec.rb +10 -36
- data/spec/unit/berkshelf/validator_spec.rb +30 -0
- data/spec/unit/berkshelf/visualizer_spec.rb +17 -2
- metadata +34 -15
- data/lib/berkshelf/mixin/dsl_eval.rb +0 -58
- data/spec/unit/berkshelf/mixin/dsl_eval_spec.rb +0 -55
data/lib/berkshelf/cli.rb
CHANGED
@@ -134,7 +134,7 @@ module Berkshelf
|
|
134
134
|
def install
|
135
135
|
if options[:path]
|
136
136
|
# TODO: Remove in Berkshelf 4.0
|
137
|
-
Berkshelf.formatter.deprecation "`berks install --path [PATH
|
137
|
+
Berkshelf.formatter.deprecation "`berks install --path [PATH]` has been replaced by `berks vendor`."
|
138
138
|
Berkshelf.formatter.deprecation "Re-run your command as `berks vendor [PATH]` or see `berks help vendor`."
|
139
139
|
exit(1)
|
140
140
|
end
|
@@ -387,6 +387,16 @@ module Berkshelf
|
|
387
387
|
berksfile.vendor(path)
|
388
388
|
end
|
389
389
|
|
390
|
+
method_option :berksfile,
|
391
|
+
type: :string,
|
392
|
+
default: nil
|
393
|
+
desc "verify", "Perform a quick validation on the contents of your resolved cookbooks"
|
394
|
+
def verify
|
395
|
+
berksfile = Berksfile.from_options(options)
|
396
|
+
berksfile.verify
|
397
|
+
Berkshelf.formatter.msg "Verified."
|
398
|
+
end
|
399
|
+
|
390
400
|
method_option :berksfile,
|
391
401
|
type: :string,
|
392
402
|
default: nil,
|
data/lib/berkshelf/config.rb
CHANGED
@@ -60,7 +60,19 @@ module Berkshelf
|
|
60
60
|
# @param [Hash] options
|
61
61
|
# @see {Buff::Config::JSON}
|
62
62
|
def initialize(path = self.class.path, options = {})
|
63
|
-
super(path, options)
|
63
|
+
super(path, options).tap do
|
64
|
+
# Deprecation
|
65
|
+
if !self.vagrant.omnibus.enabled.nil?
|
66
|
+
Berkshelf.ui.warn "`vagrant.omnibus.enabled' is deprecated and " \
|
67
|
+
"will be removed in a future release. Please remove the " \
|
68
|
+
"`enabled' attribute from your Berkshelf config."
|
69
|
+
end
|
70
|
+
if !self.vagrant.vm.box_url.nil?
|
71
|
+
Berkshelf.ui.warn "`vagrant.vm.box_url' is deprecated and " \
|
72
|
+
"will be removed in a future release. Please remove the " \
|
73
|
+
"`box_url' attribute from your Berkshelf config."
|
74
|
+
end
|
75
|
+
end
|
64
76
|
end
|
65
77
|
|
66
78
|
attribute 'chef.chef_server_url',
|
@@ -97,19 +109,21 @@ module Berkshelf
|
|
97
109
|
type: String,
|
98
110
|
default: 'chef/ubuntu-14.04',
|
99
111
|
required: true
|
112
|
+
# @todo Deprecated, remove?
|
100
113
|
attribute 'vagrant.vm.box_url',
|
101
114
|
type: String,
|
102
|
-
default:
|
103
|
-
required: true
|
115
|
+
default: nil
|
104
116
|
attribute 'vagrant.vm.forward_port',
|
105
117
|
type: Hash,
|
106
118
|
default: Hash.new
|
107
119
|
attribute 'vagrant.vm.provision',
|
108
120
|
type: String,
|
109
121
|
default: 'chef_solo'
|
122
|
+
# @todo Deprecated, remove. There's a really weird tri-state here where
|
123
|
+
# nil is used to represent an unset value, just FYI
|
110
124
|
attribute 'vagrant.omnibus.enabled',
|
111
125
|
type: Buff::Boolean,
|
112
|
-
default:
|
126
|
+
default: nil
|
113
127
|
attribute 'vagrant.omnibus.version',
|
114
128
|
type: String,
|
115
129
|
default: 'latest'
|
@@ -121,7 +121,7 @@ module Berkshelf
|
|
121
121
|
msg << "of a name attribute as a bug.\n\n"
|
122
122
|
msg << "You can remove each cookbook in #{skipped_cookbooks} from the Berkshelf shelf "
|
123
123
|
msg << "by using the `berks shelf uninstall` command:\n\n"
|
124
|
-
msg << " $
|
124
|
+
msg << " $ berks shelf uninstall <name>"
|
125
125
|
Berkshelf.log.warn msg
|
126
126
|
end
|
127
127
|
|
data/lib/berkshelf/downloader.rb
CHANGED
@@ -133,6 +133,10 @@ module Berkshelf
|
|
133
133
|
end.first
|
134
134
|
|
135
135
|
(unpack_dir + cookbook_directory).to_s
|
136
|
+
when :file_store
|
137
|
+
tmp_dir = Dir.mktmpdir
|
138
|
+
FileUtils.cp_r(remote_cookbook.location_path, tmp_dir)
|
139
|
+
File.join(tmp_dir, name)
|
136
140
|
else
|
137
141
|
raise RuntimeError, "unknown location type #{remote_cookbook.location_type}"
|
138
142
|
end
|
data/lib/berkshelf/errors.rb
CHANGED
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Berkshelf
|
4
|
+
module FileSyncer
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# Files to be ignored during a directory globbing
|
8
|
+
IGNORED_FILES = %w(. ..).freeze
|
9
|
+
|
10
|
+
#
|
11
|
+
# Glob across the given pattern, accounting for dotfiles, removing Ruby's
|
12
|
+
# dumb idea to include +'.'+ and +'..'+ as entries.
|
13
|
+
#
|
14
|
+
# @param [String] pattern
|
15
|
+
# the path or glob pattern to get all files from
|
16
|
+
#
|
17
|
+
# @return [Array<String>]
|
18
|
+
# the list of all files
|
19
|
+
#
|
20
|
+
def glob(pattern)
|
21
|
+
Dir.glob(pattern, File::FNM_DOTMATCH).sort.reject do |file|
|
22
|
+
basename = File.basename(file)
|
23
|
+
IGNORED_FILES.include?(basename)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Copy the files from +source+ to +destination+, while removing any files
|
29
|
+
# in +destination+ that are not present in +source+.
|
30
|
+
#
|
31
|
+
# The method accepts an optional +:exclude+ parameter to ignore files and
|
32
|
+
# folders that match the given pattern(s). Note the exclude pattern behaves
|
33
|
+
# on paths relative to the given source. If you want to exclude a nested
|
34
|
+
# directory, you will need to use something like +**/directory+.
|
35
|
+
#
|
36
|
+
# @raise ArgumentError
|
37
|
+
# if the +source+ parameter is not a directory
|
38
|
+
#
|
39
|
+
# @param [String] source
|
40
|
+
# the path on disk to sync from
|
41
|
+
# @param [String] destination
|
42
|
+
# the path on disk to sync to
|
43
|
+
#
|
44
|
+
# @option options [String, Array<String>] :exclude
|
45
|
+
# a file, folder, or globbing pattern of files to ignore when syncing
|
46
|
+
#
|
47
|
+
# @return [true]
|
48
|
+
#
|
49
|
+
def sync(source, destination, options = {})
|
50
|
+
unless File.directory?(source)
|
51
|
+
raise ArgumentError, "`source' must be a directory, but was a " \
|
52
|
+
"`#{File.ftype(source)}'! If you just want to sync a file, use " \
|
53
|
+
"the `copy' method instead."
|
54
|
+
end
|
55
|
+
|
56
|
+
# Reject any files that match the excludes pattern
|
57
|
+
excludes = Array(options[:exclude]).map do |exclude|
|
58
|
+
[exclude, "#{exclude}/*"]
|
59
|
+
end.flatten
|
60
|
+
|
61
|
+
source_files = glob(File.join(source, '**/*'))
|
62
|
+
source_files = source_files.reject do |source_file|
|
63
|
+
basename = relative_path_for(source_file, source)
|
64
|
+
excludes.any? { |exclude| File.fnmatch?(exclude, basename, File::FNM_DOTMATCH) }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Ensure the destination directory exists
|
68
|
+
FileUtils.mkdir_p(destination) unless File.directory?(destination)
|
69
|
+
|
70
|
+
# Copy over the filtered source files
|
71
|
+
source_files.each do |source_file|
|
72
|
+
relative_path = relative_path_for(source_file, source)
|
73
|
+
|
74
|
+
# Create the parent directory
|
75
|
+
parent = File.join(destination, File.dirname(relative_path))
|
76
|
+
FileUtils.mkdir_p(parent) unless File.directory?(parent)
|
77
|
+
|
78
|
+
case File.ftype(source_file).to_sym
|
79
|
+
when :directory
|
80
|
+
FileUtils.mkdir_p("#{destination}/#{relative_path}")
|
81
|
+
when :link
|
82
|
+
target = File.readlink(source_file)
|
83
|
+
|
84
|
+
Dir.chdir(destination) do
|
85
|
+
FileUtils.ln_sf(target, "#{destination}/#{relative_path}")
|
86
|
+
end
|
87
|
+
when :file
|
88
|
+
FileUtils.cp(source_file, "#{destination}/#{relative_path}")
|
89
|
+
else
|
90
|
+
type = File.ftype(source_file)
|
91
|
+
raise RuntimeError, "Unknown file type: `#{type}' at " \
|
92
|
+
"`#{source_file}'. Failed to sync `#{source_file}' to " \
|
93
|
+
"`#{destination}/#{relative_path}'!"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Remove any files in the destination that are not in the source files
|
98
|
+
destination_files = glob("#{destination}/**/*")
|
99
|
+
|
100
|
+
# Calculate the relative paths of files so we can compare to the
|
101
|
+
# source.
|
102
|
+
relative_source_files = source_files.map do |file|
|
103
|
+
relative_path_for(file, source)
|
104
|
+
end
|
105
|
+
relative_destination_files = destination_files.map do |file|
|
106
|
+
relative_path_for(file, destination)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Remove any extra files that are present in the destination, but are
|
110
|
+
# not in the source list
|
111
|
+
extra_files = relative_destination_files - relative_source_files
|
112
|
+
extra_files.each do |file|
|
113
|
+
FileUtils.rm_rf(File.join(destination, file))
|
114
|
+
end
|
115
|
+
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
#
|
121
|
+
# The relative path of the given +path+ to the +parent+.
|
122
|
+
#
|
123
|
+
# @param [String] path
|
124
|
+
# the path to get relative with
|
125
|
+
# @param [String] parent
|
126
|
+
# the parent where the path is contained (hopefully)
|
127
|
+
#
|
128
|
+
# @return [String]
|
129
|
+
#
|
130
|
+
def relative_path_for(path, parent)
|
131
|
+
Pathname.new(path).relative_path_from(Pathname.new(parent)).to_s
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -59,7 +59,12 @@ module Berkshelf
|
|
59
59
|
raise NotACookbook.new(path)
|
60
60
|
end
|
61
61
|
|
62
|
-
|
62
|
+
begin
|
63
|
+
cookbook = CachedCookbook.from_path(path)
|
64
|
+
rescue Ridley::Errors::RidleyError => e
|
65
|
+
raise InternalError, "The following error occurred while reading the " \
|
66
|
+
"cookbook `#{dependency.name}':\n#{e.class}: #{e.message}"
|
67
|
+
end
|
63
68
|
|
64
69
|
unless @dependency.version_constraint.satisfies?(cookbook.version)
|
65
70
|
raise CookbookValidationFailure.new(dependency, cookbook)
|
@@ -27,7 +27,7 @@ module Berkshelf
|
|
27
27
|
|
28
28
|
# @see BaseLoation#installed?
|
29
29
|
def installed?
|
30
|
-
revision && install_path.exist?
|
30
|
+
!!(revision && install_path.exist?)
|
31
31
|
end
|
32
32
|
|
33
33
|
# Install this git cookbook into the cookbook store. This method leverages
|
@@ -71,7 +71,7 @@ module Berkshelf
|
|
71
71
|
FileUtils.cp_r(scratch_path, install_path)
|
72
72
|
|
73
73
|
# Remove the git history
|
74
|
-
FileUtils.rm_rf(File.join(install_path, '.git'))
|
74
|
+
FileUtils.rm_rf(File.join(install_path, '.git'))
|
75
75
|
|
76
76
|
install_path.chmod(0777 & ~File.umask)
|
77
77
|
ensure
|
data/lib/berkshelf/lockfile.rb
CHANGED
@@ -16,7 +16,10 @@ module Berkshelf
|
|
16
16
|
# @param [Berkshelf::Berksfile] berksfile
|
17
17
|
# the Berksfile associated with the Lockfile
|
18
18
|
def from_berksfile(berksfile)
|
19
|
-
|
19
|
+
parent = File.expand_path(File.dirname(berksfile.filepath))
|
20
|
+
lockfile_name = "#{File.basename(berksfile.filepath)}.lock"
|
21
|
+
|
22
|
+
filepath = File.join(parent, lockfile_name)
|
20
23
|
new(berksfile: berksfile, filepath: filepath)
|
21
24
|
end
|
22
25
|
end
|
@@ -36,7 +39,7 @@ module Berkshelf
|
|
36
39
|
# the Berksfile for this Lockfile
|
37
40
|
attr_reader :berksfile
|
38
41
|
|
39
|
-
# @return [
|
42
|
+
# @return [Lockfile::Graph]
|
40
43
|
# the dependency graph
|
41
44
|
attr_reader :graph
|
42
45
|
|
@@ -213,6 +216,11 @@ module Berkshelf
|
|
213
216
|
end
|
214
217
|
end
|
215
218
|
|
219
|
+
# @return [Array<CachedCookbook>]
|
220
|
+
def cached
|
221
|
+
graph.locks.values.collect { |dependency| dependency.cached_cookbook }
|
222
|
+
end
|
223
|
+
|
216
224
|
# The list of dependencies constrained in this lockfile.
|
217
225
|
#
|
218
226
|
# @return [Array<Berkshelf::Dependency>]
|
@@ -257,6 +265,10 @@ module Berkshelf
|
|
257
265
|
@dependencies[Dependency.name(dependency)] = dependency
|
258
266
|
end
|
259
267
|
|
268
|
+
def locks
|
269
|
+
graph.locks
|
270
|
+
end
|
271
|
+
|
260
272
|
# Retrieve information about a given cookbook that is in this lockfile.
|
261
273
|
#
|
262
274
|
# @raise [DependencyNotFound]
|
data/lib/berkshelf/uploader.rb
CHANGED
@@ -31,7 +31,7 @@ module Berkshelf
|
|
31
31
|
end
|
32
32
|
|
33
33
|
# Perform all validations first to prevent partially uploaded cookbooks
|
34
|
-
|
34
|
+
Validator.validate_files(cookbooks)
|
35
35
|
|
36
36
|
upload(cookbooks)
|
37
37
|
cookbooks
|
@@ -100,23 +100,16 @@ module Berkshelf
|
|
100
100
|
cookbooks[dependency] ||= lockfile.retrieve(dependency)
|
101
101
|
end
|
102
102
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
path = cookbook.path.to_s
|
113
|
-
|
114
|
-
files = Dir.glob(File.join(path, '**', '*.rb')).select do |f|
|
115
|
-
parent = Pathname.new(path).dirname.to_s
|
116
|
-
f.gsub(parent, '') =~ /[[:space:]]/
|
103
|
+
# This is a temporary change and will be removed in a future release. We should
|
104
|
+
# add the ability to define a custom uploader which would allow the authors of Chef-Guard
|
105
|
+
# to define their upload strategy instead of using Ridley.
|
106
|
+
#
|
107
|
+
# See https://github.com/berkshelf/berkshelf/pull/1316 for details.
|
108
|
+
if Berkshelf.chef_config.knife[:chef_guard] == true
|
109
|
+
cookbooks.values
|
110
|
+
else
|
111
|
+
cookbooks.values.sort
|
117
112
|
end
|
118
|
-
|
119
|
-
raise InvalidCookbookFiles.new(cookbook, files) unless files.empty?
|
120
113
|
end
|
121
114
|
end
|
122
115
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Berkshelf
|
2
|
+
module Validator
|
3
|
+
class << self
|
4
|
+
# Perform a complete cookbook validation checking:
|
5
|
+
# * File names for inappropriate characters
|
6
|
+
# * Invalid Ruby syntax
|
7
|
+
# * Invalid ERB templates
|
8
|
+
#
|
9
|
+
# @param [Array<CachedCookbook>, CachedCookbook] cookbooks
|
10
|
+
# the Cookbook(s) to validate
|
11
|
+
def validate(cookbooks)
|
12
|
+
Array(cookbooks).each do |cookbook|
|
13
|
+
validate_files(cookbook)
|
14
|
+
cookbook.validate
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Validate that the given cookbook does not have "bad" files. Currently
|
19
|
+
# this means including spaces in filenames (such as recipes)
|
20
|
+
#
|
21
|
+
# @param [Array<CachedCookbook>, CachedCookbook] cookbooks
|
22
|
+
# the Cookbook(s) to validate
|
23
|
+
def validate_files(cookbooks)
|
24
|
+
Array(cookbooks).each do |cookbook|
|
25
|
+
path = cookbook.path.to_s
|
26
|
+
|
27
|
+
files = Dir.glob(File.join(path, '**', '*.rb')).select do |f|
|
28
|
+
parent = Pathname.new(path).dirname.to_s
|
29
|
+
f.gsub(parent, '') =~ /[[:space:]]/
|
30
|
+
end
|
31
|
+
|
32
|
+
raise InvalidCookbookFiles.new(cookbook, files) unless files.empty?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/berkshelf/version.rb
CHANGED
data/lib/berkshelf/visualizer.rb
CHANGED
@@ -11,7 +11,7 @@ module Berkshelf
|
|
11
11
|
instance.node(item.name)
|
12
12
|
|
13
13
|
item.dependencies.each do |name, version|
|
14
|
-
instance.edge(item.name, name)
|
14
|
+
instance.edge(item.name, name, version)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -37,11 +37,11 @@ module Berkshelf
|
|
37
37
|
nodes.each(&block)
|
38
38
|
end
|
39
39
|
|
40
|
-
def edge(a, b)
|
40
|
+
def edge(a, b, version)
|
41
41
|
node(a)
|
42
42
|
node(b)
|
43
43
|
|
44
|
-
@nodes[a].add(b)
|
44
|
+
@nodes[a].add(b => version)
|
45
45
|
end
|
46
46
|
|
47
47
|
def adjacencies(object)
|
@@ -61,7 +61,14 @@ module Berkshelf
|
|
61
61
|
|
62
62
|
nodes.each do |node|
|
63
63
|
adjacencies(node).each do |edge|
|
64
|
-
|
64
|
+
edge.each do |name, version|
|
65
|
+
if version == Semverse::DEFAULT_CONSTRAINT
|
66
|
+
label = ""
|
67
|
+
else
|
68
|
+
label = " #{version}"
|
69
|
+
end
|
70
|
+
out << %| "#{node}" -> "#{name}" [ fontsize = 10, label = "#{label}" ]\n|
|
71
|
+
end
|
65
72
|
end
|
66
73
|
end
|
67
74
|
|
@@ -81,11 +88,11 @@ module Berkshelf
|
|
81
88
|
tempfile.write(to_dot)
|
82
89
|
tempfile.rewind
|
83
90
|
|
84
|
-
unless Berkshelf.which('dot')
|
91
|
+
unless Berkshelf.which('dot') || Berkshelf.which('dot.exe')
|
85
92
|
raise GraphvizNotInstalled.new
|
86
93
|
end
|
87
94
|
|
88
|
-
command =
|
95
|
+
command = %|dot -T png #{tempfile.path} -o "#{outfile}"|
|
89
96
|
response = shell_out(command)
|
90
97
|
|
91
98
|
unless response.success?
|