Search code examples
rubyglob

In Ruby, how can I interpret (expand) a glob relative to a directory?


Wider context: Case-insensitive filename on case sensitive file system

Given the path of a directory (as a string, might be relative to the current working dir or absolute), I'd like to open a specific file. I know the file's filename except for the its case. (It could be TASKDATA.XML, TaskData.xml or even tAsKdAtA.xMl.)

Inspired by the accepted answer to Open a file case-insensitively in Ruby under Linux, I've come up with this little module to produce a glob for matching the file's name:

module Utils
  def self.case_insensitive_glob_string(string)
    string.each_char.map do |c|
      cased = c.upcase != c.downcase
      cased ? "[#{c.upcase}#{c.downcase}]" : c
    end.join
  end
end

For my specific case, I'd call this with

 Utils.case_insensitive_glob_string('taskdata.xml')

and would get

'[Tt][Aa][Ss][Kk][Dd][Aa][Tt][Aa].[Xx][Mm][Ll]'

Specific context: glob relative to a dir ≠ pwd

Now I have to expand the glob, i.e. match it against actual files in the given directory. Unfortunately, Dir.glob(...) doesn't seem have an argument to pass a directory('s path) relative to which the glob should be expanded. Intuitively, it would make sense to me to create a Dir object and have that handle the glob:

d = Dir.new(directory_path)
# => #<Dir:/the/directory>

filename = d.glob(Utils.case_insensitive_glob_string('taskdata.xml')).first() # I wish ...
# NoMethodError: undefined method `glob' for #<Dir:/the/directory>

... but glob only exists as a class method, not as an instance method. (Anybody know why that's true of so many of Dir's methods that would perfectly make sense relative to a specific directory?)

So it looks like I have two options:

  1. Change the current working dir to the given directory

    or

  2. expand the filename's glob in combination with the directory path

The first option is easy: Use Dir.chdir. But because this is in a Gem, and I don't want to mess with the environment of the users of my Gem, I shy away from it. (It's probably somewhat better when used with the block synopsis than manually (or not) resetting the working dir when I'm done.)

The second option looks easy. Simply do

taskdata_xml_name_glob = Utils.case_insensitive_glob_string('taskdata.xml')
taskdata_xml_path_glob = File.join(directory_path, taskdata_xml_name_glob)
filename = Dir.glob(taskdata_xml_path_glob).first()

, right? Almost. When directory_path contains characters that have a special meaning in globs, they will wrongly be expanded, when I only want glob expansion on the filename. This is unlikely, but as the path is provided by the Gem user, I have to account for it, anyway.

Question

Should I escape directory_path before File.joining it with the filename glob? If so, is there a facility to do that or would I have to code the escaping function myself?

Or should I use a different approach (be it chdir, or something yet different)?


Solution

  • If I were implementing that behaviour, I would go with filtering an array, returned by Dir#entries:

    Dir.entries("#{target}").select { |f| f =~ /\A#{filename}\z/i }
    

    Please be aware that on unix platform both . and .. entries will be listed as well, but they are unlikely to be matched on the second step. Also, probably the filename should be escaped with Regexp.escape:

    Dir.entries("#{target}").select { |f| f =~ /\A#{Regexp.escape(filename)}\z/i }