Repository: fakefs/fakefs (Ruby)

Issue:
File.exists? Expands Globs
```
~ >gem list fakefs

*** LOCAL GEMS ***

fakefs (0.6.0)
~ >ruby -rfakefs/safe -e'FakeFS { Dir.mkdir("a"); File.write("a/1.txt","a"); p File.exists?("**/*.txt") }'
true
```

FakeFS::FileSystem.find cannot find files with curly braces in the file name
I believe this is related to #290 (which may still be an issue despite being closed), but it seems that `FakeFS::FileSystem.find` is unable to find files with curly braces in the file name, and perhaps other special characters.  To reproduce:
```
[1] pry(main)> require 'fakefs/safe'
=> true
[2] pry(main)> FakeFS do
[2] pry(main)*   File.open('test_file_with_{curly}_braces', 'w') { |f| f.puts "Can't be found!" }
[2] pry(main)*   File.open('test_file_with_no_braces', 'w') { |f| f.puts "Can be found!" }
[2] pry(main)* end
=> nil
[3] pry(main)> FakeFS::FileSystem.files
=> [(FakeFile name:"test_file_with_{curly}_braces" parent:"/" size:16), (FakeFile name:"test_file_with_no_braces" parent:"/" size:14)]
[4] pry(main)> FakeFS::FileSystem.find('test_file_with_no_braces')
=> (FakeFile name:"test_file_with_no_braces" parent:"/" size:14)
[5] pry(main)> FakeFS::FileSystem.find('test_file_with_{curly}_braces')
=> nil
```
Interestingly, if I use a splat then the file is found!
```
[6] pry(main)> FakeFS::FileSystem.find('test_file_*')
=> [(FakeFile name:"test_file_with_{curly}_braces" parent:"/" size:16), (FakeFile name:"test_file_with_no_braces" parent:"/" size:14)]
```
I'm using MRI Ruby 2.3.1 and FakeFS 0.18
```
[7] pry(main)> `gem list fakefs`
=> "fakefs (0.18.0)\n"
[8] pry(main)> `ruby -v`
=> "ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15]\n"
[9] pry(main)>
```


Discussion:
This seems to be a bug in `FakeFS::FileSystem::find`, which is used by `File::exist?` and many other methods:

``` ruby
require 'fakefs'
Dir.mkdir('a')
FileUtils.touch('a/1.txt')
FakeFS::FileSystem.find('**/*.txt')
# => (FakeFile name:"1.txt" parent:"/a" size:0)
```

The above call to find should return nil.

2.3.0

can you open a PR to fix ?
Hey @grosser I'll look into this

@mikegrissom - what was the mocked method that was causing issues? AFAIK, you shouldn't have to invoke `Fakefs` code directly.
I see the same thing with brackets:
```
require 'fakefs' # v1.2.2
require 'fileutils'

begin
  FakeFS.activate!
  fs1 = FakeFS::FileSystem.add("foo/a")
  fs2 = FakeFS::FileSystem.add("foo/a[1]")
  FileUtils.touch('foo/a/bar.txt')
  FileUtils.touch('foo/a[1]/bar.txt')
  p Dir.glob("foo/a/*")    # => ["/foo/a/bar.txt"]
  p Dir.glob("foo/a[1]/*") # => []
ensure
  FakeFS.deactivate!
end

# Alternatively
p FakeFS::FileSystem.find('foo/a/bar.txt')   # => (FakeFile name:"bar.txt" parent:"/foo/a" size:0)
p FakeFS::FileSystem.find('foo/a[1]/bar.txt') # => nil # oops, yes, this works, I had [a1] before
```

Update: disregard, I had a bug in my sample code. The FakeFS::Filesystem.find call works, though I'm confused by the Dir.glob failure.

Update2: oh, had to escape the brackets properly. Just ignore my whole post. ;)
PR welcome, most likely some simple bug
@grosser Maybe. I'll take a shot at it, I'm guessing it's something in `FakeFS::Globber`.
yep

On Fri, Oct 9, 2020 at 10:09 AM Daniel Berger <notifications@github.com>
wrote:

> @grosser <https://github.com/grosser> Maybe. I'll take a shot at it, I'm
> guessing it's something in FakeFS::Globber.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/fakefs/fakefs/issues/423#issuecomment-706299047>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAACYZ7MVTLCPDQPKFXXXIDSJ47U3ANCNFSM4FWMXD4Q>
> .
>

2.3.0



Patch:
diff --git a/lib/fakefs/dir.rb b/lib/fakefs/dir.rb
index 01e66ea..c3e7960 100644
--- a/lib/fakefs/dir.rb
+++ b/lib/fakefs/dir.rb
@@ -120,7 +120,7 @@ def self.foreach(dirname, &_block)
     def self.glob(pattern, _flags = 0, flags: _flags, base: nil, &block) # rubocop:disable Lint/UnderscorePrefixedVariableName
       pwd = FileSystem.normalize_path(base || Dir.pwd)
       matches_for_pattern = lambda do |matcher|
