From 4dafc89efe269b18f486720a26e437ad0751876d Mon Sep 17 00:00:00 2001 From: Dana Sherson Date: Sun, 19 Nov 2023 06:39:48 +1300 Subject: [PATCH] Use ftype & give windows a glob_gitignore escape --- .github/workflows/ci.yml | 2 +- .spellr_wordlists/english.txt | 1 + bin/benchmark | 14 +++ lib/path_list/candidate.rb | 55 ++++++------ .../pattern_parser/glob_gitignore.rb | 34 ++++++-- .../glob_gitignore/expandable_path.rb | 30 +++++-- spec/candidate_spec.rb | 27 +++--- spec/path_list_spec.rb | 4 +- spec/pattern_parser/glob_gitignore_spec.rb | 85 ++++++++++--------- 9 files changed, 150 insertions(+), 102 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6ff45f..3539cac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [2.7, '3.0', 3.1, 3.2, 3.3, ruby-head, jruby-9.4, jruby-head] + ruby: [2.7, '3.0', 3.1, 3.2, ruby-head, jruby-9.4, jruby-head] platform: [ubuntu, windows, macos] continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} runs-on: ${{matrix.platform}}-latest diff --git a/.spellr_wordlists/english.txt b/.spellr_wordlists/english.txt index 74a6b2a..4ba3629 100644 --- a/.spellr_wordlists/english.txt +++ b/.spellr_wordlists/english.txt @@ -137,6 +137,7 @@ tsx ttributes txt unanchorable +unc unexpandable unfuck unnegated diff --git a/bin/benchmark b/bin/benchmark index 8765dcd..392b1a1 100755 --- a/bin/benchmark +++ b/bin/benchmark @@ -268,6 +268,16 @@ benchmark('add-trailing-slash') do @string_trailing_slash = 'D:/' @string_no_trailing_slash = '/bin' + raise unless "/str" == (@string_slash.end_with?('/') ? @string_slash : "#{@string_slash}/") + "str" + raise unless "D:/str" == (@string_trailing_slash.end_with?('/') ? @string_trailing_slash : "#{@string_trailing_slash}/") + "str" + raise unless "/bin/str" == (@string_no_trailing_slash.end_with?('/') ? @string_no_trailing_slash : "#{@string_no_trailing_slash}/") + "str" + raise unless "/str" == ::File.join(@string_slash, "str") + raise unless "D:/str" == ::File.join(@string_trailing_slash, "str") + raise unless "/bin/str" == ::File.join(@string_no_trailing_slash, "str") + raise unless "/str" == @string_slash.sub(/(? 'foo_target') candidate = described_class.new(File.expand_path('foo')) - expect(File.symlink?('./foo')).to be true - expect(File.stat('./foo')).to have_attributes(directory?: false, symlink?: true) - expect(candidate.send(:lstat)).to have_attributes(directory?: false, symlink?: true) expect(candidate).not_to be_directory end end diff --git a/spec/path_list_spec.rb b/spec/path_list_spec.rb index b1f6252..5e15bba 100644 --- a/spec/path_list_spec.rb +++ b/spec/path_list_spec.rb @@ -40,8 +40,8 @@ it 'copes with being given fs root' do whatever_file_we_get = subject.each('/').first expect(whatever_file_we_get).not_to start_with('/') - # use lstat because it could be a symlink to nowhere and File.exist? will be sad - expect(File.lstat("/#{whatever_file_we_get}")).to be_a File::Stat + # use symlink? because it could be a symlink to nowhere and File.exist? would return false + expect { File.symlink?("/#{whatever_file_we_get}") || File.exist?("/#{whatever_file_we_get}") }.not_to raise_error end it 'copes with being given nonsense root' do diff --git a/spec/pattern_parser/glob_gitignore_spec.rb b/spec/pattern_parser/glob_gitignore_spec.rb index be3ea79..6ff3778 100644 --- a/spec/pattern_parser/glob_gitignore_spec.rb +++ b/spec/pattern_parser/glob_gitignore_spec.rb @@ -11,6 +11,8 @@ let(:polarity) { :ignore } let(:root) { nil } + let(:escape_character) { windows? ? '`' : '\\' } + def build(pattern) described_class .new(+pattern, polarity, root) @@ -35,14 +37,14 @@ def build(pattern) describe 'The windows case' do before do + # use windows expand_path: allow(File).to receive(:expand_path).and_call_original - # use windows regexp: allow(File).to receive(:expand_path).with('/').and_return('D:/') - stub_const('::File::ALT_SEPARATOR', '\\') - - # treat these as absolute allow(File).to receive(:expand_path) - .with(a_string_matching(%r{D:[\\/]foo[\\/]bar[\\/]?}), '/a/path').and_return('D:/foo/bar') + .with(a_string_matching(%r{D:/.*}), '/a/path') do |path, _| + path.tr('\\\\', '/').delete_suffix('/') + end + stub_const('::File::ALT_SEPARATOR', '\\') silence_warnings do load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb', __dir__) @@ -60,6 +62,7 @@ def build(pattern) 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('D:/foo/bar/')) @@ -123,59 +126,59 @@ def build(pattern) .to be_like PathList::Matcher::ExactString.new('/a/path/ #foo', :ignore) end - describe 'Put a backslash ("\") in front of patterns that begin with a hash', skip: windows? do + describe 'Put a backslash ("\") in front of patterns that begin with a hash' do it do - expect(build('\\#foo')) + expect(build("#{escape_character}#foo")) .to be_like PathList::Matcher::ExactString.new('/a/path/#foo', :ignore) end end end - describe 'literal backslashes in filenames', skip: windows? do + describe 'literal backslashes in filenames' do it 'matches an escaped backslash at the end of the pattern' do - expect(build('foo\\\\')) - .to be_like PathList::Matcher::ExactString.new('/a/path/foo\\', :ignore) + expect(build("foo#{escape_character}#{escape_character}")) + .to be_like PathList::Matcher::ExactString.new("/a/path/foo#{escape_character}", :ignore) end it 'never matches a literal backslash at the end of the pattern' do - expect(build('foo\\')) + expect(build("foo#{escape_character}")) .to eq PathList::Matcher::Blank end it 'matches an escaped backslash at the start of the pattern' do - expect(build('\\\\foo')) - .to be_like PathList::Matcher::ExactString.new('/a/path/\\foo', :ignore) + expect(build("#{escape_character}#{escape_character}foo")) + .to be_like PathList::Matcher::ExactString.new("/a/path/#{escape_character}foo", :ignore) end it 'matches a literal escaped f at the start of the pattern' do - expect(build('\\foo')) + expect(build("#{escape_character}foo")) .to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) end end - 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 ("\")' do it 'ignores trailing spaces in the gitignore file' do expect(build('foo ')) .to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) end it "doesn't ignore trailing spaces if there's a backslash" do - expect(build('foo \\ ')) + expect(build("foo #{escape_character} ")) .to be_like PathList::Matcher::ExactString.new('/a/path/foo ', :ignore) end it 'considers trailing backslashes to never be matched' do - expect(build('foo\\')) + expect(build("foo#{escape_character}")) .to eq PathList::Matcher::Blank end it "doesn't ignore trailing spaces if there's a backslash before every space" do - expect(build('foo\\ \\ ')) + expect(build("foo#{escape_character} #{escape_character} ")) .to be_like PathList::Matcher::ExactString.new('/a/path/foo ', :ignore) end it "doesn't ignore just that trailing spaces if there's a backslash before the non last space" do - expect(build('foo\\ ')) + expect(build("foo#{escape_character} ")) .to be_like PathList::Matcher::ExactString.new('/a/path/foo ', :ignore) end end @@ -250,8 +253,8 @@ def build(pattern) describe 'Put a backslash ("\") in front of the first "!" for patterns that begin with a literal "!"' do # for example, "\!important!.txt".' - it 'matches files starting with a literal ! if its preceded by a backslash', skip: windows? do - expect(build('\!important!.txt')) + it 'matches files starting with a literal ! if its preceded by a backslash' do + expect(build("#{escape_character}!important!.txt")) .to be_like PathList::Matcher::ExactString.new('/a/path/!important!.txt', :ignore) end end @@ -377,33 +380,33 @@ def build(pattern) .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', skip: windows? do - expect(build('a[\\]-\\[]')) + it 'treats escaped backward character class range as the first character of the range' do + expect(build("a[#{escape_character}]-#{escape_character}[]")) .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', skip: windows? do - expect(build('a[^\\]-\\[]')) + it 'treats negated escaped backward character class range as the first char of range' do + expect(build("a[^#{escape_character}]-#{escape_character}[]")) .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', skip: windows? do - expect(build('a[\\[-\\]]')) + it 'treats a escaped character class range as as a range' do + expect(build("a[#{escape_character}[-#{escape_character}]]")) .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', skip: windows? do - expect(build('a[^\\[-\\]]')) + it 'treats a negated escaped character class range as a range' do + expect(build("a[^#{escape_character}[-#{escape_character}]]")) .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', skip: windows? do - expect(build('a[\\a-\\c]')) + it 'treats an unnecessarily escaped character class range as a range' do + expect(build("a[#{escape_character}a-#{escape_character}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', skip: windows? do - expect(build('a[^\\a-\\c]')) + it 'treats a negated unnecessarily escaped character class range as a range' do + expect(build("a[^#{escape_character}a-#{escape_character}c]")) .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[^a-c]\z}, :ignore) end @@ -546,8 +549,8 @@ def build(pattern) .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[/\^]\z}, :ignore) end - it '[\\^] matches literal ^', skip: windows? do - expect(build('a[\^]')) + it '[\\^] matches literal ^' do + expect(build("a[#{escape_character}^]")) .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\^]\z}, :ignore) end @@ -588,18 +591,18 @@ def build(pattern) .to eq PathList::Matcher::Blank end - it 'assumes an unfinished [ followed by \ matches nothing', skip: windows? do - expect(build('a[\\')) + it 'assumes an unfinished [ followed by \ matches nothing' do + expect(build("a[#{escape_character}")) .to eq PathList::Matcher::Blank end - it 'assumes an escaped [ is literal', skip: windows? do - expect(build('a\\[')) + it 'assumes an escaped [ is literal' do + expect(build("a#{escape_character}[")) .to be_like PathList::Matcher::ExactString.new('/a/path/a[', :ignore) end - it 'assumes an escaped [ is literal inside a group', skip: windows? do - expect(build('a[\\[]')) + it 'assumes an escaped [ is literal inside a group' do + expect(build("a[#{escape_character}[]")) .to be_like PathList::Matcher::PathRegexp.new(%r{\A/a/path/a(?!/)[\[]\z}, :ignore) end