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.
- data/LICENSE +201 -0
- data/README.rdoc +114 -0
- data/Rakefile +24 -0
- data/lib/chef/knife/diff.rb +28 -0
- data/lib/chef/knife/list.rb +107 -0
- data/lib/chef/knife/show.rb +30 -0
- data/lib/chef_fs.rb +9 -0
- data/lib/chef_fs/command_line.rb +91 -0
- data/lib/chef_fs/diff.rb +158 -0
- data/lib/chef_fs/file_pattern.rb +292 -0
- data/lib/chef_fs/file_system.rb +69 -0
- data/lib/chef_fs/file_system/base_fs_dir.rb +23 -0
- data/lib/chef_fs/file_system/base_fs_object.rb +52 -0
- data/lib/chef_fs/file_system/chef_server_root_dir.rb +48 -0
- data/lib/chef_fs/file_system/cookbook_dir.rb +80 -0
- data/lib/chef_fs/file_system/cookbook_file.rb +32 -0
- data/lib/chef_fs/file_system/cookbook_subdir.rb +25 -0
- data/lib/chef_fs/file_system/cookbooks_dir.rb +21 -0
- data/lib/chef_fs/file_system/data_bag_dir.rb +22 -0
- data/lib/chef_fs/file_system/data_bags_dir.rb +23 -0
- data/lib/chef_fs/file_system/file_system_entry.rb +36 -0
- data/lib/chef_fs/file_system/file_system_root_dir.rb +11 -0
- data/lib/chef_fs/file_system/nonexistent_fs_object.rb +23 -0
- data/lib/chef_fs/file_system/not_found_exception.rb +12 -0
- data/lib/chef_fs/file_system/rest_list_dir.rb +42 -0
- data/lib/chef_fs/file_system/rest_list_entry.rb +72 -0
- data/lib/chef_fs/knife.rb +47 -0
- data/lib/chef_fs/path_utils.rb +43 -0
- data/lib/chef_fs/version.rb +4 -0
- data/spec/chef_fs/diff_spec.rb +263 -0
- data/spec/chef_fs/file_pattern_spec.rb +507 -0
- data/spec/chef_fs/file_system_spec.rb +117 -0
- data/spec/support/file_system_support.rb +108 -0
- 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,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
|
data/lib/chef_fs/diff.rb
ADDED
@@ -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
|