chef 11.12.0.alpha.1 → 11.12.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/lib/chef/api_client/registration.rb +46 -9
  3. data/lib/chef/application.rb +1 -0
  4. data/lib/chef/application/client.rb +25 -24
  5. data/lib/chef/client.rb +34 -0
  6. data/lib/chef/config.rb +11 -0
  7. data/lib/chef/cookbook/chefignore.rb +10 -2
  8. data/lib/chef/cookbook/metadata.rb +31 -3
  9. data/lib/chef/cookbook/synchronizer.rb +2 -2
  10. data/lib/chef/cookbook/syntax_check.rb +4 -4
  11. data/lib/chef/encrypted_data_bag_item.rb +37 -1
  12. data/lib/chef/exceptions.rb +1 -0
  13. data/lib/chef/guard_interpreter/default_guard_interpreter.rb +42 -0
  14. data/lib/chef/guard_interpreter/resource_guard_interpreter.rb +122 -0
  15. data/lib/chef/http.rb +0 -1
  16. data/lib/chef/http/decompressor.rb +7 -4
  17. data/lib/chef/http/simple.rb +5 -0
  18. data/lib/chef/http/validate_content_length.rb +28 -12
  19. data/lib/chef/knife.rb +1 -0
  20. data/lib/chef/knife/client_bulk_delete.rb +48 -9
  21. data/lib/chef/knife/client_delete.rb +4 -4
  22. data/lib/chef/knife/cookbook_bulk_delete.rb +1 -1
  23. data/lib/chef/knife/cookbook_upload.rb +17 -7
  24. data/lib/chef/knife/core/bootstrap_context.rb +1 -1
  25. data/lib/chef/knife/core/ui.rb +42 -5
  26. data/lib/chef/knife/node_run_list_add.rb +31 -2
  27. data/lib/chef/knife/ssh.rb +44 -31
  28. data/lib/chef/knife/ssl_check.rb +213 -0
  29. data/lib/chef/knife/ssl_fetch.rb +145 -0
  30. data/lib/chef/mixin/deep_merge.rb +13 -5
  31. data/lib/chef/mixin/shell_out.rb +9 -3
  32. data/lib/chef/node.rb +23 -4
  33. data/lib/chef/node/immutable_collections.rb +32 -0
  34. data/lib/chef/platform/provider_mapping.rb +21 -18
  35. data/lib/chef/platform/query_helpers.rb +10 -2
  36. data/lib/chef/policy_builder/expand_node_object.rb +3 -6
  37. data/lib/chef/provider/cron.rb +25 -3
  38. data/lib/chef/provider/mount/mount.rb +1 -1
  39. data/lib/chef/provider/package/dpkg.rb +2 -1
  40. data/lib/chef/provider/package/windows.rb +80 -0
  41. data/lib/chef/provider/package/windows/msi.rb +69 -0
  42. data/lib/chef/provider/powershell_script.rb +19 -6
  43. data/lib/chef/provider/service/solaris.rb +11 -7
  44. data/lib/chef/resource.rb +18 -5
  45. data/lib/chef/resource/conditional.rb +20 -7
  46. data/lib/chef/resource/cron.rb +18 -2
  47. data/lib/chef/resource/execute.rb +0 -2
  48. data/lib/chef/resource/powershell_script.rb +23 -1
  49. data/lib/chef/resource/script.rb +25 -0
  50. data/lib/chef/resource/subversion.rb +4 -0
  51. data/lib/chef/resource/windows_package.rb +79 -0
  52. data/lib/chef/resource/windows_script.rb +0 -5
  53. data/lib/chef/resources.rb +1 -0
  54. data/lib/chef/rest.rb +6 -1
  55. data/lib/chef/run_context.rb +22 -2
  56. data/lib/chef/run_context/cookbook_compiler.rb +12 -0
  57. data/lib/chef/util/editor.rb +92 -0
  58. data/lib/chef/util/file_edit.rb +22 -54
  59. data/lib/chef/version.rb +2 -2
  60. data/lib/chef/win32/api/installer.rb +166 -0
  61. data/lib/chef/win32/version.rb +8 -0
  62. data/spec/data/standalone_cookbook/Gemfile +1 -0
  63. data/spec/data/standalone_cookbook/chefignore +9 -0
  64. data/spec/data/standalone_cookbook/recipes/default.rb +3 -0
  65. data/spec/data/standalone_cookbook/vendor/bundle/ruby/2.0.0/gems/multi_json-1.9.0/lib/multi_json.rb +1 -0
  66. data/spec/functional/resource/powershell_spec.rb +262 -1
  67. data/spec/functional/win32/versions_spec.rb +3 -3
  68. data/spec/integration/knife/chefignore_spec.rb +1 -2
  69. data/spec/integration/knife/raw_spec.rb +8 -13
  70. data/spec/integration/knife/redirection_spec.rb +6 -14
  71. data/spec/integration/solo/solo_spec.rb +19 -0
  72. data/spec/support/shared/functional/windows_script.rb +1 -1
  73. data/spec/support/shared/integration/app_server_support.rb +42 -0
  74. data/spec/support/shared/integration/integration_helper.rb +1 -0
  75. data/spec/support/shared/unit/script_resource.rb +38 -0
  76. data/spec/unit/api_client/registration_spec.rb +109 -38
  77. data/spec/unit/application/client_spec.rb +48 -1
  78. data/spec/unit/cookbook/chefignore_spec.rb +10 -0
  79. data/spec/unit/cookbook/metadata_spec.rb +45 -1
  80. data/spec/unit/cookbook/syntax_check_spec.rb +28 -0
  81. data/spec/unit/cookbook_spec.rb +0 -10
  82. data/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb +56 -0
  83. data/spec/unit/http/simple_spec.rb +32 -0
  84. data/spec/unit/http/validate_content_length_spec.rb +187 -0
  85. data/spec/unit/knife/bootstrap_spec.rb +13 -4
  86. data/spec/unit/knife/client_bulk_delete_spec.rb +123 -38
  87. data/spec/unit/knife/client_delete_spec.rb +4 -4
  88. data/spec/unit/knife/cookbook_upload_spec.rb +181 -88
  89. data/spec/unit/knife/core/bootstrap_context_spec.rb +11 -1
  90. data/spec/unit/knife/core/ui_spec.rb +109 -38
  91. data/spec/unit/knife/node_run_list_add_spec.rb +24 -1
  92. data/spec/unit/knife/ssh_spec.rb +17 -6
  93. data/spec/unit/knife/ssl_check_spec.rb +187 -0
  94. data/spec/unit/knife/ssl_fetch_spec.rb +151 -0
  95. data/spec/unit/mixin/deep_merge_spec.rb +17 -0
  96. data/spec/unit/node/immutable_collections_spec.rb +55 -0
  97. data/spec/unit/node_spec.rb +9 -0
  98. data/spec/unit/platform/query_helpers_spec.rb +32 -0
  99. data/spec/unit/platform_spec.rb +193 -175
  100. data/spec/unit/policy_builder/expand_node_object_spec.rb +1 -1
  101. data/spec/unit/provider/cron_spec.rb +175 -1
  102. data/spec/unit/provider/mount/mount_spec.rb +33 -3
  103. data/spec/unit/provider/package/dpkg_spec.rb +4 -0
  104. data/spec/unit/provider/package/windows/msi_spec.rb +60 -0
  105. data/spec/unit/provider/package/windows_spec.rb +80 -0
  106. data/spec/unit/provider/service/macosx_spec.rb +3 -3
  107. data/spec/unit/provider/service/solaris_smf_service_spec.rb +35 -10
  108. data/spec/unit/pure_application_spec.rb +32 -0
  109. data/spec/unit/recipe_spec.rb +4 -0
  110. data/spec/unit/resource/conditional_spec.rb +13 -12
  111. data/spec/unit/resource/cron_spec.rb +7 -2
  112. data/spec/unit/resource/powershell_spec.rb +85 -2
  113. data/spec/unit/resource/subversion_spec.rb +5 -0
  114. data/spec/unit/resource/windows_package_spec.rb +74 -0
  115. data/spec/unit/resource_spec.rb +23 -1
  116. data/spec/unit/rest_spec.rb +15 -0
  117. data/spec/unit/run_context/cookbook_compiler_spec.rb +12 -0
  118. data/spec/unit/run_context_spec.rb +7 -0
  119. data/spec/unit/util/editor_spec.rb +152 -0
  120. data/spec/unit/util/file_edit_spec.rb +37 -1
  121. metadata +41 -30
