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..2110825 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 == 'scanner' name.upcase else name.gsub(/(?:^|_)(\w)/, &:upcase).delete('_') diff --git a/lib/path_list/candidate.rb b/lib/path_list/candidate.rb index e715001..489b3d0 100644 --- a/lib/path_list/candidate.rb +++ b/lib/path_list/candidate.rb @@ -4,6 +4,26 @@ class PathList # @api private # The object that gets passed to all {PathList::Matcher} subclasses #match class Candidate + # https://github.com/jruby/jruby/issues/8018 + # ftype follows symlinks on jruby on windows. + if ::RUBY_PLATFORM == 'java' && ::RbConfig::CONFIG['host_os'].match?(/mswin|mingw/) + # :nodoc: + module FileFtypeFix + refine ::File do + # :nodoc: + def ftype(path) + if ::File.symlink?(path) + 'link' + else + super + end + end + end + end + using FileFtypeFix + end + # :nocov: + attr_reader :full_path # @param full_path [String] resolved absolute path @@ -106,22 +126,6 @@ def shebang private # :nocov: - # https://github.com/jruby/jruby/issues/8018 - # ftype follows symlinks on jruby on windows. - if ::RUBY_PLATFORM == 'java' && ::RbConfig::CONFIG['host_os'].match?(/mswin|mingw/) - refine ::File do - # :nodoc: - def ftype(path) - if ::File.symlink?(path) - 'link' - else - super - end - end - end - end - # :nocov: - def ftype return @ftype if @ftype diff --git a/lib/path_list/canonical_path.rb b/lib/path_list/canonical_path.rb index c0238a0..d260ce2 100644 --- a/lib/path_list/canonical_path.rb +++ b/lib/path_list/canonical_path.rb @@ -10,20 +10,29 @@ class << self class_eval <<~RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition def case_insensitive? #{ - pwd = ::Dir.pwd - pwd_swapcase = pwd.swapcase + test_dir = ::Dir.pwd + test_dir_swapcase = test_dir.swapcase + # :nocov: # if the current directory has no casing differences # (maybe because it's at /) # then: - if pwd == pwd_swapcase + if test_dir == test_dir_swapcase require 'tmpdir' - pwd = ::File.write(::Dir.mktmpdir + '/case_test', '') - pwd_swapcase = pwd.swapcase + test_dir = ::File.write(::Dir.mktmpdir + '/case_test', '') + test_dir_swapcase = test_dir.swapcase end # :nocov: - ::File.identical?(pwd, pwd_swapcase) + test_dir_stat = ::File.stat(test_dir) + if ::File.exist?(test_dir_swapcase) + test_dir_swapcase_stat = ::File.stat(test_dir_swapcase) + + test_dir_stat.dev == test_dir_swapcase_stat.dev && + test_dir_stat.ino == test_dir_swapcase_stat.ino + else + false + end } end RUBY diff --git a/lib/path_list/pattern_parser/gitignore.rb b/lib/path_list/pattern_parser/gitignore.rb index 9e21a50..0d44ec6 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 @@ -51,9 +53,7 @@ def implicit_matcher private def prepare_regexp_builder - @re = if @root.nil? - TokenRegexp::Path.new([:start_anchor]) - elsif @root.end_with?('/') + @re = if @root.end_with?('/') TokenRegexp::Path.new_from_path(@root, [:any_dir]) else TokenRegexp::Path.new_from_path(@root, [:dir, :any_dir]) @@ -117,12 +117,13 @@ def append_string(string) end def emit_end + @re.remove_trailing_dir append_part :end_anchor 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 +143,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 +159,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 +183,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..0117471 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_end + matched if scan(%r{/\s*\z}) + end + + # @return [String, nil] + def root + matched if scan(%r{/}) + end + + # @return [String, nil] + def home_slash_end + self[1] if scan(%r{(~[^/]*)/\s*\z}) + end + + # @return [String, nil] + def home_slash_or_end + self[1] if scan(%r{(~[^/]*)(?:/|\s*\z)}) + end + + # @return [Boolean] + def dot_slash_or_end? + skip(%r{\.(?:/|\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_or_end? + skip(%r{\.\.(?:/|\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..9e97f12 --- /dev/null +++ b/lib/path_list/pattern_parser/gitignore/windows_rule_scanner.rb @@ -0,0 +1,118 @@ +# 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 root_end + # / or \ or UMC path or driver letter + matched if scan(%r{(?:[\\/]{1,2}|[a-zA-Z]:[\\/])\s*\z}) + end + + # @return [String, nil] + def root + # / or \ or UMC path or driver letter + matched if scan(%r{(?:[\\/]{1,2}|[a-zA-Z]:[\\/])}) + end + + # @return [String, nil] + def home_slash_end + '~' if scan(%r{~[\\/]\s*\z}) + end + + # @return [String, nil] + def home_slash_or_end + '~' if scan(%r{~(?:[\\/]|\s*\z)}) + end + + # @return [Boolean] + def dot_slash_or_end? + skip(%r{\.(?:[\\/]|\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_or_end? + skip(%r{\.\.(?:[\\/]|\s*\z)}) + 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..da3d8e5 100644 --- a/lib/path_list/pattern_parser/glob_gitignore.rb +++ b/lib/path_list/pattern_parser/glob_gitignore.rb @@ -15,18 +15,18 @@ class PatternParser # # Differences from standard gitignore patterns: # - 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 `!~`) are resolved relative to the home directory or. # - 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,73 @@ 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_end + dir_only! if root + root ||= @s.root - pattern = "#{negated_sigil}#{CanonicalPath.full_path_from(pattern, root)}" - root = nil - @anchored = true - else - pattern = "#{negated_sigil}/#{pattern}" - end + return false unless root + + @root = ::File.expand_path(root) + emitted! + true + end + + # @return [Boolean] + def process_home + home = @s.home_slash_end + dir_only! if home + home ||= @s.home_slash_or_end + + return false unless home - super(normalize_escape(pattern), polarity, root) + @root = ::File.expand_path(home) + emitted! + true + rescue ArgumentError + @s.unscan + nil end - private + # @return [true] + def process_up_a_level + @re.up_a_level + emitted! + true + end + + # @return [Boolean] + def end_with_dir? + @re.end_with_dir? + end + + # @return [void] + def process_first_characters + if process_root || process_home + prepare_regexp_builder + anchored! + 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! && emitted! if @s.dot_slash_end? + return emitted! if @s.dot_slash_or_end? + return process_up_a_level && dir_only! if @s.dot_dot_slash_end? + return process_up_a_level if @s.dot_dot_slash_or_end? end - def normalize_escape(pattern) - return pattern unless ::File::ALT_SEPARATOR + # @return [void] + def process_next_characters + return dir_only! && emitted! if end_with_dir? && @s.dot_slash_end? + return emitted! if end_with_dir? && @s.dot_slash_or_end? + return process_up_a_level && dir_only! if end_with_dir? && @s.dot_dot_slash_end? + return process_up_a_level if end_with_dir? && @s.dot_dot_slash_or_end? - 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 deleted file mode 100644 index b9e17f8..0000000 --- a/lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb +++ /dev/null @@ -1,30 +0,0 @@ -# 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 - EXPANDABLE_PATH = %r{(?: - \A(?: - [~/] # start with slash or tilde - | - \.{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 - # :nocov: - end - 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/canonical_path_spec.rb b/spec/canonical_path_spec.rb new file mode 100644 index 0000000..bfd2168 --- /dev/null +++ b/spec/canonical_path_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +RSpec.describe(PathList::CanonicalPath) do + let(:home) do + File.expand_path(Dir.home) + end + + describe '.case_insensitive?' do + it 'returns a boolean' do + expect(described_class.case_insensitive?) + .to be(RbConfig::CONFIG['host_os'] == 'darwin') + end + end + + describe '.full_path_from' do + it 'handles nil values' do + expect(described_class.full_path_from(nil, nil)) + .to eq(Dir.pwd) + end + + it 'handles `from` value being nil' do + expect(described_class.full_path_from('foo', nil)) + .to eq("#{Dir.pwd}/foo") + end + + it 'handles `to` value being nil' do + expect(described_class.full_path_from(nil, '/bar')) + .to eq("#{FSROOT}bar") + end + + it 'appends path' do + expect(described_class.full_path_from('foo', '/bar')) + .to eq("#{FSROOT}bar/foo") + end + + it 'replaces absolute path' do + expect(described_class.full_path_from('/foo', '/bar')) + .to eq("#{FSROOT}foo") + end + + it 'expands home' do + expect(described_class.full_path_from('~', '/bar')) + .to eq(home) + end + + it 'expands home with subdir' do + expect(described_class.full_path_from('~/foo', '/bar')) + .to eq("#{home}/foo") + end + + context 'with ~user', skip: ('Not applicable on windows' if windows?) do + it 'expands real user home' do + expect(described_class.full_path_from("~#{os_user}", '/bar')) + .to eq(home) + end + + it 'expands real user home with subdir' do + expect(described_class.full_path_from("~#{os_user}/foo", '/bar')) + .to eq("#{home}/foo") + end + end + + it 'treats fake user home as relative' do + expect(described_class.full_path_from('~nonsense-not-a-user-1437801', '/bar')) + .to eq("#{FSROOT}bar/~nonsense-not-a-user-1437801") + end + + it 'treats fake user home with subdir as relative' do + expect(described_class.full_path_from('~nonsense-not-a-user-1437801/foo', '/bar')) + .to eq("#{FSROOT}bar/~nonsense-not-a-user-1437801/foo") + end + end + + describe '.full_path' do + it 'handles nil value' do + expect(described_class.full_path(nil)) + .to eq(Dir.pwd) + end + + it 'appends path to current path' do + expect(described_class.full_path('foo')) + .to eq("#{Dir.pwd}/foo") + end + + it 'replaces absolute path' do + expect(described_class.full_path('/foo')) + .to eq("#{FSROOT}foo") + end + + it 'expands home' do + expect(described_class.full_path('~')) + .to eq(home) + end + + it 'expands home with subdir' do + expect(described_class.full_path('~/foo')) + .to eq("#{Dir.home}/foo") + end + + context 'with ~user', skip: ('Not applicable on windows' if windows?) do + it 'expands real user home' do + expect(described_class.full_path("~#{os_user}")) + .to eq(home) + end + + it 'expands real user home with subdir' do + expect(described_class.full_path("~#{os_user}/foo")) + .to eq("#{home}/foo") + end + end + + it 'treats fake user home as relative' do + expect(described_class.full_path('~nonsense-not-a-user-1437801')) + .to eq("#{Dir.pwd}/~nonsense-not-a-user-1437801") + end + + it 'treats fake user home with subdir as relative' do + expect(described_class.full_path('~nonsense-not-a-user-1437801/foo')) + .to eq("#{Dir.pwd}/~nonsense-not-a-user-1437801/foo") + end + end +end diff --git a/spec/ignore_or_include_spec.rb b/spec/ignore_or_include_spec.rb index c1d1b10..0cba26d 100644 --- a/spec/ignore_or_include_spec.rb +++ b/spec/ignore_or_include_spec.rb @@ -53,8 +53,10 @@ end end - # can't have literal backslashes in filenames in windows - describe 'literal backslashes in filenames', skip: windows? do + describe( + 'literal backslashes in filenames', + skip: ("can't have literal backslashes in filenames in windows" if windows?) + ) do it "never matches backslashes when they're not in the pattern" do gitignore 'foo' @@ -90,8 +92,10 @@ end end - # can't end with literal backslashes in filenames in windows - describe 'Trailing spaces are ignored unless they are quoted with backslash ("\")', skip: windows? do + describe( + 'Trailing spaces are ignored unless they are quoted with backslash ("\")', + skip: ("can't end with literal backslashes in filenames in windows" if windows?) + ) do it 'ignores trailing spaces in the gitignore file' do gitignore 'foo ' diff --git a/spec/path_list_spec.rb b/spec/path_list_spec.rb index 5e15bba..4507be1 100644 --- a/spec/path_list_spec.rb +++ b/spec/path_list_spec.rb @@ -1174,6 +1174,15 @@ end end + context 'when given an root with an unexpandable user path' do + subject(:path_list) { described_class.only('foo', root: '~not-a-user635728345', format: :glob_gitignore) } + + it 'treats it as literal' do + expect(subject).not_to allow_files('foo') + expect(subject).to allow_files('~not-a-user635728345/foo') + end + end + context 'when given an array of negated argv_rules with absolute paths and gitignore' do subject(:path_list) do described_class.gitignore.only(['*', '!./foo', "!#{Dir.pwd}/baz"], format: :glob_gitignore) diff --git a/spec/pattern_parser/glob_gitignore_spec.rb b/spec/pattern_parser/glob_gitignore_spec.rb index 6ff3778..66e55c6 100644 --- a/spec/pattern_parser/glob_gitignore_spec.rb +++ b/spec/pattern_parser/glob_gitignore_spec.rb @@ -35,85 +35,794 @@ def build(pattern) it { expect(build('foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) } end + describe 'root only' do + it do + expect(build('/')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new(FSROOT, :ignore) + ) + end + end + + describe 'initial ~' do + let(:home) { PathList::CanonicalPath.full_path('~') } + + it { expect(build('~')).to be_like PathList::Matcher::ExactString.new(home, :ignore) } + + context 'with ~user', skip: ('Not applicable on windows' if windows?) do + it do + expect(build("~#{os_user}")) + .to be_like PathList::Matcher::ExactString.new(home, :ignore) + end + + it do + expect(build("~#{os_user}/foo")) + .to be_like PathList::Matcher::ExactString.new("#{home}/foo", :ignore) + end + + it do + expect(build("~#{os_user}/")) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new(home, :ignore) + ) + end + end + + it do + expect(build('~not-a-user635728345')) + .to be_like PathList::Matcher::ExactString + .new('/a/path/~not-a-user635728345', :ignore) + end + + it do + expect(build('~/')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new(home, :ignore) + ) + end + + it do + expect(build('~/.')) + .to be_like PathList::Matcher::ExactString.new(home, :ignore) + end + + it do + expect(build('./~')) + .to be_like PathList::Matcher::ExactString.new('/a/path/~', :ignore) + end + + it do + expect(build('~not-a-user635728345/')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a/path/~not-a-user635728345', :ignore) + ) + end + 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) } + 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/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) } + + it do + expect(build('bar/**/../foo')) + .to be_like PathList::Matcher::ExactString.new('/a/path/bar/foo', :ignore) + end + + it do + expect(build('bar/**/**/../foo')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/bar/(?:.*/)?foo\z}, :ignore) + end + + 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/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', :ignore) } + it { expect(build('b[ai]r/..')).to be_like PathList::Matcher::ExactString.new('/a/path', :ignore) } + it { expect(build('ba[rb]/..')).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('b[ai]r/../..')).to be_like PathList::Matcher::ExactString.new('/a', :ignore) } + it { expect(build('ba[rb]/../..')).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) } + + 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', :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('b[ai]r/../')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a/path', :ignore) + ) + end + + it do + expect(build('ba[rb]/../')) + .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('b[ai]r/../../')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a', :ignore) + ) + end + + it do + expect(build('ba[rb]/../../')) + .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 + + 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', :ignore) + ) + end + end + + describe 'only ../' do + it do + expect(build('..')) + .to be_like PathList::Matcher::ExactString.new('/a', :ignore) + end + + 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) } + + 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) } + + 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) } + + 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) } + + 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 + + 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::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 + + it do + expect(build('./.')) + .to be_like 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('/') + .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/scanner.rb', __dir__) + load File.expand_path('../../lib/path_list/token_regexp/build.rb', __dir__) + end + end + + after do + 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/scanner.rb', __dir__) + load File.expand_path('../../lib/path_list/token_regexp/build.rb', __dir__) + end + end + + it do + expect(build('/')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new("#{drive_letter}:/", :ignore) + ) + end + + it do + expect(build("#{drive_letter}:/")) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new("#{drive_letter}:/", :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("#{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("#{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("#{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 + 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('foo/bar')) + .to be_like PathList::Matcher::ExactString.new('/a/path/foo/bar', :ignore) + end + + it do + expect(build('foo\bar')) + .to be_like PathList::Matcher::ExactString.new('/a/path/foo/bar', :ignore) + end + + it do + expect(build('**/foo/bar')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/(?:.*/)?foo/bar\z}o, :ignore) + end + + it do + expect(build('**\\foo\\bar')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/(?:.*/)?foo/bar\z}o, :ignore) + end + + it do + expect(build('**\\foo\\bar\\')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::PathRegexp.new(%r{\A/a/path/(?:.*/)?foo/bar\z}o, :ignore) + ) + end + + describe 'initial ~' do + let(:home) { PathList::CanonicalPath.full_path('~') } + + it { expect(build('~')).to be_like PathList::Matcher::ExactString.new(home, :ignore) } + + it do + expect(build('~not-a-user635728345')) + .to be_like PathList::Matcher::ExactString + .new('/a/path/~not-a-user635728345', :ignore) + end + + it do + expect(build('~/')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new(home, :ignore) + ) + end + + it do + expect(build('~\\')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new(home, :ignore) + ) + end + + it do + expect(build('~/.')) + .to be_like PathList::Matcher::ExactString.new(home, :ignore) + end + + it do + expect(build('~\\.')) + .to be_like PathList::Matcher::ExactString.new(home, :ignore) + end + + it do + expect(build('./~')) + .to be_like PathList::Matcher::ExactString.new('/a/path/~', :ignore) + end + + it do + expect(build('.\\~')) + .to be_like PathList::Matcher::ExactString.new('/a/path/~', :ignore) + end + + it do + expect(build('~not-a-user635728345/')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a/path/~not-a-user635728345', :ignore) + ) + end + + it do + expect(build('~not-a-user635728345\\')) + .to be_like PathList::Matcher::MatchIfDir.new( + PathList::Matcher::ExactString.new('/a/path/~not-a-user635728345', :ignore) + ) + end + end + + describe '"[]" matches one character in a selected range' do + it 'matches a single character in a character class' do + expect(build('a[ab]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[ab]\z}, :ignore) + end + + it 'matches a single character in a character class range' do + expect(build('a[a-c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[a-c]\z}, :ignore) + end + + it 'treats a backward character class range as only the first character of the range' do + expect(build('a[d-a]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[d]\z}, :ignore) + end + + it 'treats a negated backward character class range as only the first character of the range' do + expect(build('a[^d-a]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^d]\z}, :ignore) + end + + it 'treats escaped backward character class range as the first character of the range' do + expect(build('a[`]-`[]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\]]\z}, :ignore) + end + + it 'treats negated escaped backward character class range as the first char of range' do + expect(build('a[^`]-`[]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^\]]\z}, :ignore) + end + + it 'treats a escaped character class range as as a range' do + expect(build('a[`[-`]]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\[-\]]\z}, :ignore) + end + + it 'treats a negated escaped character class range as a range' do + expect(build('a[^`[-`]]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^\[-\]]\z}, :ignore) + end + + it 'treats an unnecessarily escaped character class range as a range' do + expect(build('a[`a-`c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[a-c]\z}, :ignore) + end + + it 'treats a negated unnecessarily escaped character class range as a range' do + expect(build('a[^`a-`c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^a-c]\z}, :ignore) + end + + it 'treats a backward character class range with other options as only the first character of the range' do + expect(build('a[d-ba]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[da]\z}, :ignore) + end + + it 'treats a negated backward character class range with other chars as the first character of the range' do + expect(build('a[^d-ba]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^da]\z}, :ignore) + end + + it 'treats a backward char class range with other initial options as the first char of the range' do + expect(build('a[ad-b]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[ad]\z}, :ignore) + end + + it 'treats a negated backward char class range with other initial options as the first char of the range' do + expect(build('a[^ad-b]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^ad]\z}, :ignore) + end + + it 'treats a equal character class range as only the first character of the range' do + expect(build('a[d-d]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[d]\z}, :ignore) + end + + it 'treats a negated equal character class range as only the first character of the range' do + expect(build('a[^d-d]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^d]\z}, :ignore) + end + + it 'interprets a / after a character class range as not there' do + expect(build('a[a-c/]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[a-c/]\z}, :ignore) + end + + it 'interprets a \\ after a character class range as not there' do + expect(build('a[a-c\\]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[a-c\\]\z}, :ignore) + end + + it 'interprets a / before a character class range as not there' do + expect(build('a[/a-c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[/a-c]\z}, :ignore) + end + + it 'interprets a \\ before a character class range as not there' do + expect(build('a[\\a-c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\\a-c]\z}, :ignore) + end + + # TODO: confirm if that matches a slash character + it 'interprets a / before the dash in a character class range as any character from / to c' do + expect(build('a[+/-c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\+/-c]\z}, :ignore) + end + + it 'interprets a \\ before the dash in a character class range as any character from \\ to c' do + expect(build('a[+\\-c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\+\\-c]\z}, :ignore) + end + + it 'interprets a / after the dash in a character class range as any character from start to /' do + expect(build('a["-/c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)["-/c]\z}, :ignore) + end + + it 'interprets a \\ after the dash in a character class range as any character from start to \\' do + expect(build('a["-\\c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)["-\\c]\z}, :ignore) + end + + it 'interprets a slash then dash then character to be a character range' do + expect(build('a[/-c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[/-c]\z}, :ignore) + end + + it 'interprets a backslash then dash then character to be a character range' do + expect(build('a[\\-c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\\-c]\z}, :ignore) + end + + it 'interprets a character then dash then slash to be a character range' do + expect(build('a["-/]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)["-/]\z}, :ignore) + end + + it 'interprets a character then dash then backslash to be a character range' do + expect(build('a["-\\]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)["-\\]\z}, :ignore) + end + + context 'without raising warnings' do + # these edge cases raise warnings + # they're edge-casey enough if you hit them you deserve warnings. + before { allow(Warning).to receive(:warn) } + + it 'interprets dash dash character as a character range beginning with -' do + expect(build('a[--c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\--c]\z}, :ignore) end - stub_const('::File::ALT_SEPARATOR', '\\') - silence_warnings do - load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb', __dir__) + it 'interprets character dash dash as a character range ending with -' do + expect(build('a["--]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)["-\-]\z}, :ignore) + end + + it 'interprets dash dash dash as a character range of only with -' do + expect(build('a[---]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\-]\z}, :ignore) + end + + it 'interprets character dash dash dash as a character range of only with " to - with literal -' do + # for some reason this as a regexp literal triggers the warning raise + # and building it with Regexp.new results in a regexp that is identical but not equal + expect(build('a["---]')) + .to be_like PathList::Matcher::PathRegexp.new( + Regexp.new('\\A/a/path/a(?!\/)["-\\-\\-]\\z'), :ignore + ) + end + + it 'interprets dash dash dash character as a character range of only - with literal c' do + expect(build('a[---c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\-c]\z}, :ignore) + end + + it 'interprets character dash dash character as a character range ending with - and a literal c' do + # this could just as easily be interpreted the other way around (" is the literal, --c is the range), + # but ruby regex and git seem to treat this edge case the same + expect(build('a["--c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)["-\-c]\z}, :ignore) + end end - end - after do - allow(File).to receive(:expand_path).with('/').and_call_original + it '^ is not' do + expect(build('a[^a-c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^a-c]\z}, :ignore) + end - silence_warnings do - load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb', __dir__) + # this doesn't appear to be documented anywhere i just stumbled onto it + it '! is also not' do + expect(build('a[!a-c]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^a-c]\z}, :ignore) 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 '[^/] matches everything' do + expect(build('a[^/]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^/]\z}, :ignore) + end - it do - expect(build('D:/foo/bar/')) - .to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore)) - end + it '[^\\] matches everything' do + expect(build('a[^\\]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^\\]\z}, :ignore) + end - it do - expect(build('D:\\foo\\bar/')) - .to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore)) - end + it '[^^] matches everything except literal ^' do + expect(build('a[^^]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^\^]\z}, :ignore) + end - it do - expect(build('D:\\foo/bar/')) - .to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore)) - end + it '[^/a] matches everything except a' do + expect(build('a[^/a]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^/a]\z}, :ignore) + end - it do - expect(build('D:\\foo/bar\\')) - .to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore)) - end + it '[^\\a] matches everything except a' do + expect(build('a[^\\a]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^\\a]\z}, :ignore) + end - it do - expect(build('foo/bar')) - .to be_like PathList::Matcher::ExactString.new('/a/path/foo/bar', :ignore) - end + it '[/^a] matches literal ^ and a' do + expect(build('a[/^a]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[/\^a]\z}, :ignore) + end - it do - expect(build('foo\bar')) - .to be_like PathList::Matcher::ExactString.new('/a/path/foo/bar', :ignore) - end + it '[\\^a] matches literal ^ and a' do + expect(build('a[\\^a]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\\\^a]\z}, :ignore) + end - it do - expect(build('**/foo/bar')) - .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/(?:.*/)?foo/bar\z}o, :ignore) - end + it '[/^] matches literal ^' do + expect(build('a[/^]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[/\^]\z}, :ignore) + end - it do - expect(build('**\\foo\\bar')) - .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/(?:.*/)?foo/bar\z}o, :ignore) - end + it '[\\^] matches literal ^' do + expect(build('a[\\^]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\\\^]\z}, :ignore) + end - it do - expect(build('**\\foo\\bar\\')) - .to be_like PathList::Matcher::MatchIfDir.new( - PathList::Matcher::PathRegexp.new(%r{\A/a/path/(?:.*/)?foo/bar\z}o, :ignore) - ) + it '[`^] matches literal ^' do + expect(build('a[`^]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\^]\z}, :ignore) + end + + it 'later ^ is literal' do + expect(build('a[a-c^]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[a-c\^]\z}, :ignore) + end + + it "doesn't match a slash even if you specify it last" do + expect(build('b[i/]b')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/b(?!/)[i/]b\z}, :ignore) + end + + it "doesn't match a slash even if you specify it alone" do + expect(build('b[/]b')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/b(?!/)[/]b\z}, :ignore) + end + + it 'empty class matches nothing' do + expect(build('b[]b')) + .to eq PathList::Matcher::Blank + end + + it "doesn't match a slash even if you specify it middle" do + expect(build('b[i/a]b')) + .to be_like PathList::Matcher::PathRegexp.new( + %r{\A/a/path/b(?!/)[i/a]b\z}, :ignore + ) + end + + it "doesn't match a backslash even if you specify it middle" do + expect(build('b[i\\a]b')) + .to be_like PathList::Matcher::PathRegexp.new( + %r{\A/a/path/b(?!/)[i\\a]b\z}, :ignore + ) + end + + it "doesn't match a slash even if you specify it start" do + expect(build('b[/ai]b')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/b(?!/)[/ai]b\z}, :ignore) + end + + it "doesn't match a backslash even if you specify it start" do + expect(build('b[\\ai]b')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/b(?!/)[\\ai]b\z}, :ignore) + end + + it 'assumes an unfinished [ matches nothing' do + expect(build('a[')) + .to eq PathList::Matcher::Blank + end + + it 'assumes an unfinished [ followed by \ matches nothing' do + expect(build('a[`')) + .to eq PathList::Matcher::Blank + end + + it 'assumes an escaped [ is literal' do + expect(build('a`[')) + .to be_like PathList::Matcher::ExactString.new('/a/path/a[', :ignore) + end + + it 'assumes an escaped [ is literal inside a group' do + expect(build('a[`[]')) + .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\[]\z}, :ignore) + end + + it 'assumes an unfinished [ matches nothing when negated' do + expect(build('!a[')) + .to eq PathList::Matcher::Blank + end + + it 'assumes an unfinished [bc matches nothing' do + expect(build('a[bc')) + .to eq PathList::Matcher::Blank + end end end describe 'leading ./ means current directory based on the root' do - it { expect(build('./foo')).to be_like PathList::Matcher::ExactString.new("#{FSROOT}a/path/foo", :ignore) } + it { expect(build('./foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) } end describe 'A line starting with # serves as a comment.' do @@ -337,7 +1046,10 @@ def build(pattern) end end - describe '"?" matches any one character except "/"' do + describe( + '"?" matches any one character except "/"', + skip: ('windows behaviour is checked above' if windows?) + ) do it "matches one character at the beginning if there's a ?" do expect(build('?our')) .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/[^/]our\z}, :ignore) @@ -359,7 +1071,10 @@ def build(pattern) end end - describe '"[]" matches one character in a selected range' do + describe( + '"[]" matches one character in a selected range', + skip: ('windows behaviour is checked above' if windows?) + ) do it 'matches a single character in a character class' do expect(build('a[ab]')) .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[ab]\z}, :ignore) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1fe7a3e..f9987aa 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,7 +16,7 @@ def warn(msg) # leftovers:allow end require 'fileutils' -FileUtils.rm_rf(File.join(__dir__, '..', 'coverage')) +FileUtils.rm_rf(File.join(__dir__, '..', 'coverage')) if ENV['COVERAGE'] require 'bundler/setup' diff --git a/spec/support/os_user_helper.rb b/spec/support/os_user_helper.rb new file mode 100644 index 0000000..4534618 --- /dev/null +++ b/spec/support/os_user_helper.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module OSUserHelper + def os_user + @os_user ||= if windows? + ENV.fetch('USERNAME') + else + Etc.getpwuid.name + end + end +end + +RSpec.configure do |config| + config.include OSUserHelper + config.extend OSUserHelper +end