-        [FileSystem.find(matcher, flags, true, dir: pwd) || []].flatten.map do |e|
+        [FileSystem.find_with_glob(matcher, flags, true, dir: pwd) || []].flatten.map do |e|
           pwd_regex = %r{\A#{pwd.gsub('+') { '\+' }}/?}
           if pwd.match(%r{\A/?\z}) ||
              !e.to_s.match(pwd_regex)
diff --git a/lib/fakefs/file_system.rb b/lib/fakefs/file_system.rb
index 1194f3a..76b91ae 100644
--- a/lib/fakefs/file_system.rb
+++ b/lib/fakefs/file_system.rb
@@ -20,13 +20,22 @@ def files
       fs.entries
     end
 
-    def find(path, find_flags = 0, gave_char_class = false, dir: nil)
+    # Finds files/directories using the exact path, without expanding globs.
+    def find(path, dir: nil)
+      parts = path_parts(normalize_path(path, dir: dir))
+      return fs if parts.empty? # '/'
+
+      find_recurser(fs, parts)
+    end
+
+    # Finds files/directories expanding globs.
+    def find_with_glob(path, find_flags = 0, gave_char_class = false, dir: nil)
       parts = path_parts(normalize_path(path, dir: dir))
       return fs if parts.empty? # '/'
 
       entries = Globber.expand(path).flat_map do |pattern|
         parts = path_parts(normalize_path(pattern, dir: dir))
-        find_recurser(fs, parts, find_flags, gave_char_class).flatten
+        find_with_glob_recurser(fs, parts, find_flags, gave_char_class).flatten
       end
 
       case entries.length
@@ -120,6 +129,18 @@ def current_dir
     private
 
     def find_recurser(dir, parts, find_flags = 0, gave_char_class = false)
+      return nil unless dir.respond_to? :[]
+      head, *parts = parts
+      match = dir.entries.find { |e| e.name == head }
+
+      if parts.empty? # we're done recursing
+        match
+      else
+        find_recurser(match, parts, find_flags, gave_char_class)
+      end
+    end
+
+    def find_with_glob_recurser(dir, parts, find_flags = 0, gave_char_class = false)
       return [] unless dir.respond_to? :[]
       pattern, *parts = parts
       matches =
@@ -149,7 +170,7 @@ def find_recurser(dir, parts, find_flags = 0, gave_char_class = false)
       if parts.empty? # we're done recursing
         matches
       else
-        matches.map { |entry| find_recurser(entry, parts, find_flags, gave_char_class) }
+        matches.map { |entry| find_with_glob_recurser(entry, parts, find_flags, gave_char_class) }
       end
     end
 
diff --git a/lib/fakefs/fileutils.rb b/lib/fakefs/fileutils.rb
index 8f19bd7..4940f6d 100644
--- a/lib/fakefs/fileutils.rb
+++ b/lib/fakefs/fileutils.rb
@@ -232,7 +232,7 @@ def chown_R(user, group, list, _options = {})
       list = Array(list)
       list.each do |file|
         chown(user, group, file)
-        [FileSystem.find("#{file}/**/**")].flatten.each do |f|
+        [FileSystem.find_with_glob("#{file}/**/**")].flatten.each do |f|
           chown(user, group, f.to_s)
         end
       end
@@ -255,7 +255,7 @@ def chmod_R(mode, list, _options = {})
       list = Array(list)
       list.each do |file|
         chmod(mode, file)
-        [FileSystem.find("#{file}/**/**")].flatten.each do |f|
+        [FileSystem.find_with_glob("#{file}/**/**")].flatten.each do |f|
           chmod(mode, f.to_s)
         end
       end


Test patch:
diff --git a/test/fakefs_test.rb b/test/fakefs_test.rb
index 71c68b8..9cf8889 100644
--- a/test/fakefs_test.rb
+++ b/test/fakefs_test.rb
@@ -617,6 +617,27 @@ def test_raises_error_when_cannot_find_file_in_read_write_mode
     end
   end
 
+  def test_globs_in_exist
+    perform_with_both_string_paths_and_pathnames do
+      Dir.mkdir(string_or_pathname("a"))
+      File.write(string_or_pathname("a/1.txt"), "a");
+      assert_equal File.exist?("**/*.txt"), false
+      assert_equal File.exist?("a/1.txt"), true
+
+      # otherwise the second pass of perform_with_both_string_paths_and_pathnames fails with
+      # Errno::EEXIST: File exists - a
+      FakeFS::FileSystem.clear
+    end
+  end
+
+  def test_special_chars_in_paths
+    perform_with_both_string_paths_and_pathnames do
+      path = string_or_pathname("{}.txt")
+      File.write(path, "a")
+      assert_equal File.exist?(path), true
+    end
+  end
+
   def test_creates_files_in_write_only_mode
     perform_with_both_string_paths_and_pathnames do
       File.open(string_or_pathname('foo'), 'w')