@@ -43,6 +43,7 @@ class Chef
43
43
  @path = nil
44
44
  @shell = nil
45
45
  @home = nil
46
+ @time = nil
46
47
  @environment = {}
47
48
  end
48
49
 
@@ -121,13 +122,28 @@ class Chef
121
122
  converted_arg = arg
122
123
  end
123
124
  begin
124
- if integerize(arg) > 7 then raise RangeError end
125
+ error_message = "You provided '#{arg}' as a weekday, acceptable values are "
126
+ error_message << Provider::Cron::WEEKDAY_SYMBOLS.map {|sym| ":#{sym.to_s}"}.join(', ')
127
+ error_message << " and a string in crontab format"
128
+ if (arg.is_a?(Symbol) && !Provider::Cron::WEEKDAY_SYMBOLS.include?(arg)) ||
129
+ (!arg.is_a?(Symbol) && integerize(arg) > 7) ||
130
+ (!arg.is_a?(Symbol) && integerize(arg) < 0)
131
+ raise RangeError, error_message
132
+ end
125
133
  rescue ArgumentError
126
134
  end
127
135
  set_or_return(
128
136
  :weekday,
129
137
  converted_arg,
130
- :kind_of => String
138
+ :kind_of => [String, Symbol]
139
+ )
140
+ end
141
+
142
+ def time(arg=nil)
143
+ set_or_return(
144
+ :time,
145
+ arg,
146
+ :equal_to => Chef::Provider::Cron::SPECIAL_TIME_VALUES
131
147
  )
