Module: Cfruby::FileFind

Defined in:
lib/libcfruby/filefind.rb

Defined Under Namespace

Classes: FileExistError, FileFindError, FileMatch

Class Method Summary collapse

Class Method Details

.find(basedir, options = {}, &block) ⇒ Object

Find files that conform to a set of options within a given base directory and yield them to a given block. In addition to all of the options available to FileMatch the following options may be given:

:depth

the maximum depth to recurse into the base directory

:recursive

recurse (default to a depth => 200)

:nonrecursive

don’t recurse at all (equivilant to :depth => 0)

:followsymlinks

if true symlinks are expanded and returned as their realpath, otherwise they are returned as is (default false)

:returnorder

if unset files are returned in traversal order, if set to ‘delete’ files are returned first followed by directories

:filematch

normally internal use only, allows passing in of a FileMatch object (all other file matching options ignored)

Returns the number of files found



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/libcfruby/filefind.rb', line 316

def FileFind.find(basedir, options = {}, &block)
  filematch = nil
  matched = 0

  if(options[:filematch] != nil)
    filematch = options[:filematch]
  else
    # set real options based on helper options
    if((options[:nonrecursive] or options[:recursive]) and options[:depth])
      raise(ArgumentError, ":depth argument is not meaningful with :recursive or :nonrecursive")
    end
    if(options[:nonrecursive])
      options[:depth] = 0
    elsif(options[:recursive])
      options[:depth] = 200 if options[:depth] == nil
      options.delete(:recursive)
    end

    # set basic options if they weren't passed in
    if(options[:glob] == nil)
      Cfruby.controller.inform('debug', "No glob given for search, using '*'")
      options[:glob] = '*'
    end
    if(options[:depth] == nil)
      Cfruby.controller.inform('debug', "No depth given for search, using 0")
      options[:depth] = 0
    end

    # get a filematch object
    filematch = FileMatch.new(options)
  end

  # add the filematch back to options so we can pass it back to
  # ourselves recursively
  options[:filematch] = filematch
  
  # set some options to local variables to prevent a lookup on every file
  maxdepth = options[:depth]

  # set the file return order
  deleteorder = false
  if(options[:returnorder] == 'delete')
    deleteorder = true
  end
  
  # allow people to pass in lists
  if(basedir.respond_to?(:each) and !basedir.kind_of?(String) and !basedir.kind_of?(Pathname))
    basedir.each() { |subdir|
      find(subdir, options) { |filename|
        matched += 1
        yield(filename)
      }
    }
    return(matched)
  else
    # attempt to expand with a glob and expand_path
    dirs = Dir.glob(File.expand_path(basedir.to_s))
    if(dirs.length > 1)
      find(dirs, options) { |filename|
        matched += 1
        yield(filename)
      }
      return(matched)
    elsif(dirs.length == 0)
      # if we didn't get anything out of the glob expansion, then the file doesn't really exist
      raise(FileExistError, "\"#{basedir}\" does not exist")
    else
      basedir = dirs[0]
    end
  end
  
  # convert basedir into an absolute pathname
  if(!basedir.kind_of?(Pathname))
    basedir = Pathname.new(basedir)
  end
  
  if(!basedir.symlink?() and !basedir.exist?())
    raise(FileExistError, "#{basedir.to_s} does not exist")
  end
  
  directories = Array.new()
  visited = Hash.new()
  # if the given basedir is a symlink, only follow it if :followsymlink == true
  if(basedir.symlink? and !options[:followsymlinks])
    if(filematch.matches?(basedir.expand_path))
      matched += 1
      yield(basedir.expand_path)
    end
  else
    basedir = basedir.realpath
    directories << Array.[](basedir, 0)
  end

  yielddirs = Array.new()
  
  while(directories.length > 0)
    currentdir = directories.pop()
    
    # skip this directory if we have already been there
    if(visited[currentdir[0]] != nil)
      next
    else
      visited[currentdir[0]] = true
    end
    
    currentdepth = currentdir[1]
    currentdir = currentdir[0]
    
    # add the directory to the yielddirs list if we need to, or yield it
    # depending on :returnorder
    if(filematch.matches?(currentdir))
      if(deleteorder)
        yielddirs << currentdir
      else
        matched += 1
        yield(currentdir)
      end
    end

    # yield files at this level and recurse if needed.  We use a form of pseudo recursion to avoid
    # exhausting the stack depth.  Directories that match the find are added to a special yielddirs
    # list and are yielded to the caller after the entire recursion has finished.  This ensures that
    # things like recursive delete work properly.
    if(currentdepth < maxdepth)
      Dir.glob("#{currentdir}/*", File::FNM_DOTMATCH) { |filename|
        filename = FileFind.expand_path(Pathname.new(filename), options)

        # skip if it escapes our basedir
        if(filename.relative_path_from(basedir).to_s() =~ /(^|\/)\.\.(\/|$)/)
          next
        end

        # if it is a directory add it to the list for recursion
        if(filename.directory?())
          directories.push(Array.[](filename, currentdepth+1))
        elsif(filematch.matches?(filename))
          Cfruby.controller.inform('debug', "found #{filename}")
          matched += 1
          yield(filename)
        end
      }
    end

  end
  
  while(yielddirs.length > 0)
    matched += 1
    yield(yielddirs.pop)
  end

  return(matched)
end