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