lib/pa/directory.rb

Summary

Maintainability
C
1 day
Test Coverage
=begin
== ls family
    * Dir[*path] _support globbing_
    * Pa.glob(*path,o),(){} _support globbing with option and block_
    * each(path),(){} each_r(),(){} _support Enumerator. not support globbing_
    * ls(path) ls_r(path) _sample ls. not support globbing._

=== Example    
    tmp/
        filea
        dira/fileb

    ls2("tmp") => ["filea", "dira"]
    ls2_r("tmp") => ["filea", "dira", "dira/fileb"]
    ls2_r("tmp"){|path, rel| rel.count('/')==1} => ["dira/fileb"]

    each("tmp") => Enumerate<Pa>
    each("tmp") {|pa| Pa.rm pa if pa.file?}
    each("tmp").with_object([]){|pa,m| m<<pa.dir} #=> ["tmp", "tmp/dira"]

=end
class Pa
  module Directory
    extend Util::Concern

    module ClassMethods
      # path globbing, exclude '.' '..'
      # @note glob is * ** ? [set] {a,b}
      #
      # @overload glob2(*paths, o={})
      #     @param [String] path
      #     @param [Hash] o option
      #     @option o [Boolean] :dotmatch (false) match dot file.   FNM_DOTMATCH
      #     @option o [Boolean] :pathname (false) wildcard doesn't match /. FNM_PATHNAME
      #     @option o [Boolean] :noescape (false) makes '\\' ordinary. FNM_NOESCAPE
      #     @return [Array<String>] 
      # @overload glob2(*paths, o={})
      #   @yieldparam [String] path
      #   @return [nil]
      def glob2(*args, &blk)
        paths, o = Util.extract_options(args)
        paths.map!{|v|get(v)}
        blk ||= proc { |path| path }

        flag = 0
        o.each do |option, value|
          flag |= File.const_get("FNM_#{option.upcase}") if value
        end

        # delete . .. 
        files = Dir.glob(paths, flag).delete_if{|v| v =~ %r~(^|/)\.\.?$~}

        ret = []
        files.each { |path|
          ret << blk.call(path)
        }

        ret
      end

      def glob(*args, &blk)
        args, o = Util.extract_options(args)
        ret = []
        blk ||= proc { |path| path }

        glob2 *args, o do |path|
          ret << blk.call(Pa(path))
        end

        ret
      end

      def tmpdir2
        Dir.tmpdir
      end

      def tmpdir
        Pa(Dir.tmpdir)
      end

      # is directory empty?
      #
      # @param [String] path
      # @return [Boolean]
      def empty?(path)
        Dir.entries(get(path)).empty? 
      end

      # traverse directory 
      # @note raise Errno::ENOTDIR, Errno::ENOENT  
      #
      # @example
      #   each '.' do |pa|
      #     p pa.path #=> "foo" not "./foo"
      #   end
      #   # => '/home' ..
      #
      #  each('.', error: true).with_object([]) do |(pa,err),m|
      #    ...
      #  end
      #
      # @overload each(path=".", o={})
      #   @param [String,Pa] path
      #   @prarm [Hash] o
      #   @option o [Boolean] :dot (true) include dot file
      #   @option o [Boolean] :backup (true) include backup file
      #   @option o [Boolean] :error (false) yield(pa, err) instead of raise Errno::EPERM when Dir.open(dir)
      #   @option o [Boolean] :file (false) return path and not raise Errno:ENOTDIR if path is a file.
      #   @option o [String,Pa] :base_dir (nil) base directory.
      #   @return [Enumerator<String>]
      # @overload each(path=".", o={}){|path, abs, fname, err, [rea]|}
      #   @yieldparam [String] path
      #   @yieldparam [String] abs absolute path
      #   @yieldparam [String] fname a basename
      #   @yieldparam [String] err error
      #   @yieldparam [String] rea real relative path with o[:base_dir]
      #   @return [nil]
      def each2(*args, &blk) 
        return Pa.to_enum(:each2, *args) unless blk

        (dir,), o = Util.extract_options(args)
        dir = dir ? get(dir) : "."
        o = {dot: true, backup: true}.merge(o)

        rea_dir = o[:base_dir] ? File.join(get(o[:base_dir]), dir) : dir
        raise Errno::ENOENT, "`#{rea_dir}' doesn't exists."  unless File.exists?(rea_dir)

        if not File.directory?(rea_dir) 
          if o[:file]
            rea_path = rea_dir
            blk.call dir, File.absolute_path(rea_path, "."), File.basename(rea_path), nil, rea_path # jruby rbx
            return
          else
            raise Errno::ENOTDIR, "`#{rea_dir}' is not a directoy."
          end
        end

        begin
          d = Dir.open(rea_dir)
        rescue Errno::EPERM => err
        end
        raise err if err and !o[:error]

        while (entry=d.read)
          next if %w(. ..).include? entry
          next if not o[:dot] and entry=~/^\./
          next if not o[:backup] and entry=~/~$/

          path = Util.join_path(dir, entry)
          rea_path = Util.join_path(rea_dir, entry)
          blk.call path, File.absolute_path(rea_path, "."), File.basename(rea_path), err, rea_path # jruby rbx
        end
      end

      def each(*args, &blk)
        return Pa.to_enum(:each, *args) unless blk

        args, o = Util.extract_options(args)
        each2(*args, o) { |path, abs, fname, err, rea|
          blk.call Pa(path), abs, fname, err, rea # jruby need []
        }
      end

      # each with recursive
      # @see each2
      #
      # * each2_r() skip Exception
      # * each2_r(){path, relative, err}
      #
      # @overload each2_r(path=".", o={})
      #   @option o [String, Pa] :base_dir (nil) base directory.
      #   @return [Enumerator<String>]
      # @overload each2_r(path=".", o={})
      #   @yieldparam [String] path
      #   @yieldparam [String] abs
      #   @yieldparam [String] rel/rea relative path
      #   @yieldparam [Errno::ENOENT,Errno::EPERM] err 
      #   @return [nil]
      def each2_r(*args, &blk)
        return Pa.to_enum(:each2_r, *args) if not blk

        (dir,), o = Util.extract_options(args)
        dir ||= "."

        _each2_r(dir, "", o, &blk)
      end

      def each_r(*args, &blk)
        return Pa.to_enum(:each_r, *args) if not blk

        args, o = Util.extract_options(args)
        each2_r *args, o do |path, abs, rel, err, rea|
          blk.call Pa(path, :rel => rel), abs, rel, err, rea # jruby need []
        end
      end

      # list directory contents
      # @see each2
      #
      # block form is a filter.
      #
      # @Example
      #   Pa.ls2(".") {|path, fname| Pa.directory?(path)} # list only directories
      #
      # @overload ls2(*dirs, o={})
      #   @option o [Boolean] :absolute (false) return absolute path instead.
      #   @option o [Boolean] :include (false) return "<path>/foo"
      #     @return [Array<String>]
      # @overload ls2(*dirs, o={}){|path, abs, fname|}
      #   @yieldparam [String] path
      #   @yieldparam [String] abs
      #   @yieldparam [String] fname
      #   @return [Array<String>]
      def ls2(*args, &blk)
        dirs, o = Util.extract_options(args)
        dirs << "." if dirs.empty?
        blk ||= proc { true }

        dirs.each.with_object([]) { |dir, m|
          each2(dir, o) { |path, abs, fname, err, rea|

            view_path = if o[:absolute]
                     abs
                   elsif o[:include]
                     path
                   else
                     fname
                   end

            m << view_path if blk.call(path, abs, fname, err, rea)
          }
        }
      end

      # ls2 with recursive
      # @see ls2
      #
      # @overload ls2_r(*dirs, o={})
      #     @return [Array<String>]
      # @overload ls2_r(*dirs, o={}){|pa, abs, rel, err, [rea]|
      #   @yieldparam [String] path
      #   @yieldparam [String] abs
      #   @yieldparam [String] rel
      #   @yieldparam [Exception] err
      #   @yieldparam [String] rea
      #   @return [Array<String>]
      def ls2_r(*args, &blk)
        dirs, o = Util.extract_options(args)
        dirs << "." if dirs.empty?
        blk ||= proc { true }

        dirs.each.with_object([]) { |dir, m|
          each2_r(dir, o) { |path, abs, rel, err, rea|
            view_path = if o[:absolute]
                     abs
                   elsif o[:include]
                     path
                   else
                     rel
                   end

            m << view_path if blk.call(path, abs, rel, err, rea)
          }
        }
      end

      # @overload ls(*paths, o={})
      #   @params [Array] paths (["."])
      #     @return [Array<Pa>]
      # @overload ls(*paths, o={}){|pa, abs, fname, err, [rea]|}
      #   @yieldparam [Pa] pa
      #   @yieldparam [String] abs
      #   @yieldparam [String] fname
      #   @yieldparam [Exception] err
      #   @yieldparam [String] rea
      #   @return [Array<String>]
      def ls(*args, &blk)
        dirs, o = Util.extract_options(args)
        blk ||= proc { true }
        ret = []

        ls2(*dirs, o) { |path, abs, fname, err, rea|
          view_path = if o[:absolute]
                   abs
                 elsif o[:include]
                   path
                 else
                   fname
                 end

          ret << Pa(view_path) if blk.call(Pa(path), abs, fname, err, rea)
        }

        ret
      end

      def ls_r(*args, &blk)
        args, o = Util.extract_options(args)
        ls2_r(*args, o, &blk)
      end

    private

      # I'm rescurive.
      # @param [String] path
      def _each2_r(path, relative, o, &blk)
        relative = relative == "" ? nil : relative
        o.merge!(error: true)

        Pa.each2(path, o) do |path2, abs, fname, err, rea|
          # fix for File.join with empty string
          rel = File.join(*[relative, File.basename(path2)].compact)
          rea = o[:base_dir] ? File.join(get(o[:base_dir]), rel) : rel

          blk.call path2, abs, rel, err, rea # jruby need []

          if File.directory?(abs)
            _each2_r(path2, rel, o, &blk)
          end
        end
      end
    end

    DELEGATE_METHODS = [:each2, :each, :each2_r, :each_r, :ls2, :ls]

    DELEGATE_METHODS.each { |mth|
      class_eval <<-EOF
        def #{mth}(*args, &blk)
          Pa.#{mth}(path, *args, &blk)
        end
      EOF
    }
  end
end