knife-essentials 0.1

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.
Files changed (34) hide show
  1. data/LICENSE +201 -0
  2. data/README.rdoc +114 -0
  3. data/Rakefile +24 -0
  4. data/lib/chef/knife/diff.rb +28 -0
  5. data/lib/chef/knife/list.rb +107 -0
  6. data/lib/chef/knife/show.rb +30 -0
  7. data/lib/chef_fs.rb +9 -0
  8. data/lib/chef_fs/command_line.rb +91 -0
  9. data/lib/chef_fs/diff.rb +158 -0
  10. data/lib/chef_fs/file_pattern.rb +292 -0
  11. data/lib/chef_fs/file_system.rb +69 -0
  12. data/lib/chef_fs/file_system/base_fs_dir.rb +23 -0
  13. data/lib/chef_fs/file_system/base_fs_object.rb +52 -0
  14. data/lib/chef_fs/file_system/chef_server_root_dir.rb +48 -0
  15. data/lib/chef_fs/file_system/cookbook_dir.rb +80 -0
  16. data/lib/chef_fs/file_system/cookbook_file.rb +32 -0
  17. data/lib/chef_fs/file_system/cookbook_subdir.rb +25 -0
  18. data/lib/chef_fs/file_system/cookbooks_dir.rb +21 -0
  19. data/lib/chef_fs/file_system/data_bag_dir.rb +22 -0
  20. data/lib/chef_fs/file_system/data_bags_dir.rb +23 -0
  21. data/lib/chef_fs/file_system/file_system_entry.rb +36 -0
  22. data/lib/chef_fs/file_system/file_system_root_dir.rb +11 -0
  23. data/lib/chef_fs/file_system/nonexistent_fs_object.rb +23 -0
  24. data/lib/chef_fs/file_system/not_found_exception.rb +12 -0
  25. data/lib/chef_fs/file_system/rest_list_dir.rb +42 -0
  26. data/lib/chef_fs/file_system/rest_list_entry.rb +72 -0
  27. data/lib/chef_fs/knife.rb +47 -0
  28. data/lib/chef_fs/path_utils.rb +43 -0
  29. data/lib/chef_fs/version.rb +4 -0
  30. data/spec/chef_fs/diff_spec.rb +263 -0
  31. data/spec/chef_fs/file_pattern_spec.rb +507 -0
  32. data/spec/chef_fs/file_system_spec.rb +117 -0
  33. data/spec/support/file_system_support.rb +108 -0
  34. metadata +79 -0
