Skip to content

Commit c22b18d

Browse files
committed
Use ftype & give windows a glob_gitignore escape
1 parent 223ec20 commit c22b18d

File tree

9 files changed

+150
-102
lines changed

9 files changed

+150
-102
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
strategy:
88
fail-fast: false
99
matrix:
10-
ruby: [2.7, '3.0', 3.1, 3.2, 3.3, ruby-head, jruby-9.4, jruby-head]
10+
ruby: [2.7, '3.0', 3.1, 3.2, ruby-head, jruby-9.4, jruby-head]
1111
platform: [ubuntu, windows, macos]
1212
continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
1313
runs-on: ${{matrix.platform}}-latest

.spellr_wordlists/english.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ tsx
137137
ttributes
138138
txt
139139
unanchorable
140+
unc
140141
unexpandable
141142
unfuck
142143
unnegated

bin/benchmark

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,16 @@ benchmark('add-trailing-slash') do
268268
@string_trailing_slash = 'D:/'
269269
@string_no_trailing_slash = '/bin'
270270

271+
raise unless "/str" == (@string_slash.end_with?('/') ? @string_slash : "#{@string_slash}/") + "str"
272+
raise unless "D:/str" == (@string_trailing_slash.end_with?('/') ? @string_trailing_slash : "#{@string_trailing_slash}/") + "str"
273+
raise unless "/bin/str" == (@string_no_trailing_slash.end_with?('/') ? @string_no_trailing_slash : "#{@string_no_trailing_slash}/") + "str"
274+
raise unless "/str" == ::File.join(@string_slash, "str")
275+
raise unless "D:/str" == ::File.join(@string_trailing_slash, "str")
276+
raise unless "/bin/str" == ::File.join(@string_no_trailing_slash, "str")
277+
raise unless "/str" == @string_slash.sub(/(?<!\/)\z/, '/') + "str"
278+
raise unless "D:/str" == @string_trailing_slash.sub(/(?<!\/)\z/, '/') + "str"
279+
raise unless "/bin/str" == @string_no_trailing_slash.sub(/(?<!\/)\z/, '/') + "str"
280+
271281
x.report(:string_slash_end_with) { (@string_slash.end_with?('/') ? @string_slash : "#{@string_slash}/") + "str" }
272282
x.report(:string_trailing_slash_end_with) { (@string_trailing_slash.end_with?('/') ? @string_trailing_slash : "#{@string_trailing_slash}/") + "str" }
273283
x.report(:string_no_trailing_slash_end_with) { (@string_no_trailing_slash.end_with?('/') ? @string_no_trailing_slash : "#{@string_no_trailing_slash}/") + "str" }
@@ -276,6 +286,10 @@ benchmark('add-trailing-slash') do
276286
x.report(:string_trailing_slash_file_join) { ::File.join(@string_trailing_slash, "str") }
277287
x.report(:string_no_trailing_slash_file_join) { ::File.join(@string_no_trailing_slash, "str") }
278288

289+
x.report(:string_slash_sub) { @string_slash.sub(/(?<!\/)\z/, '/') + "str" }
290+
x.report(:string_trailing_slash_sub) { @string_trailing_slash.sub(/(?<!\/)\z/, '/') + "str" }
291+
x.report(:string_no_trailing_slash_sub) { @string_no_trailing_slash.sub(/(?<!\/)\z/, '/') + "str" }
292+
279293
x.compare!
280294
end
281295
end

lib/path_list/candidate.rb

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def initialize(full_path, directory = nil, shebang = nil)
1818

1919
@child_candidates = nil
2020
@children = nil
21+
@ftype = nil
2122
end
2223

2324
# @return [String] full path downcased
@@ -54,33 +55,16 @@ def children
5455
end
5556
end
5657

57-
# :nocov:
58-
if ::RUBY_PLATFORM == 'java' && ::RbConfig::CONFIG['host_os'].match?(/mswin|mingw/)
59-
# @return [Boolean] whether this path is a directory (false for symlinks to directories)
60-
def directory?
61-
return @directory unless @directory.nil?
62-
63-
@directory = if ::File.symlink?(@full_path)
64-
warn 'Symlink lstat'
65-
warn lstat.inspect
66-
false
67-
else
68-
lstat&.directory? || false
69-
end
70-
end
71-
# :nocov:
72-
else
73-
# @return [Boolean] whether this path is a directory (false for symlinks to directories)
74-
def directory?
75-
return @directory unless @directory.nil?
58+
# @return [Boolean] whether this path is a directory (false for symlinks to directories)
59+
def directory?
60+
return @directory unless @directory.nil?
7661

77-
@directory = lstat&.directory? || false
78-
end
62+
@directory = ftype == 'directory'
7963
end
8064

8165
# @return [Boolean] whether this path exists
8266
def exists?
83-
lstat ? true : false
67+
ftype != 'error'
8468
end
8569

