require 'date' require 'rbconfig' require 'sys/admin' include Config include Sys class File::Find # The version of this library VERSION = '0.2.2' # :stopdoc: VALID_OPTIONS = %w/ atime ctime follow ftype inum group name pattern path perm prune size user / # :startdoc: # The starting path(s) for the search. The default is the current directory. # This can be a single path or an array of paths. # attr_accessor :path # The list of options passed to the constructor and/or used by the # File::Find#find method. # attr_accessor :options # Limits searches by file access time, where the value you supply is the # number of days back from the time that the File::Find#find method was # called. Note that the File::Find#find method itself alters the access # times. # attr_accessor :atime # Limits searches by file change time, where the value you supply is the # number of days back from the time that the File::Find#find method was # called. # attr_accessor :ctime # Limits searches to files that belong to a specific group, where the # group can be either a group name or ID. # # Not currently supported on MS Windows. # attr_accessor :group # Controls the behavior of how symlinks are followed. If set to true, then # follows the file pointed to. If false, it considers the symlink itself. # attr_accessor :follow # Limits searches to specific types of files. The possible values here are # those returned by the File.ftype method. # attr_accessor :ftype # Limits search to a file with a specific inode number. Ignored on MS # Windows. # attr_accessor :inum # The name pattern used to limit file searches. The patterns that are legal # for Dir.glob are legal here. The default is '*', i.e. everything. # attr_accessor :name # Limits searches to files which have permissions that match the octal # value that you provide. For purposes of this comparison, only the user, # group, and world settings are used. Do not use a leading 0 in the values # that you supply, e.g. use 755 not 0755. # # Not currently supported on MS Windows. # attr_accessor :perm # Skips files or directories that match the string provided as an argument. # attr_accessor :prune # If the value passed is an integer, this option limits searches to files # that match the size, in bytes, exactly. If a string is passed, you can # use the standard comparable operators to match files, e.g. ">= 200" would # limit searches to files greater than or equal to 200 bytes. # attr_accessor :size # Limits searches to files that belong to a specific user, where the user # can be either a user name or an ID. # # Not currently supported on MS Windows. # attr_accessor :user # The file that matched previously in the current search. # attr_reader :previous alias pattern name alias pattern= name= # Creates and returns a new File::Find object. The options set for this # object serve as the rules for determining what files the File::Find#find # method will search for. # # Example: # # rule = File::Find.new( # :name => "*.rb", # :follow => false, # :path => ['/usr/local/lib', '/opt/local/lib'] # ) # def initialize(options = {}) @options = options @atime = nil @ctime = nil @ftype = nil @group = nil @follow = true @inum = nil @perm = nil @prune = nil @size = nil @user = nil validate_and_set_options(options) unless options.empty? @previous = nil @path ||= Dir.pwd @name ||= '*' end # Executes the find based on the rules you set for the File::Find object. # In block form, yields each file in turn that matches the specified rules. # In non-block form it will return an array of matches instead. # # Example: # # rule = File::Find.new( # :name => "*.rb", # :follow => false, # :path => ['/usr/local/lib', '/opt/local/lib'] # ) # # rule.find{ |f| # puts f # } # def find results = [] unless block_given? paths = @path.to_a if @prune prune_regex = Regexp.new(@prune) else prune_regex = nil end paths.each{ |path| begin Dir.foreach(path){ |file| next if file == '.' next if file == '..' if prune_regex next if prune_regex.match(file) end orig = file.dup file = File.join(path, file) stat_method = @follow ? :lstat : :stat # Skip files we cannot access, stale links, etc. begin stat_info = File.send(stat_method, file) rescue Errno::ENOENT, Errno::EACCES next rescue Errno::ELOOP stat_method = :lstat # Handle recursive symlinks retry end glob = File.join(File.dirname(file), @name) # Add directories back onto the list of paths to search unless # they've already been added. # # TODO: Add max_depth support here # if stat_info.directory? unless paths.include?(file) paths << file end end # Dir[] doesn't like backslashes if File::ALT_SEPARATOR file.tr!(File::ALT_SEPARATOR, File::SEPARATOR) glob.tr!(File::ALT_SEPARATOR, File::SEPARATOR) end next unless Dir[glob].include?(file) if @atime date1 = Date.parse(Time.now.to_s) date2 = Date.parse(stat_info.atime.to_s) next unless (date1 - date2).numerator == @atime end if @ctime date1 = Date.parse(Time.now.to_s) date2 = Date.parse(stat_info.ctime.to_s) next unless (date1 - date2).numerator == @ctime end if @ftype next unless File.ftype(file) == @ftype end if @group if @group.is_a?(String) next unless Admin.get_group(stat_info.gid).name == @group else next unless stat_info.gid == @group end end unless CONFIG['host_os'] =~ /windows|mswin/i if @inum next unless stat_info.ino == @inum end end # This currently doesn't work on MS Windows, even in limited # fashion for 0666 and 0664, because File.stat.mode doesn't # return the proper value. # if @perm next unless sprintf("%o", stat_info.mode & 07777) == @perm.to_s end # Allow plain numbers, or strings for comparison operators. if @size if @size.is_a?(String) regex = /^([><=]+)\s*?(\d+)$/ match = regex.match(@size) if match.nil? || match.captures.include?(nil) raise ArgumentError, "invalid size string: '#{@size}'" end operator = match.captures.first.strip number = match.captures.last.strip.to_i next unless stat_info.size.send(operator, number) else next unless stat_info.size == @size end end if @user if @user.is_a?(String) next unless Admin.get_user(stat_info.uid).name == @user else next unless stat_info.uid == @user end end if block_given? yield file else results << file end @previous = file unless @previous == file } rescue Errno::EACCES next # Skip inaccessible directories end } block_given? ? nil : results end private # This validates that the keys are valid. If they are, it sets the value # of that key's corresponding method to the given value. # def validate_and_set_options(options) options.each do |key, value| key = key.to_s.downcase unless VALID_OPTIONS.include?(key) raise ArgumentError, "invalid option '#{key}'" end send("#{key}=", value) end end end