@@ -0,0 +1,30 @@
1
+ require 'chef_fs/knife'
2
+ require 'chef_fs/file_system'
3
+
4
+ class Chef
5
+ class Knife
6
+ class Show < ChefFS::Knife
7
+ banner "show [PATTERN1 ... PATTERNn]"
8
+
9
+ def run
10
+ # Get the matches (recursively)
11
+ pattern_args.each do |pattern|
12
+ ChefFS::FileSystem.list(chef_fs, pattern) do |result|
13
+ if result.dir?
14
+ STDERR.puts "#{format_path(result.path)}: is a directory" if pattern.exact_path
15
+ else
16
+ begin
17
+ value = result.read
18
+ puts "#{format_path(result.path)}:"
19
+ output(format_for_display(result.read))
20
+ rescue ChefFS::FileSystem::NotFoundException
21
+ STDERR.puts "#{format_path(result.path)}: No such file or directory" if pattern.exact_path
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
data/lib/chef_fs.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'chef_fs/file_system/chef_server_root_dir'
2
+ require 'chef/config'
3
+ require 'chef/rest'
4
+
5
+ module ChefFS
6
+ def self.windows?
7
+ false
8
+ end
9
+ end
@@ -0,0 +1,91 @@
1
+ require 'chef_fs/diff'
2
+
3
+ module ChefFS
4
+ module CommandLine
5
+ def self.diff(pattern, a_root, b_root, recurse_depth)
6
+ found_result = false
7
+ ChefFS::Diff::diffable_leaves_from_pattern(pattern, a_root, b_root, recurse_depth) do |a_leaf, b_leaf|
8
+ found_result = true
9
+ diff = diff_leaves(a_leaf, b_leaf)
10
+ yield diff if diff != ''
11
+ end
12
+ if !found_result && pattern.exact_path
13
+ yield "#{pattern}: No such file or directory on remote or local"
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ # Diff two known leaves (could be files or dirs)
20
+ def self.diff_leaves(old_file, new_file)
21
+ result = ''
22
+ # If both are directories
23
+ # If old is a directory and new is a file
24
+ # If old is a directory and new does not exist
25
+ if old_file.dir?
26
+ if new_file.dir?
27
+ result << "Common subdirectories: #{old_file.path}\n"
28
+ elsif new_file.exists?
29
+ result << "File #{new_file.path_for_printing} is a directory while file #{new_file.path_for_printing} is a regular file\n"
30
+ else
31
+ result << "Only in #{old_file.parent.path_for_printing}: #{old_file.name}\n"
32
+ end
33
+
34
+ # If new is a directory and old does not exist
35
+ # If new is a directory and old is a file
36
+ elsif new_file.dir?
37
+ if old_file.exists?
38
+ result << "File #{old_file.path_for_printing} is a regular file while file #{old_file.path_for_printing} is a directory\n"
39
+ else
40
+ result << "Only in #{new_file.parent.path_for_printing}: #{new_file.name}\n"
41
+ end
42
+
43
+ else
44
+ # Neither is a directory, so they are diffable with file diff
45
+ different, old_value, new_value = ChefFS::Diff::diff_files(old_file, new_file)
46
+ if different
47
+ old_path = old_file.path_for_printing
48
+ new_path = new_file.path_for_printing
49
+ result << "diff --knife #{old_path} #{new_path}\n"
50
+ if !old_value
51
+ result << "new file\n"
52
+ old_path = "/dev/null"
53
+ old_value = ''
54
+ end
55
+ if !new_value
56
+ result << "deleted file\n"
57
+ new_path = "/dev/null"
58
+ new_value = ''
59
+ end
60
+ result << diff_text(old_path, new_path, old_value, new_value)
61
+ end
62
+ end
63
+ return result
64
+ end
65
+
66
+ def self.diff_text(old_path, new_path, old_value, new_value)
67
+ # Copy to tempfiles before diffing
68
+ # TODO don't copy things that are already in files! Or find an in-memory diff algorithm
69
+ begin
70
+ new_tempfile = Tempfile.new("new")
71
+ new_tempfile.write(new_value)
72
+ new_tempfile.close
73
+
74
+ begin
75
+ old_tempfile = Tempfile.new("old")
76
+ old_tempfile.write(old_value)
77
+ old_tempfile.close
78
+
79
+ result = `diff -u #{old_tempfile.path} #{new_tempfile.path}`
80
+ result = result.gsub(/^--- #{old_tempfile.path}/, "--- #{old_path}")
81
+ result = result.gsub(/^\+\+\+ #{new_tempfile.path}/, "+++ #{new_path}")
82
+ result
83
+ ensure
84
+ old_tempfile.close!
85
+ end
86
+ ensure
87
+ new_tempfile.close!
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,158 @@
1
+ require 'chef_fs/file_system'
2
+ require 'chef/json_compat'
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+ require 'digest/md5'
6
+ require 'set'
7
+
8
+ module ChefFS
9
+ class Diff
10
+ def self.calc_checksum(value)
11
+ return nil if value == nil
12
+ Digest::MD5.hexdigest(value)
13
+ end
14
+
15
+ def self.diff_files(old_file, new_file)
16
+ #
17
+ # Short-circuit expensive comparison (could be an extra network
18
+ # request) if a pre-calculated checksum is there
19
+ #
20
+ if new_file.respond_to?(:checksum)
21
+ new_checksum = new_file.checksum
22
+ end
23
+ if old_file.respond_to?(:checksum)
24
+ old_checksum = old_file.checksum
25
+ end
26
+
27
+ old_value = :not_retrieved
28
+ new_value = :not_retrieved
29
+
30
+ if old_checksum || new_checksum
31
+ if !old_checksum
32
+ old_value = read_file_value(old_file)
33
+ if old_value
34
+ old_checksum = calc_checksum(old_value)
35
+ end
36
+ end
37
+ if !new_checksum
38
+ new_value = read_file_value(new_file)
39
+ if new_value
40
+ new_checksum = calc_checksum(new_value)
41
+ end
42
+ end
43
+
44
+ # If the checksums are the same, they are the same. Return.
45
+ return false if old_checksum == new_checksum
46
+ end
47
+
48
+ #
49
+ # Grab the values if we don't have them already from calculating checksum
50
+ #
51
+ old_value = read_file_value(old_file) if old_value == :not_retrieved
52
+ new_value = read_file_value(new_file) if new_value == :not_retrieved
53
+
54
+ return false if old_value == new_value
55
+ return false if old_value && new_value && !context_aware_diff(old_file, new_file, old_value, new_value)
56
+ return [ true, old_value, new_value ]
57
+ end
58
+
59
+ def self.context_aware_diff(old_file, new_file, old_value, new_value)
60
+ # TODO handle errors in reading JSON
61
+ if old_file.content_type == :json || new_file.content_type == :json
62
+ new_value = Chef::JSONCompat.from_json(new_value)
63
+ old_value = Chef::JSONCompat.from_json(old_value)
64
+
65
+ old_value != new_value
66
+ else
67
+ true
68
+ end
69
+ end
70
+
71
+ # Gets all common leaves, recursively, starting from the results of
72
+ # a pattern search on two roots.
73
+ #
74
+ # ==== Attributes
75
+ #
76
+ # * +pattern+ - a ChefFS::FilePattern representing the search you want to
77
+ # do on both roots.
78
+ # * +a_root+ - the first root.
79
+ # * +b_root+ -
80
+ # * +recurse_depth+ - the maximum number of directories to recurse from each
81
+ # pattern result. +0+ will cause pattern results to be immediately returned.
82
+ # +nil+ means recurse infinitely to find all leaves.
83
+ #
84
+ def self.diffable_leaves_from_pattern(pattern, a_root, b_root, recurse_depth)
85
+ # Make sure everything on the server is also on the filesystem, and diff
86
+ found_paths = Set.new
87
+ ChefFS::FileSystem.list(a_root, pattern) do |a|
88
+ found_paths << a.path
89
+ b = ChefFS::FileSystem.resolve_path(b_root, a.path)
90
+ diffable_leaves(a, b, recurse_depth) do |a_leaf, b_leaf|
91
+ yield [ a_leaf, b_leaf ]
92
+ end
93
+ end
94
+
95
+ # Check the outer regex pattern to see if it matches anything on the
96
+ # filesystem that isn't on the server
97
+ ChefFS::FileSystem.list(b_root, pattern) do |b|
98
+ if !found_paths.include?(b.path)
99
+ a = ChefFS::FileSystem.resolve_path(a_root, b.path)
100
+ yield [ a, b ]
101
+ end
102
+ end
103
+ end
104
+
105
+ # Gets all common leaves, recursively, from a pair of directories or files. It
106
+ # recursively descends into all children of +a+ and +b+, yielding equivalent
107
+ # pairs (common children with the same name) when it finds:
108
+ # * +a+ or +b+ is not a directory.
109
+ # * Both +a+ and +b+ are empty.
110
+ # * It reaches +recurse_depth+ depth in the tree.
111
+ #
112
+ # This method will *not* check whether files exist, nor will it actually diff
113
+ # the contents of files.
114
+ #
115
+ # ==== Attributes
116
+ #
117
+ # +a+ - the first directory to recursively scan
118
+ # +b+ - the second directory to recursively scan, in tandem with +a+
119
+ # +recurse_depth - the maximum number of directories to go down. +0+ will
120
+ # cause +a+ and +b+ to be immediately returned. +nil+ means recurse
121
+ # infinitely.
122
+ #
123
+ def self.diffable_leaves(a, b, recurse_depth)
124
+ # If we have children, recurse into them and diff the children instead of returning ourselves.
125
+ if recurse_depth != 0 && a.dir? && b.dir? && a.children.length > 0 && b.children.length > 0
126
+ a_children_names = Set.new
127
+ a.children.each do |a_child|
128
+ a_children_names << a_child.name
129
+ diffable_leaves(a_child, b.child(a_child.name), recurse_depth ? recurse_depth - 1 : nil) do |a_leaf, b_leaf|
130
+ yield [ a_leaf, b_leaf ]
131
+ end
132
+ end
133
+
134
+ # Check b for children that aren't in a
135
+ b.children.each do |b_child|
136
+ if !a_children_names.include?(b_child.name)
137
+ yield [ a.child(b_child.name), b_child ]
138
+ end
139
+ end
140
+ return
141
+ end
142
+
143
+ # Otherwise, this is a leaf we must diff.
144
+ yield [a, b]
145
+ end
146
+
147
+ private
148
+
149
+ def self.read_file_value(file)
150
+ begin
151
+ return file.read
152
+ rescue ChefFS::FileSystem::NotFoundException
153
+ return nil
154
+ end
155
+ end
156
+ end
157
+ end
158
+
@@ -0,0 +1,292 @@
1
+ require 'chef_fs'
2
+ require 'chef_fs/path_utils'
3
+
4
+ module ChefFS
5
+ #
6
+ # Represents a glob pattern. This class is designed so that it can
7
+ # match arbitrary strings, and tell you about partial matches.
8
+ #
9
+ # Examples:
10
+ # * <tt>a*z</tt>
11
+ # - Matches <tt>abcz</tt>
12
+ # - Does not match <tt>ab/cd/ez</tt>
13
+ # - Does not match <tt>xabcz</tt>
14
+ # * <tt>a**z</tt>
15
+ # - Matches <tt>abcz</tt>
16
+ # - Matches <tt>ab/cd/ez</tt>
17
+ #
18
+ # Special characters supported:
19
+ # * <tt>/</tt> (and <tt>\\</tt> on Windows) - directory separators
20
+ # * <tt>\*</tt> - match zero or more characters (but not directory separators)
21
+ # * <tt>\*\*</tt> - match zero or more characters, including directory separators
22
+ # * <tt>?</tt> - match exactly one character (not a directory separator)
23
+ # Only on Unix:
24
+ # * <tt>[abc0-9]</tt> - match one of the included characters
25
+ # * <tt>\\<character></tt> - escape character: match the given character
26
+ #
27
+ class FilePattern
28
+ # Initialize a new FilePattern with the pattern string.
29
+ #
30
+ # Raises +ArgumentError+ if empty file pattern is specified
31
+ def initialize(pattern)
32
+ @pattern = pattern
33
+ end
34
+
35
+ # The pattern string.
36
+ attr_reader :pattern
37
+
38
+ # Reports whether this pattern could match children of <tt>path</tt>.
39
+ # If the pattern doesn't match the path up to this point or
40
+ # if it matches and doesn't allow further children, this will
41
+ # return <tt>false</tt>.
42
+ #
43
+ # ==== Attributes
44
+ #
45
+ # * +path+ - a path to check
46
+ #
47
+ # ==== Examples
48
+ #
49
+ # abc/def.could_match_children?('abc') == true
50
+ # abc.could_match_children?('abc') == false
51
+ # abc/def.could_match_children?('x') == false
52
+ # a**z.could_match_children?('ab/cd') == true
53
+ def could_match_children?(path)
54
+ return false if path == '' # Empty string is not a path
55
+
56
+ argument_is_absolute = !!(path[0] =~ /^#{ChefFS::PathUtils::regexp_path_separator}/)
57
+ return false if is_absolute != argument_is_absolute
58
+ path = path[1,path.length-1] if argument_is_absolute
59
+
60
+ path_parts = ChefFS::PathUtils::split(path)
61
+ # If the pattern is shorter than the path (or same size), children will be larger than the pattern, and will not match.
62
+ return false if regexp_parts.length <= path_parts.length && !has_double_star
63
+ # If the path doesn't match up to this point, children won't match either.
64
+ return false if path_parts.zip(regexp_parts).any? { |part,regexp| !regexp.nil? && !regexp.match(part) }
65
+ # Otherwise, it's possible we could match: the path matches to this point, and the pattern is longer than the path.
66
+ # TODO There is one edge case where the double star comes after some characters like abc**def--we could check whether the next
67
+ # bit of path starts with abc in that case.
68
+ return true
69
+ end
70
+
71
+ # Returns the immediate child of a path that would be matched
72
+ # if this FilePattern was applied. If more than one child
73
+ # could match, this method returns nil.
74
+ #
75
+ # ==== Attributes
76
+ #
77
+ # * +path+ - The path to look for an exact child name under.
78
+ #
79
+ # ==== Returns
80
+ #
81
+ # The next directory in the pattern under the given path.
82
+ # If the directory part could match more than one child, it
83
+ # returns +nil+.
84
+ #
85
+ # ==== Examples
86
+ #
87
+ # abc/def.exact_child_name_under('abc') == 'def'
88
+ # abc/def/ghi.exact_child_name_under('abc') == 'def'
89
+ # abc/*/ghi.exact_child_name_under('abc') == nil
90
+ # abc/*/ghi.exact_child_name_under('abc/def') == 'ghi'
91
+ # abc/**/ghi.exact_child_name_under('abc/def') == nil
92
+ #
93
+ # This method assumes +could_match_children?(path)+ is +true+.
94
+ def exact_child_name_under(path)
95
+ path = path[1,path.length-1] if !!(path[0] =~ /^#{ChefFS::PathUtils::regexp_path_separator}/)
96
+ dirs_in_path = ChefFS::PathUtils::split(path).length
97
+ return nil if exact_parts.length <= dirs_in_path
98
+ return exact_parts[dirs_in_path]
99
+ end
100
+
101
+ # If this pattern represents an exact path, returns the exact path.
102
+ #
103
+ # abc/def.exact_path == 'abc/def'
104
+ # abc/*def.exact_path == 'abc/def'
105
+ # abc/x\\yz.exact_path == 'abc/xyz'
106
+ def exact_path
107
+ return nil if has_double_star || exact_parts.any? { |part| part.nil? }
108
+ result = ChefFS::PathUtils::join(*exact_parts)
109
+ is_absolute ? ChefFS::PathUtils::join('', result) : result
110
+ end
111
+
112
+ # Returns the normalized version of the pattern, with / as the directory
113
+ # separator, and "." and ".." removed.
114
+ #
115
+ # This does not presently change things like \b to b, but in the future
116
+ # it might.
117
+ def normalized_pattern
118
+ calculate
119
+ @normalized_pattern
120
+ end
121
+
122
+ # Tell whether this pattern matches absolute, or relative paths
123
+ def is_absolute
124
+ calculate
125
+ @is_absolute
126
+ end
127
+
128
+ # Returns <tt>true+ if this pattern matches the path, <tt>false+ otherwise.
129
+ #
130
+ # abc/*/def.match?('abc/foo/def') == true
131
+ # abc/*/def.match?('abc/foo') == false
132
+ def match?(path)
133
+ argument_is_absolute = !!(path[0] =~ /^#{ChefFS::PathUtils::regexp_path_separator}/)
134
+ return false if is_absolute != argument_is_absolute
135
+ path = path[1,path.length-1] if argument_is_absolute
136
+ !!regexp.match(path)
137
+ end
138
+
139
+ # Returns the string pattern
140
+ def to_s
141
+ pattern
142
+ end
143
+
144
+ # Given a relative file pattern and a directory, makes a new file pattern
145
+ # starting with the directory.
146
+ #
147
+ # FilePattern.relative_to('/usr/local', 'bin/*grok') == FilePattern.new('/usr/local/bin/*grok')
148
+ #
149
+ # BUG: this does not support patterns starting with <tt>..</tt>
150
+ def self.relative_to(dir, pattern)
151
+ return FilePattern.new(pattern) if pattern =~ /^#{ChefFS::PathUtils::regexp_path_separator}/
152
+ FilePattern.new(ChefFS::PathUtils::join(dir, pattern))
153
+ end
154
+
155
+ private
156
+
157
+ def regexp
158
+ calculate
159
+ @regexp
160
+ end
161
+
162
+ def regexp_parts
163
+ calculate
164
+ @regexp_parts
165
+ end
166
+
167
+ def exact_parts
168
+ calculate
169
+ @exact_parts
170
+ end
171
+
172
+ def has_double_star
173
+ calculate
174
+ @has_double_star
175
+ end
176
+
177
+ def calculate
178
+ if !@regexp
179
+ @is_absolute = !!(@pattern =~ /^#{ChefFS::PathUtils::regexp_path_separator}/)
180
+
181
+ full_regexp_parts = []
182
+ normalized_parts = []
183
+ @regexp_parts = []
184
+ @exact_parts = []
185
+ @has_double_star = false
186
+
187
+ ChefFS::PathUtils::split(pattern).each do |part|
188
+ regexp, exact, has_double_star = FilePattern::pattern_to_regexp(part)
189
+ if has_double_star
190
+ @has_double_star = true
191
+ end
192
+
193
+ # Skip // and /./ (pretend it's not there)
194
+ if exact == '' || exact == '.'
195
+ next
196
+ end
197
+
198
+ # Back up when you see .. (unless the prior part has ** in it, in which case .. must be preserved)
199
+ if exact == '..'
200
+ if @is_absolute && normalized_parts.length == 0
201
+ # If we are at the root, just pretend the .. isn't there
202
+ next
203
+ elsif normalized_parts.length > 0
204
+ regexp_prev, exact_prev, has_double_star_prev = FilePattern.pattern_to_regexp(normalized_parts[-1])
205
+ if has_double_star_prev
206
+ raise ArgumentError, ".. overlapping a ** is unsupported"
207
+ end
208
+ full_regexp_parts.pop
209
+ normalized_parts.pop
210
+ if !@has_double_star
211
+ @regexp_parts.pop
212
+ @exact_parts.pop
213
+ end
214
+ next
215
+ end
216
+ end
217
+
218
+ # Build up the regexp
219
+ full_regexp_parts << regexp
220
+ normalized_parts << part
221
+ if !@has_double_star
222
+ @regexp_parts << Regexp.new("^#{regexp}$")
223
+ @exact_parts << exact
224
+ end
225
+ end
226
+
227
+ @regexp = Regexp.new("^#{full_regexp_parts.join(ChefFS::PathUtils::regexp_path_separator)}$")
228
+ @normalized_pattern = ChefFS::PathUtils.join(*normalized_parts)
229
+ @normalized_pattern = ChefFS::PathUtils.join('', @normalized_pattern) if @is_absolute
230
+ end
231
+ end
232
+
233
+ def self.pattern_special_characters
234
+ if ChefFS::windows?
235
+ @pattern_special_characters ||= /(\*\*|\*|\?|[\*\?\.\|\(\)\[\]\{\}\+\\\\\^\$])/
236
+ else
237
+ # Unix also supports character regexes and backslashes
238
+ @pattern_special_characters ||= /(\\.|\[[^\]]+\]|\*\*|\*|\?|[\*\?\.\|\(\)\[\]\{\}\+\\\\\^\$])/
239
+ end
240
+ @pattern_special_characters
241
+ end
242
+
243
+ def self.regexp_escape_characters
244
+ [ '[', '\\', '^', '$', '.', '|', '?', '*', '+', '(', ')', '{', '}' ]
245
+ end
246
+
247
+ def self.pattern_to_regexp(pattern)
248
+ regexp = ""
249
+ exact = ""
250
+ has_double_star = false
251
+ pattern.split(pattern_special_characters).each_with_index do |part, index|
252
+ # Odd indexes from the split are symbols. Even are normal bits.
253
+ if index % 2 == 0
254
+ exact << part if !exact.nil?
255
+ regexp << part
256
+ else
257
+ case part
258
+ # **, * and ? happen on both platforms.
259
+ when '**'
260
+ exact = nil
261
+ has_double_star = true
262
+ regexp << '.*'
263
+ when '*'
264
+ exact = nil
265
+ regexp << '[^\/]*'
266
+ when '?'
267
+ exact = nil
268
+ regexp << '.'
269
+ else
270
+ if part[0] == '\\' && part.length == 2
271
+ # backslash escapes are only supported on Unix, and are handled here by leaving the escape on (it means the same thing in a regex)
272
+ exact << part[1] if !exact.nil?
273
+ if regexp_escape_characters.include?(part[1])
274
+ regexp << part
275
+ else
276
+ regexp << part[1]
277
+ end
278
+ elsif part[0] == '[' && part.length > 1
279
+ # [...] happens only on Unix, and is handled here by *not* backslashing (it means the same thing in and out of regex)
280
+ exact = nil
281
+ regexp << part
282
+ else
283
+ exact += part if !exact.nil?
284
+ regexp << "\\#{part}"
285
+ end
286
+ end
287
+ end
288
+ end
289
+ [regexp, exact, has_double_star]
290
+ end
291
+ end
292
+ end