132
148
  end
133
149
 
@@ -125,8 +125,6 @@ class Chef
125
125
  )
126
126
  end
127
127
 
128
-
129
-
130
128
  end
131
129
  end
132
130
  end
@@ -15,17 +15,39 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
-
19
18
  require 'chef/resource/windows_script'
20
19
 
21
20
  class Chef
22
21
  class Resource
23
22
  class PowershellScript < Chef::Resource::WindowsScript
24
23
 
24
+ set_guard_inherited_attributes(:architecture)
25
+
25
26
  def initialize(name, run_context=nil)
26
27
  super(name, run_context, :powershell_script, "powershell.exe")
28
+ @convert_boolean_return = false
29
+ end
30
+
31
+ def convert_boolean_return(arg=nil)
32
+ set_or_return(
33
+ :convert_boolean_return,
34
+ arg,
35
+ :kind_of => [ FalseClass, TrueClass ]
36
+ )
27
37
  end
28
38
 
39
+ protected
40
+
41
+ # Allow callers evaluating guards to request default
42
+ # attribute values. This is needed to allow
43
+ # convert_boolean_return to be true in guard context by default,
44
+ # and false by default otherwise. When this mode becomes the
45
+ # default for this resource, this method can be removed since
46
+ # guard context and recipe resource context will have the
47
+ # same behavior.
48
+ def self.get_default_attributes(opts)
49
+ {:convert_boolean_return => true}
50
+ end
29
51
  end
30
52
  end
31
53
  end
@@ -58,6 +58,31 @@ class Chef
58
58
  )
59
59
  end
60
60
 
