diff --git a/.spellr_wordlists/english.txt b/.spellr_wordlists/english.txt index 4ba3629..1a7c2f0 100644 --- a/.spellr_wordlists/english.txt +++ b/.spellr_wordlists/english.txt @@ -136,6 +136,7 @@ torvolds tsx ttributes txt +umc unanchorable unc unexpandable @@ -143,6 +144,7 @@ unfuck unnegated unrecursive unstaged +unstub untr upcase urrent diff --git a/lib/path_list/autoloader.rb b/lib/path_list/autoloader.rb index 9f6e39e..94ecfcb 100644 --- a/lib/path_list/autoloader.rb +++ b/lib/path_list/autoloader.rb @@ -19,7 +19,7 @@ def autoload(klass) def class_from_path(path) name = ::File.basename(path).delete_suffix('.rb') - if name == 'version' || name == 'expandable_path' + if name == 'version' || name == 'expandable_path' || name == 'scanner' name.upcase else name.gsub(/(?:^|_)(\w)/, &:upcase).delete('_') diff --git a/lib/path_list/pattern_parser/gitignore.rb b/lib/path_list/pattern_parser/gitignore.rb index 9e21a50..64775d6 100644 --- a/lib/path_list/pattern_parser/gitignore.rb +++ b/lib/path_list/pattern_parser/gitignore.rb @@ -15,12 +15,14 @@ class PatternParser class Gitignore Autoloader.autoload(self) + SCANNER = RuleScanner + # @api private # @param pattern [String] # @param polarity [:ignore, :allow] # @param root [String] def initialize(pattern, polarity, root) - @s = RuleScanner.new(pattern) + @s = self.class::SCANNER.new(pattern) @default_polarity = polarity @rule_polarity = polarity @root = root @@ -60,6 +62,7 @@ def prepare_regexp_builder end @start_any_dir_position = @re.length - 1 + @re.delete_at(@start_any_dir_position) if @root && @anchored end def break! @@ -94,7 +97,7 @@ def dir_only? def anchored! @anchored ||= begin - @re.delete_at(@start_any_dir_position) + @re&.delete_at(@start_any_dir_position) true end end @@ -121,8 +124,8 @@ def emit_end break! end - def process_backslash - return unless @s.backslash? + def process_escape + return unless @s.escape? if @re.append_string(@s.next_character) emitted! @@ -142,7 +145,7 @@ def process_character_class until @s.character_class_end? next if process_character_class_range - next if process_backslash + next if process_escape next if append_string(@s.character_class_literal) unmatchable_rule! @@ -158,11 +161,9 @@ def process_character_class_range start = @s.character_class_range_start return unless start - start = start.delete_prefix('\\') - append_string(start) - finish = @s.character_class_range_end.delete_prefix('\\') + finish = @s.character_class_range_end return true unless start < finish @@ -184,31 +185,39 @@ def process_rule catch :abort_build do blank! if @s.hash? negated! if @s.exclamation_mark? - prepare_regexp_builder - anchored! if !@anchored && @s.slash? + process_first_characters catch :break do loop do - next if process_backslash - next unmatchable_rule! if @s.star_star_slash_slash? - next append_part(:any) && dir_only! if @s.star_star_slash_end? - next append_part(:any_dir) && anchored! if @s.star_star_slash? - next unmatchable_rule! if @s.slash_slash? - next append_part(:dir) && append_part(:any) && anchored! if @s.slash_star_star_end? - next append_part(:any_non_dir) if @s.star? - next dir_only! if @s.slash_end? - next append_part(:dir) && anchored! if @s.slash? - next append_part(:one_non_dir) if @s.question_mark? - next if process_character_class - next if append_string(@s.literal) - next if append_string(@s.significant_whitespace) - - process_end + process_next_characters end end end end + def process_first_characters + prepare_regexp_builder + anchored! if !@anchored && @s.slash? + end + + def process_next_characters + return if process_escape + return unmatchable_rule! if @s.star_star_slash_slash? + return append_part(:any) && dir_only! if @s.star_star_slash_end? + return append_part(:any_dir) && anchored! if @s.star_star_slash? + return unmatchable_rule! if @s.slash_slash? + return append_part(:dir) && append_part(:any) && anchored! if @s.slash_star_star_end? + return append_part(:any_non_dir) if @s.star? + return dir_only! if @s.slash_end? + return append_part(:dir) && anchored! if @s.slash? + return append_part(:one_non_dir) if @s.question_mark? + return if process_character_class + return if append_string(@s.literal) + return if append_string(@s.significant_whitespace) + + process_end + end + def build_matcher @main_re ||= @re.dup.compress diff --git a/lib/path_list/pattern_parser/gitignore/rule_scanner.rb b/lib/path_list/pattern_parser/gitignore/rule_scanner.rb index b099cfe..44b7612 100644 --- a/lib/path_list/pattern_parser/gitignore/rule_scanner.rb +++ b/lib/path_list/pattern_parser/gitignore/rule_scanner.rb @@ -27,13 +27,53 @@ def slash? skip(%r{/}) end + # @return [String, nil] + def root + matched if scan(%r{/}) + end + + # @return [String, nil] + def home + scan(%r{~[^/]*}) + end + + # @return [Boolean] + def dot_slash? + skip(%r{\./}) + end + + # @return [Boolean] + def dot_end? + skip(/\.\s*\z/) + end + + # @return [Boolean] + def dot_slash_end? + skip(%r{\./\s*\z}) + end + + # @return [Boolean] + def dot_dot_slash_end? + skip(%r{\.\./\s*\z}) + end + + # @return [Boolean] + def dot_dot_slash? + skip(%r{\.\./}) + end + + # @return [Boolean] + def dot_dot_end? + skip(/\.\.\s*\z/) + end + # @return [Boolean] def slash_end? skip(%r{/\s*\z}) end # @return [Boolean] - def backslash? + def escape? skip(/\\/) end @@ -84,7 +124,7 @@ def character_class_literal # @return [String, nil] def character_class_range_start - matched if scan(/(\\.|[^\\\]])(?=-(\\.|[^\\\]]))/) + matched.delete_prefix('\\') if scan(/(\\.|[^\\\]])(?=-(\\.|[^\\\]]))/) end # @return [String, nil] @@ -93,7 +133,7 @@ def character_class_range_end # with the lookahead in character_class_range_start skip(/-/) scan(/(\\.|[^\\\]])/) - matched + matched.delete_prefix('\\') end # @return [String, nil] diff --git a/lib/path_list/pattern_parser/gitignore/windows_rule_scanner.rb b/lib/path_list/pattern_parser/gitignore/windows_rule_scanner.rb new file mode 100644 index 0000000..5b43c69 --- /dev/null +++ b/lib/path_list/pattern_parser/gitignore/windows_rule_scanner.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'strscan' + +class PathList + class PatternParser + class Gitignore + # @api private + class WindowsRuleScanner < RuleScanner + # @return [Boolean] + def slash? + skip(%r{[\\/]}) + end + + # @return [String, nil] + def home + # not sure this makes sense on windows, but just for similarity + scan(%r{~[^/\\]*}) + end + + # @return [String, nil] + def root + # / or \ or UMC path or driver letter + matched if scan(%r{(?:[\\/]{1,2}|[a-zA-Z]:[\\/])}) + end + + # @return [Boolean] + def dot_slash? + skip(%r{\.[\\/]}) + end + + # @return [Boolean] + def dot_slash_end? + skip(%r{\.[\\/]\s*\z}) + end + + # @return [Boolean] + def dot_dot_slash_end? + skip(%r{\.\.[\\/]\s*\z}) + end + + # @return [Boolean] + def dot_dot_slash? + skip(%r{\.\.[\\/]}) + end + + # @return [Boolean] + def slash_end? + skip(%r{[\\/]\s*\z}) + end + + # @return [Boolean] + def escape? + skip(/`/) + end + + # @return [Boolean] + def star_star_slash_end? + skip(%r{\*{2,}[\\/]\s*\z}) + end + + # @return [Boolean] + def star_star_slash_slash? + skip(%r{\*{2,}[\\/]{2}}) + end + + # @return [Boolean] + def slash_slash? + skip(%r{[\\/]{2}}) + end + + # @return [Boolean] + def star_star_slash? + skip(%r{\*{2,}[\\/]}) + end + + # @return [Boolean] + def slash_star_star_end? + skip(%r{[\\/]\*{2,}\s*\z}) + end + + # @return [String, nil] + def character_class_literal + matched if scan(/[^\]`][^\]`-]*(?!-)/) + end + + # @return [String, nil] + def character_class_range_start + matched.delete_prefix('`') if scan(/(`.|[^`\]])(?=-(`.|[^`\]]))/) + end + + # @return [String, nil] + def character_class_range_end + # we already confirmed this was going to match + # with the lookahead in character_class_range_start + skip(/-/) + scan(/(`.|[^`\]])/) + matched.delete_prefix('`') + end + + # @return [String, nil] + def literal + matched if scan(%r{[^*[\\/]?\[`\s]+}) + end + end + end + end +end diff --git a/lib/path_list/pattern_parser/glob_gitignore.rb b/lib/path_list/pattern_parser/glob_gitignore.rb index d7abb6d..cfcf902 100644 --- a/lib/path_list/pattern_parser/glob_gitignore.rb +++ b/lib/path_list/pattern_parser/glob_gitignore.rb @@ -17,16 +17,16 @@ class PatternParser # - Patterns beginning with `/` (or `!/`) are absolute. Not relative to the `root:` directory. # - Patterns beginning with `~` (or `!~`) are resolved relative to the `$HOME` or `~user` directory # - Patterns beginning with `./` or `../` (or `!./` or `!../`) are resolved relative to the `root:` directory - # - Patterns containing with `/../` are resolved relative to the `root:` directory # - Patterns beginning with `*` (or `!*`) will match any descendant of the `root:` directory # - Other patterns match children (not descendants) of the `root:` directory + # - Patterns containing with `/../` will remove the previous path segment, (`/**/` counts as one path segment) # - Additionally, on windows: # - either / or \ (slash or backslash) can be used as path separators. # - therefore \ (backslash) isn't available to be used as an escape character - # - instead ` (grave accent) is used as an escape character + # - instead ` (grave accent) is used as an escape character anywhere a backslash would be used # - patterns beginning with `c:/`, `d:\`, or `!c:/`, or etc are absolute. - # - a path beginning with / or \ is a shortcut for the current working directory drive. - # - there is no cross platform escape character, this is intended to match the current shell + # - a path beginning with / or \ is a shortcut for the current working directory's drive. + # - there is no cross platform escape character. # @example # PathList.only(ARGV, format: :glob_gitignore) # PathList.only( @@ -47,41 +47,78 @@ class PatternParser class GlobGitignore < Gitignore Autoloader.autoload(self) - # @api private - # @param pattern [String] - # @param polarity [:ignore, :allow] - # @param root [String] - def initialize(pattern, polarity, root) - pattern = +'' if pattern.start_with?('#') - negated_sigil = '!' if pattern.delete_prefix!('!') - pattern = normalize_slash(pattern) - if pattern.start_with?('*') - pattern = "#{negated_sigil}#{pattern}" - elsif pattern.match?(EXPANDABLE_PATH) - dir_only! if pattern.match?(%r{/\s*\z}) # expand_path will remove it + # @return [Boolean] + def process_root + root = @s.root + return false unless root - pattern = "#{negated_sigil}#{CanonicalPath.full_path_from(pattern, root)}" - root = nil - @anchored = true - else - pattern = "#{negated_sigil}/#{pattern}" - end + @root = ::File.expand_path(root) + emitted! + true + end + + # @return [Boolean] + def process_home + home = @s.home + return false unless home + + @root = ::File.expand_path(home) + emitted! + true + rescue ArgumentError + @s.unscan + nil + end - super(normalize_escape(pattern), polarity, root) + # @return [true] + def process_up_a_level + @re.up_a_level + emitted! + true end - private + # @return [Boolean] + def end_with_dir? + @re.end_with_dir? + end + + # @return [true] + def remove_trailing_dir + @re.remove_trailing_dir + emitted! + true + end + + # @return [void] + def process_first_characters + if process_root || process_home + anchored! + prepare_regexp_builder + return + end - def normalize_slash(pattern) - return pattern unless ::File::ALT_SEPARATOR + prepare_regexp_builder + return @s.unscan if @s.star? - pattern.tr('\\', '/') + anchored! + return dir_only! && remove_trailing_dir if @s.dot_slash_end? + return remove_trailing_dir if @s.dot_end? + return emitted! if @s.dot_slash? + return process_up_a_level && remove_trailing_dir && dir_only! if @s.dot_dot_slash_end? + return process_up_a_level && remove_trailing_dir if @s.dot_dot_end? + return process_up_a_level if @s.dot_dot_slash? end - def normalize_escape(pattern) - return pattern unless ::File::ALT_SEPARATOR + # @return [void] + def process_next_characters + return dir_only! && remove_trailing_dir if end_with_dir? && @s.dot_slash_end? + return remove_trailing_dir if end_with_dir? && @s.dot_end? + return emitted! if end_with_dir? && @s.dot_slash? + return process_up_a_level && remove_trailing_dir && dir_only! if end_with_dir? && @s.dot_dot_slash_end? + return process_up_a_level && remove_trailing_dir if end_with_dir? && @s.dot_dot_end? + return process_up_a_level if end_with_dir? && @s.dot_dot_slash? - pattern.tr('`', '\\') + super end end end diff --git a/lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb b/lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb index b9e17f8..d6274b9 100644 --- a/lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb +++ b/lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb @@ -5,25 +5,31 @@ class PatternParser class GlobGitignore # :nocov: # this isn't actually nocov, but it's cov is because i reload the file - EXPANDABLE_PATH = %r{(?: - \A(?: - [~/] # start with slash or tilde + EXPANDABLE_PATH = if ::File.expand_path('/') == '/' + %r{(?: + \A(?: + [~/] # start with slash or tilde + | + \.{1,2}(?:/|\z) # start with dot or dot dot followed by slash or nothing + ) | - \.{1,2}(?:/|\z) # start with dot or dot dot followed by slash or nothing - #{ - if ::File.expand_path('/') != '/' # only if drive letters are applicable - " - | - [a-zA-Z]:/ # drive letter - | - // # UNC path - " - end - } - ) - | - (?:[^\\]|\A)(?:\\{2})*/\.\./) # unescaped slash dot dot slash - }x.freeze + (?:[^\\]|\A)(?:\\{2})*/\.\./ # unescaped slash dot dot slash + )}x + else + %r{(?: + \A(?: + [~\\/] # start with slash or tilde + | + \.{1,2}(?:[\\/]|\z) # start with dot or dot dot followed by slash or nothing + | + [a-zA-Z]:[\\/] # start with drive letter + | + [\\/]{2} # UNC path + ) + | + (?:[^`]|\A)(?:`{2})*[\\/]\.\.[\\/] # unescaped slash dot dot slash + )}x.freeze + end # :nocov: end end diff --git a/lib/path_list/pattern_parser/glob_gitignore/scanner.rb b/lib/path_list/pattern_parser/glob_gitignore/scanner.rb new file mode 100644 index 0000000..8b2c1a8 --- /dev/null +++ b/lib/path_list/pattern_parser/glob_gitignore/scanner.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class PathList + class PatternParser + class GlobGitignore + # :nocov: + # this isn't actually nocov, but it's cov is because i reload the file + SCANNER = if ::File::ALT_SEPARATOR + Gitignore::WindowsRuleScanner + else + Gitignore::RuleScanner + end + # :nocov: + end + end +end diff --git a/lib/path_list/token_regexp/path.rb b/lib/path_list/token_regexp/path.rb index 3e3020d..1ea96c0 100644 --- a/lib/path_list/token_regexp/path.rb +++ b/lib/path_list/token_regexp/path.rb @@ -36,6 +36,27 @@ def compress self end + # @return [void] + def up_a_level + return if @parts.count(:dir) <= 1 + + @parts.pop # remove trailing dir + @parts.pop until end_with_dir? + end + + # @return [void] + def remove_trailing_dir + return if @parts.count(:dir) <= 1 + + @parts.pop if @parts.last == :dir + end + + # @return [Boolean] + def end_with_dir? + last = @parts.last + last == :dir || last == :any_dir + end + # @return [Array] def ancestors prev_rule = [] diff --git a/spec/pattern_parser/glob_gitignore_spec.rb b/spec/pattern_parser/glob_gitignore_spec.rb index 6ff3778..f79db17 100644 --- a/spec/pattern_parser/glob_gitignore_spec.rb +++ b/spec/pattern_parser/glob_gitignore_spec.rb @@ -35,53 +35,204 @@ def build(pattern) it { expect(build('foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) } end + describe 'initial ../' do + it { expect(build('../foo')).to be_like PathList::Matcher::ExactString.new('/a/foo', :ignore) } + it { expect(build('../../foo')).to be_like PathList::Matcher::ExactString.new('/foo', :ignore) } + it { expect(build('../../../foo')).to be_like PathList::Matcher::ExactString.new('/foo', :ignore) } + end + + describe 'mid /../' do + it { expect(build('bar/../foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) } + it { expect(build('bar/../../foo')).to be_like PathList::Matcher::ExactString.new('/a/foo', :ignore) } + it { expect(build('bar/../../../foo')).to be_like PathList::Matcher::ExactString.new('/foo', :ignore) } + it { expect(build('bar/../../../../foo')).to be_like PathList::Matcher::ExactString.new('/foo', :ignore) } + end + + describe 'trailing /..' do + it { expect(build('bar/..')).to be_like PathList::Matcher::ExactString.new('/a/path', :ignore) } + it { expect(build('bar/../..')).to be_like PathList::Matcher::ExactString.new('/a', :ignore) } + it { expect(build('bar/../../..')).to be_like PathList::Matcher::ExactString.new('/', :ignore) } + it { expect(build('bar/../../../..')).to be_like PathList::Matcher::ExactString.new('/', :ignore) } + end + + describe 'trailing /../' do + it do + expect(build('bar/../')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a/path', :ignore) + ) + end + + it do + expect(build('bar/../../')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a', :ignore) + ) + end + + it do + expect(build('bar/../../../')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/', :ignore) + ) + end + + it do + expect(build('bar/../../../../')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/', :ignore) + ) + end + end + + describe 'only ../' do + it do + expect(build('../')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a', :ignore) + ) + end + + it do + expect(build('../../')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/', :ignore) + ) + end + + it do + expect(build('../../../')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/', :ignore) + ) + end + end + + describe 'initial ./' do + it { expect(build('./foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) } + it { expect(build('././foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) } + end + + describe 'mid /./' do + it { expect(build('bar/./foo')).to be_like PathList::Matcher::ExactString.new('/a/path/bar/foo', :ignore) } + it { expect(build('bar/././foo')).to be_like PathList::Matcher::ExactString.new('/a/path/bar/foo', :ignore) } + end + + describe 'trailing /.' do + it { expect(build('bar/.')).to be_like PathList::Matcher::ExactString.new('/a/path/bar', :ignore) } + it { expect(build('bar/./.')).to be_like PathList::Matcher::ExactString.new('/a/path/bar', :ignore) } + end + + describe 'trailing /./' do + it do + expect(build('bar/./')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a/path/bar', :ignore) + ) + end + + it do + expect(build('bar/././')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a/path/bar', :ignore) + ) + end + end + + describe 'only ./' do + it do + expect(build('./')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a/path', :ignore) + ) + end + + it do + expect(build('././')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a/path', :ignore) + ) + end + end + describe 'The windows case' do + let(:drive_letter) { windows? ? File.expand_path('/')[0] : 'D' } + before do + next if windows? + # use windows expand_path: allow(File).to receive(:expand_path).and_call_original - allow(File).to receive(:expand_path).with('/').and_return('D:/') allow(File).to receive(:expand_path) - .with(a_string_matching(%r{D:/.*}), '/a/path') do |path, _| - path.tr('\\\\', '/').delete_suffix('/') - end + .with(%r{\A(?:#{drive_letter}:)?[\\/]\z}) + .and_return("#{drive_letter}:/") stub_const('::File::ALT_SEPARATOR', '\\') silence_warnings do load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb', __dir__) + load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/scanner.rb', __dir__) end end after do - allow(File).to receive(:expand_path).with('/').and_call_original + next if windows? + + allow(File).to receive(:expand_path).and_call_original + # i need this to be as i expect for the rest of the tests, and i'm not sure how to unstub it properly: + stub_const('::File::ALT_SEPARATOR', windows? ? '\\' : nil) silence_warnings do load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb', __dir__) + load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/scanner.rb', __dir__) end end - it { expect(build('D:/foo/bar')).to be_like PathList::Matcher::ExactString.new('D:/foo/bar', :ignore) } - it { expect(build('D:\\foo\\bar')).to be_like PathList::Matcher::ExactString.new('D:/foo/bar', :ignore) } - it { expect(build('D:\foo/bar')).to be_like PathList::Matcher::ExactString.new('D:/foo/bar', :ignore) } - it { expect(build('D:\`f`o`o`/`b`a`r')).to be_like PathList::Matcher::ExactString.new('D:/foo/bar', :ignore) } + it do + expect(build("#{drive_letter}:/foo/bar")) + .to be_like PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore) + end + + it do + expect(build("#{drive_letter}:\\foo\\bar")) + .to be_like PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore) + end it do - expect(build('D:/foo/bar/')) - .to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore)) + expect(build("#{drive_letter}:\\foo/bar")) + .to be_like PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore) end it do - expect(build('D:\\foo\\bar/')) - .to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore)) + expect(build("#{drive_letter}:\\`f`o`o`/`b`a`r")) + .to be_like PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore) end it do - expect(build('D:\\foo/bar/')) - .to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore)) + expect(build("#{drive_letter}:/foo/bar/")) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore) + ) end it do - expect(build('D:\\foo/bar\\')) - .to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore)) + expect(build("#{drive_letter}:\\foo\\bar/")) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore) + ) + end + + it do + expect(build("#{drive_letter}:\\foo/bar/")) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore) + ) + end + + it do + expect(build("#{drive_letter}:\\foo/bar\\")) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore) + ) end it do