8670
alias_method :original_inspect, :inspect # leftovers:keep
@@ -112,7 +96,7 @@ def shebang
11296
''
11397
end
11498
rescue ::IOError, ::SystemCallError
115-
@lstat ||= nil
99+
@ftype ||= 'error'
116100
''
117101
ensure
118102
file&.close
@@ -121,12 +105,29 @@ def shebang
121105

122106
private
123107

124-
def lstat
125-
return @lstat if defined?(@lstat)
108+
# :nocov:
109+
# https://github.com/jruby/jruby/issues/8018
110+
# ftype follows symlinks on jruby on windows.
111+
if ::RUBY_PLATFORM == 'java' && ::RbConfig::CONFIG['host_os'].match?(/mswin|mingw/)
112+
refine ::File do
113+
# :nodoc:
114+
def ftype(path)
115+
if ::File.symlink?(path)
116+
'link'
117+
else
118+
super
119+
end
120+
end
121+
end
122+
end
123+
# :nocov:
124+
125+
def ftype
126+
return @ftype if @ftype
126127

127-
@lstat = ::File.lstat(@full_path)
128+
@ftype = ::File.ftype(@full_path)
128129
rescue ::SystemCallError
129-
@lstat = nil
130+
@ftype = 'error'
130131
end
131132
end
132133
end

lib/path_list/pattern_parser/glob_gitignore.rb

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ class PatternParser
2020
# - Patterns containing with `/../` are resolved relative to the `root:` directory
2121
# - Patterns beginning with `*` (or `!*`) will match any descendant of the `root:` directory
2222
# - Other patterns match children (not descendants) of the `root:` directory
23-
# - Additionally, on windows
24-
# - \ is treated as a path separator, not an escape character.
25-
# There is no cross-platform escape character when using :glob_gitignore format.
26-
# - Patterns beginning with `c:/`, `d:\`, or `!c:/`, or etc are absolute.
23+
# - Additionally, on windows:
24+
# - either / or \ (slash or backslash) can be used as path separators.
25+
# - therefore \ (backslash) isn't available to be used as an escape character
26+
# - instead ` (grave accent) is used as an escape character
27+
# - patterns beginning with `c:/`, `d:\`, or `!c:/`, or etc are absolute.
28+
# - a path beginning with / or \ is a shortcut for the current working directory drive.
29+
# - there is no cross platform escape character, this is intended to match the current shell
2730
# @example
2831
# PathList.only(ARGV, format: :glob_gitignore)
2932
# PathList.only(
@@ -51,19 +54,34 @@ class GlobGitignore < Gitignore
5154
def initialize(pattern, polarity, root)
5255
pattern = +'' if pattern.start_with?('#')
5356
negated_sigil = '!' if pattern.delete_prefix!('!')
57+
pattern = normalize_slash(pattern)
5458
if pattern.start_with?('*')
55-
pattern = "#{negated_sigil}#{pattern.tr(::File::ALT_SEPARATOR.to_s, ::File::SEPARATOR)}"
59+
pattern = "#{negated_sigil}#{pattern}"
5660
elsif pattern.match?(EXPANDABLE_PATH)
57-
dir_only! if pattern.match?(%r{[/\\]\s*\z}) # expand_path will remove it
61+
dir_only! if pattern.match?(%r{/\s*\z}) # expand_path will remove it
5862

5963
pattern = "#{negated_sigil}#{CanonicalPath.full_path_from(pattern, root)}"
6064
root = nil
6165
@anchored = true
6266
else
63-
pattern = "#{negated_sigil}/#{pattern.tr(::File::ALT_SEPARATOR.to_s, ::File::SEPARATOR)}"
67+
pattern = "#{negated_sigil}/#{pattern}"
6468
end
6569

66-
super(pattern, polarity, root)
70+
super(normalize_escape(pattern), polarity, root)
71+
end
72+
73+
private
74+
75+
def normalize_slash(pattern)
76+
return pattern unless ::File::ALT_SEPARATOR
77+
78+
pattern.tr('\\', '/')
79+
end
80+
81+
def normalize_escape(pattern)
82+
return pattern unless ::File::ALT_SEPARATOR
83+
84+
pattern.tr('`', '\\')
6785
end
6886
end
6987
end

lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,28 @@
33
class PathList
44
class PatternParser
55
class GlobGitignore
6-
EXPANDABLE_PATH = if File.expand_path('/') == '/'
7-
%r{(?:\A(?:[~/]|\.{1,2}(?:/|\z))|(?:[^\\]|\A)(?:\\{2})*/\.\./)}
8-
# :nocov:
9-
else
10-
# this isn't actually nocov, but it's cov is because i reload the file
11-
%r{(?:\A(?:[~/\\]|[a-zA-Z]:[/\\]|[/\\]{2}|\.{1,2}(?:[/\\]|\z))|[\\/]\.\.[/\\])}
12-
# :nocov:
13-
end
6+
# :nocov:
7+
# this isn't actually nocov, but it's cov is because i reload the file
8+
EXPANDABLE_PATH = %r{(?:
9+
\A(?:
10+
[~/] # start with slash or tilde
11+
|
12+
\.{1,2}(?:/|\z) # start with dot or dot dot followed by slash or nothing
13+
#{
14+
if ::File.expand_path('/') != '/' # only if drive letters are applicable
15+
"
16+
|
17+
[a-zA-Z]:/ # drive letter
18+
|
19+
// # UNC path
20+
"
21+
end
22+
}
23+
)
24+
|
25+
(?:[^\\]|\A)(?:\\{2})*/\.\./) # unescaped slash dot dot slash
26+
}x.freeze
27+
# :nocov:
1428
end
1529
end
1630
end

spec/candidate_spec.rb

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525
end
2626

2727
describe '#parent' do
28-
before { allow(File).to receive_messages(exist?: nil, lstat: nil, directory?: nil) }
28+
before { allow(File).to receive_messages(exist?: nil, ftype: nil, directory?: nil) }
2929

3030
it 'returns a candidate for the parent with preset directory value' do
3131
expect(candidate.parent).to be_like described_class.new('/path/from/root', true)
3232
expect(candidate.parent).to have_attributes(
3333
directory?: true
3434
)
3535
expect(File).not_to have_received(:directory?)
36-
expect(File).not_to have_received(:lstat)
36+
expect(File).not_to have_received(:ftype)
3737
end
3838

3939
context 'when the path is /' do
@@ -54,35 +54,35 @@
5454
before { create_file_list 'foo' }
5555

5656
it 'is memoized when true' do
57-
allow(File).to receive(:lstat).and_call_original
57+
allow(File).to receive(:ftype).and_call_original
5858

5959
expect(candidate.exists?).to be true
60-
expect(File).to have_received(:lstat).once
60+
expect(File).to have_received(:ftype).once
6161
expect(candidate.exists?).to be true
62-
expect(File).to have_received(:lstat).once
62+
expect(File).to have_received(:ftype).once
6363
end
6464
end
6565

6666
context 'when the file does not exist' do
6767
let(:full_path) { './foo' }
6868

6969
it 'is memoized when false' do
70-
allow(File).to receive(:lstat).and_call_original
70+
allow(File).to receive(:ftype).and_call_original
7171

7272
expect(candidate.exists?).to be false
73-
expect(File).to have_received(:lstat).with('./foo').once
73+
expect(File).to have_received(:ftype).with('./foo').once
7474
expect(candidate.exists?).to be false
75-
expect(File).to have_received(:lstat).with('./foo').once
75+
expect(File).to have_received(:ftype).with('./foo').once
7676
end
7777

7878
it 'is false when there is an error' do
79-
allow(File).to receive(:lstat).and_call_original
80-
allow(File).to receive(:lstat).with(full_path).and_raise(Errno::EACCES)
79+
allow(File).to receive(:ftype).and_call_original
80+
allow(File).to receive(:ftype).with(full_path).and_raise(Errno::EACCES)
8181

8282
expect(candidate.exists?).to be false
83-
expect(File).to have_received(:lstat).with('./foo').once
83+
expect(File).to have_received(:ftype).with('./foo').once
8484
expect(candidate.exists?).to be false
85-
expect(File).to have_received(:lstat).with('./foo').once
85+
expect(File).to have_received(:ftype).with('./foo').once
8686
end
8787
end
8888
end
@@ -139,9 +139,6 @@
139139
create_symlink('foo' => 'foo_target')
140140

141141
candidate = described_class.new(File.expand_path('foo'))
142-
expect(File.symlink?('./foo')).to be true
143-
expect(File.stat('./foo')).to have_attributes(directory?: false, symlink?: true)
144-
expect(candidate.send(:lstat)).to have_attributes(directory?: false, symlink?: true)
145142
expect(candidate).not_to be_directory
146143
end
147144
end

spec/path_list_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
it 'copes with being given fs root' do
4141
whatever_file_we_get = subject.each('/').first
4242
expect(whatever_file_we_get).not_to start_with('/')
43-
# use lstat because it could be a symlink to nowhere and File.exist? will be sad
44-
expect(File.lstat("/#{whatever_file_we_get}")).to be_a File::Stat
43+
# use symlink? because it could be a symlink to nowhere and File.exist? would return false
44+
expect { File.symlink?("/#{whatever_file_we_get}") || File.exist?("/#{whatever_file_we_get}") }.not_to raise_error
4545
end
4646

4747
it 'copes with being given nonsense root' do

0 commit comments

Comments
 (0)