61
+ def self.set_guard_inherited_attributes(*inherited_attributes)
62
+ @class_inherited_attributes = inherited_attributes
63
+ end
64
+
65
+ def self.guard_inherited_attributes(*inherited_attributes)
66
+ # Similar to patterns elsewhere, return attributes from this
67
+ # class and superclasses as a form of inheritance
68
+ ancestor_attributes = []
69
+
70
+ if superclass.respond_to?(:guard_inherited_attributes)
71
+ ancestor_attributes = superclass.guard_inherited_attributes
72
+ end
73
+
74
+ ancestor_attributes.concat(@class_inherited_attributes ? @class_inherited_attributes : []).uniq
75
+ end
76
+
77
+ set_guard_inherited_attributes(
78
+ :cwd,
79
+ :environment,
80
+ :group,
81
+ :path,
82
+ :user,
83
+ :umask
84
+ )
85
+
61
86
  end
62
87
  end
63
88
  end
@@ -32,6 +32,10 @@ class Chef
32
32
  allowed_actions << :force_export
33
33
  end
34
34
 
35
+ # Override exception to strip password if any, so it won't appear in logs and different Chef notifications
36
+ def custom_exception_message(e)
37
+ "#{self} (#{defined_at}) had an error: #{e.class.name}: #{svn_password ? e.message.gsub(svn_password, "[hidden_password]") : e.message}"
38
+ end
35
39
  end
36
40
  end
37
41
  end
@@ -0,0 +1,79 @@
1
+ #
2
+ # Author:: Bryan McLellan <btm@loftninjas.org>
3
+ # Copyright:: Copyright (c) 2014 Chef Software, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/resource/package'
20
+ require 'chef/provider/package/windows'
21
+ require 'chef/win32/error' if RUBY_PLATFORM =~ /mswin|mingw|windows/
22
+
23
+ class Chef
24
+ class Resource
25
+ class WindowsPackage < Chef::Resource::Package
26
+
27
+ provides :package, :on_platforms => ["windows"]
28
+
29
+ def initialize(name, run_context=nil)
30
+ super
31
+ @allowed_actions = [ :install, :remove ]
32
+ @provider = Chef::Provider::Package::Windows
33
+ @resource_name = :windows_package
34
+ @source ||= source(@package_name)
35
+
36
+ # Unique to this resource
37
+ @installer_type = nil
38
+ @timeout = 600
39
+ # In the past we accepted return code 127 for an unknown reason and 42 because of a bug
40
+ @returns = [ 0 ]
41
+ end
42
+
43
+ def installer_type(arg=nil)
44
+ set_or_return(
45
+ :installer_type,
46
+ arg,
47
+ :kind_of => [ String ]
48
+ )
49
+ end
50
+
51
+ def timeout(arg=nil)
52
+ set_or_return(
53
+ :timeout,
54
+ arg,
55
+ :kind_of => [ String, Integer ]
56
+ )
57
+ end
58
+
59
+ def returns(arg=nil)
60
+ set_or_return(
61
+ :returns,
62
+ arg,
63
+ :kind_of => [ String, Integer, Array ]
64
+ )
65
+ end
66
+
67
+ def source(arg=nil)
68
+ if arg == nil && self.instance_variable_defined?(:@source) == true
69
+ @source
70
+ else
71
+ raise ArgumentError, "Bad type for WindowsPackage resource, use a String" unless arg.is_a?(String)
72
+ Chef::Log.debug("#{package_name}: sanitizing source path '#{arg}'")
73
+ @source = ::File.absolute_path(arg).gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
@@ -52,11 +52,6 @@ class Chef
52
52
  "cannot execute script with requested architecture '#{desired_architecture.to_s}' on a system with architecture '#{node_windows_architecture(node)}'"
53
53
  end
54
54
  end
55
-
56
- def node
57
- run_context && run_context.node
58
- end
59
-
60
55
  end
61
56
  end
62
57
  end
@@ -69,6 +69,7 @@ require 'chef/resource/template'
69
69
  require 'chef/resource/timestamped_deploy'
70
70
  require 'chef/resource/user'
71
71
  require 'chef/resource/whyrun_safe_ruby_block'
