# 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