# frozen_string_literal: true # Copyright (C) 2023 Thomas Baron # # This file is part of term_utils. # # term_utils is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, version 3 of the License. # # term_utils is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with term_utils. If not, see . module TermUtils module FF # Represents filesystem Entry. class FinderEntry # @return [Integer] attr_accessor :index # @return [String] attr_accessor :name # Identifies the type of stat. The return string is one of: “file”, # “directory”, “characterSpecial”, “blockSpecial”, “fifo”, “link”, # “socket”, or “unknown”. # @return [String] attr_accessor :kind # Returns the numeric user id of the owner of stat. # @return [Integer] attr_accessor :uid # Returns the numeric group id of the owner of stat. # @return [Integer] attr_accessor :gid # Returns an integer representing the permission bits of stat. The meaning # of the bits is platform dependent. # @return [Integer] attr_accessor :mode # Returns the size of stat in bytes. # @return [Integer] attr_accessor :size # @return [Array] attr_accessor :path_parts # @return [String] attr_accessor :path def initialize @index = nil @name = nil @kind = nil @path_parts = nil @path = nil end def directory? @kind == 'directory' end def file? @kind == 'file' end # @return [Integer] def depth @path_parts.length end end # Represents a Query. class FinderQuery # @return [Integer] attr_accessor :min_depth # @return [Integer, nil] attr_accessor :max_depth # @return [Boolean] attr_accessor :use_stat # @return [String, Array, nil] attr_accessor :entry_kind def initialize @min_depth = 0 @max_depth = nil @use_stat = false @entry_kind = nil @filters = [] end def filter(kind, &block) raise StandardError, 'wrong filter kind' unless %i[enter skip include exclude].include?(kind) raise StandardError, "missing #{kind} block" if block.nil? @filters << { kind: kind, block: block } end # Wether to enter a directory. def enter_directory(&block) filter(:enter, &block) end # Wether not to enter a directory. def skip_directory(&block) filter(:skip, &block) end # Wether to include an entry into the results. def include_entry(&block) filter(:include, &block) end # Wether to exclue an entry from the results. def exclude_entry(&block) filter(:exclude, &block) end def each_filter(&block) @filters.each(&block) end end # Represents the find method engine. class Finder attr_reader :query def initialize(paths) @paths = paths.dup @query = FinderQuery.new end def exec @paths.each_with_object([]) do |path, obj| ctx = { entries: obj, basedir: path } list_start(ctx) end end def list_start(ctx) entry = FinderEntry.new.tap do |e| e.index = ctx[:entries].length e.name = File.basename(ctx[:basedir]) if @query.use_stat st = File.stat(ctx[:basedir]) e.kind = st.ftype e.uid = st.uid e.gid = st.gid e.mode = st.mode e.size = st.size end e.path_parts = [] e.path = ctx[:basedir] end ctx[:entries] << entry if match?(entry) && include?(entry) return unless enter?(entry) list(ctx) end def list(ctx) path_parts = ctx.fetch(:path_parts, []) absolute_path = if path_parts.empty? ctx[:basedir] else "#{ctx[:basedir]}/#{path_parts.join('/')}" end entries = Dir.entries(absolute_path) entries.each do |name| next if %w[. ..].include?(name) entry = FinderEntry.new.tap do |e| e.index = ctx[:entries].length e.name = name if @query.use_stat st = File.stat("#{absolute_path}/#{name}") e.kind = st.ftype e.uid = st.uid e.gid = st.gid e.mode = st.mode e.size = st.size end e.path_parts = path_parts.dup + [name] e.path = "#{ctx[:basedir]}/#{e.path_parts.join('/')}" end ctx[:entries] << entry if match?(entry) && include?(entry) if enter?(entry) list(ctx.merge({ path_parts: entry.path_parts })) end end end def match?(entry) # (1of2) Depth return false if entry.depth < @query.min_depth return false if !@query.max_depth.nil? && entry.depth > @query.max_depth # (2of2) Entry kind. if @query.entry_kind.nil? true elsif @query.entry_kind.is_a?(String) entry.kind == @query.entry_kind elsif @query.entry_kind.is_a?(Array) @query.entry_kind.include?(entry.kind) else false end end def enter?(entry) unless @query.use_stat return File.directory?(entry.path) end return false unless entry.directory? @query.each_filter do |f| case f[:kind] when :enter return true if f[:block].call(entry) when :skip return false if f[:block].call(entry) end end true end def include?(entry) @query.each_filter do |f| case f[:kind] when :include return true if f[:block].call(entry) when :exclude return false if f[:block].call(entry) end end true end end # Finds files. # @param paths [Array] # @return [Array] def self.find(*paths, &block) fdr = if paths.empty? TermUtils::FF::Finder.new(['.']) else TermUtils::FF::Finder.new(paths) end block&.call(fdr.query) fdr.exec end end end