72
+ require 'chef/resource/windows_package'
72
73
  require 'chef/resource/yum_package'
73
74
  require 'chef/resource/lwrp_base'
74
75
  require 'chef/resource/bff_package'
@@ -57,6 +57,7 @@ class Chef
57
57
  # http://localhost:4000, a call to +get_rest+ with 'nodes' will make an
58
58
  # HTTP GET request to http://localhost:4000/nodes
59
59
  def initialize(url, client_name=Chef::Config[:node_name], signing_key_filename=Chef::Config[:client_key], options={})
60
+ options = options.dup
60
61
  options[:client_name] = client_name
61
62
  options[:signing_key_filename] = signing_key_filename
62
63
  super(url, options)
@@ -65,7 +66,6 @@ class Chef
65
66
  @authenticator = Authenticator.new(options)
66
67
  @request_id = RemoteRequestID.new(options)
67
68
 
68
- @middlewares << ValidateContentLength.new(options)
69
69
  @middlewares << JSONInput.new(options)
70
70
  @middlewares << JSONToModelOutput.new(options)
71
71
  @middlewares << CookieManager.new(options)
@@ -73,6 +73,11 @@ class Chef
73
73
  @middlewares << @authenticator
74
74
  @middlewares << @request_id
75
75
 
76
+ # ValidateContentLength should come after Decompressor
77
+ # because the order of middlewares is reversed when handling
78
+ # responses.
79
+ @middlewares << ValidateContentLength.new(options)
80
+
76
81
  end
77
82
 
78
83
  def signing_key_filename
@@ -77,13 +77,15 @@ class Chef
77
77
  @events = events
78
78
 
79
79
  @node.run_context = self
80
+
81
+ @cookbook_compiler = nil
80
82
  end
81
83
 
82
84
  # Triggers the compile phase of the chef run. Implemented by
83
85
  # Chef::RunContext::CookbookCompiler
84
86
  def load(run_list_expansion)
85
- compiler = CookbookCompiler.new(self, run_list_expansion, events)
86
- compiler.compile
87
+ @cookbook_compiler = CookbookCompiler.new(self, run_list_expansion, events)
88
+ @cookbook_compiler.compile
87
89
  end
88
90
 
89
91
  # Adds an immediate notification to the
@@ -141,6 +143,18 @@ class Chef
141
143
  Chef::Log.debug("Loading Recipe #{recipe_name} via include_recipe")
142
144
 
143
145
  cookbook_name, recipe_short_name = Chef::Recipe.parse_recipe_name(recipe_name)
146
+
147
+ if unreachable_cookbook?(cookbook_name) # CHEF-4367
148
+ Chef::Log.warn(<<-ERROR_MESSAGE)
149
+ MissingCookbookDependency:
150
+ Recipe `#{recipe_name}` is not in the run_list, and cookbook '#{cookbook_name}'
151
+ is not a dependency of any cookbook in the run_list. To load this recipe,
152
+ first add a dependency on cookbook '#{cookbook_name}' in the cookbook you're
153
+ including it from in that cookbook's metadata.
154
+ ERROR_MESSAGE
155
+ end
156
+
157
+
144
158
  if loaded_fully_qualified_recipe?(cookbook_name, recipe_short_name)
145
159
  Chef::Log.debug("I am not loading #{recipe_name}, because I have already seen it.")
146
160
  false
@@ -228,6 +242,12 @@ class Chef
228
242
  cookbook.has_cookbook_file_for_node?(node, cb_file_name)
229
243
  end
230
244
 
245
+ # Delegates to CookbookCompiler#unreachable_cookbook?
246
+ # Used to raise an error when attempting to load a recipe belonging to a
247
+ # cookbook that is not in the dependency graph. See also: CHEF-4367
248
+ def unreachable_cookbook?(cookbook_name)
249
+ @cookbook_compiler.unreachable_cookbook?(cookbook_name)
250
+ end
231
251
 
232
252
  private
233
253
 
@@ -16,6 +16,7 @@
16
16
  # limitations under the License.
17
17
  #
18
18
 
19
+ require 'set'
19
20
  require 'chef/log'
20
21
  require 'chef/recipe'
21
22
  require 'chef/resource/lwrp_base'
@@ -149,6 +150,17 @@ class Chef
149
150
  @events.recipe_load_complete
150
151
  end
151
152
 
153
+ # Whether or not a cookbook is reachable from the set of cookbook given
154
+ # by the run_list plus those cookbooks' dependencies.
155
+ def unreachable_cookbook?(cookbook_name)
156
+ !reachable_cookbooks.include?(cookbook_name)
157
+ end
158
+
159
+ # All cookbooks in the dependency graph, returned as a Set.
160
+ def reachable_cookbooks
161
+ @reachable_cookbooks ||= Set.new(cookbook_order)
162
+ end
163
+
152
164
  private
153
165
 
154
166
  def load_attributes_from_cookbook(cookbook_name)
@@ -0,0 +1,92 @@
1
+ #
2
+ # Author:: Chris Bandy (<bandy.chris@gmail.com>)
3
+ # Copyright:: Copyright (c) 2014 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ class Chef
20
+ class Util
21
+ class Editor
22
+ attr_reader :lines
23
+
24
+ def initialize(lines)
25
+ @lines = lines.to_a.clone
26
+ end
27
+
28
+ def append_line_after(search, line_to_append)
29
+ lines = []
30
+
31
+ @lines.each do |line|
32
+ lines << line
33
+ lines << line_to_append if line.match(search)
34
+ end
35
+
36
+ (lines.length - @lines.length).tap { @lines = lines }
37
+ end
38
+
39
+ def append_line_if_missing(search, line_to_append)
40
+ count = 0
41
+
42
+ unless @lines.find { |line| line.match(search) }
43
+ count = 1
44
+ @lines << line_to_append
45
+ end
46
+
47
+ count
48
+ end
49
+
50
+ def remove_lines(search)
51
+ count = 0
52
+
53
+ @lines.delete_if do |line|
54
+ count += 1 if line.match(search)
55
+ end
56
+
57
+ count
58
+ end
59
+
60
+ def replace(search, replace)
61
+ count = 0
62
+
63
+ @lines.map! do |line|
64
+ if line.match(search)
65
+ count += 1
66
+ line.gsub!(search, replace)
67
+ else
68
+ line
69
+ end
70
+ end
71
+
72
+ count
73
+ end
74
+
75
+ def replace_lines(search, replace)
76
+ count = 0
77
+
78
+ @lines.map! do |line|
79
+ if line.match(search)
80
+ count += 1
81
+ replace
82
+ else
83
+ line
84
+ end
85
+ end
86
+
87
+ count
88
+ end
89
+ end
90
+ end
91
+ end
92
+
@@ -15,8 +15,8 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
 
18
+ require 'chef/util/editor'
18
19
  require 'fileutils'
19
- require 'tempfile'
20
20
 
21
21
  class Chef
22
22
  class Util
@@ -24,108 +24,76 @@ class Chef
24
24
 
25
25
  private
26
26
 
27
- attr_accessor :original_pathname, :contents, :file_edited
27
+ attr_reader :editor, :original_pathname
28
28
 
29
29
  public
30
30
 
31
31
  def initialize(filepath)
32
+ raise ArgumentError, "File '#{filepath}' does not exist" unless File.exist?(filepath)
33
+ @editor = Editor.new(File.open(filepath, &:readlines))
32
34
  @original_pathname = filepath
33
35
  @file_edited = false
36
+ end
34
37
 
35
- raise ArgumentError, "File doesn't exist" unless File.exist? @original_pathname
36
- @contents = File.open(@original_pathname) { |f| f.readlines }
38
+ # return if file has been edited
39
+ def file_edited?
40
+ @file_edited
37
41
  end
38
42
 
39
43
  #search the file line by line and match each line with the given regex
40
44
  #if matched, replace the whole line with newline.
41
45
  def search_file_replace_line(regex, newline)
42
- search_match(regex, newline, 'r', 1)
46
+ @changes = (editor.replace_lines(regex, newline) > 0) || @changes
43
47
  end
44
48
 
45
49
  #search the file line by line and match each line with the given regex
46
50
  #if matched, replace the match (all occurances) with the replace parameter
47
51
  def search_file_replace(regex, replace)
48
- search_match(regex, replace, 'r', 2)
52
+ @changes = (editor.replace(regex, replace) > 0) || @changes
49
53
  end
50
54
 
51
55
  #search the file line by line and match each line with the given regex
52
56
  #if matched, delete the line
53
57
  def search_file_delete_line(regex)
54
- search_match(regex, " ", 'd', 1)
58
+ @changes = (editor.remove_lines(regex) > 0) || @changes
55
59
  end
56
60
 
57
61
  #search the file line by line and match each line with the given regex
58
62
  #if matched, delete the match (all occurances) from the line
59
63
  def search_file_delete(regex)
60
- search_match(regex, " ", 'd', 2)
64
+ search_file_replace(regex, '')
61
65
  end
62
66
 
63
67
  #search the file line by line and match each line with the given regex
64
68
  #if matched, insert newline after each matching line
65
69
  def insert_line_after_match(regex, newline)
66
- search_match(regex, newline, 'i', 1)
70
+ @changes = (editor.append_line_after(regex, newline) > 0) || @changes
67
71
  end
68
72
 
69
73
  #search the file line by line and match each line with the given regex
70
74
  #if not matched, insert newline at the end of the file
71
75
  def insert_line_if_no_match(regex, newline)
72
- search_match(regex, newline, 'i', 2)
76
+ @changes = (editor.append_line_if_missing(regex, newline) > 0) || @changes
77
+ end
78
+
79
+ def unwritten_changes?
80
+ !!@changes
73
81
  end
74
82
 
75
83
  #Make a copy of old_file and write new file out (only if file changed)
76
84
  def write_file
77
-
78
- # file_edited is false when there was no match in the whole file and thus no contents have changed.
79
- if file_edited
85
+ if @changes
80
86
  backup_pathname = original_pathname + ".old"
81
87
  FileUtils.cp(original_pathname, backup_pathname, :preserve => true)
82
88
  File.open(original_pathname, "w") do |newfile|
83
- contents.each do |line|
89
+ editor.lines.each do |line|
84
90
  newfile.puts(line)
85
91
  end
86
92
  newfile.flush
87
93
  end
94
+ @file_edited = true
88
95
  end
89
- self.file_edited = false
90
- end
91
-
92
- private
93
-
94
- #helper method to do the match, replace, delete, and insert operations
95
- #command is the switch of delete, replace, and insert ('d', 'r', 'i')
96
- #method is to control operation on whole line or only the match (1 for line, 2 for match)
97
- def search_match(regex, replace, command, method)
98
-
99
- #convert regex to a Regexp object (if not already is one) and store it in exp.
100
- exp = Regexp.new(regex)
101
-
102
- #loop through contents and do the appropriate operation depending on 'command' and 'method'
103
- new_contents = []
104
-
105
- contents.each do |line|
106
- if line.match(exp)
107
- self.file_edited = true
108
- case
109
- when command == 'r'
110
- new_contents << ((method == 1) ? replace : line.gsub!(exp, replace))
111
- when command == 'd'
112
- if method == 2
113
- new_contents << line.gsub!(exp, "")
114
- end
115
- when command == 'i'
116
- new_contents << line
117
- new_contents << replace unless method == 2
118
- end
119
- else
120
- new_contents << line
121
- end
122
- end
123
- if command == 'i' && method == 2 && ! file_edited
124
- new_contents << replace
125
- self.file_edited = true
126
- end
127
-
128
- self.contents = new_contents
96
+ @changes = false
129
97
  end
130
98
  end
131
99
  end