class FunWith::Files::FilePath

Constants

DEFAULT_TIMESTAMP_FORMAT
SUCC_DIGIT_COUNT

Attributes

path[RW]

Public Class Methods

new( *args ) click to toggle source
Calls superclass method
# File lib/fun_with/files/file_path.rb, line 7
def initialize( *args )
  super( File.join( *args ) )
end
tmpdir( ) { |fwf_filepath| ... } click to toggle source

If block given, temporary directory is deleted at the end of the block, and the value given by the block is returned.

If no block given, the path to the temp directory is returned as a FilePath. Don't forget to delete it when you're done.

# File lib/fun_with/files/file_path.rb, line 18
def self.tmpdir( &block )
  if block_given?
    Dir.mktmpdir do |d|
      yield d.fwf_filepath
    end
  else
    Dir.mktmpdir.fwf_filepath
  end
end
tmpfile( ext = :tmp ) { |join.touch| ... } click to toggle source

The file is created within a temporary directory

# File lib/fun_with/files/file_path.rb, line 29
def self.tmpfile( ext = :tmp, &block )
  filename = rand( 2 ** 64 ).to_s(16).fwf_filepath.ext( ext )
  
  if block_given?
    self.tmpdir do |tmp|
      yield tmp.join( filename ).touch
    end
  else
    self.tmpdir.join( filename ).touch
  end
end

Public Instance Methods

/(arg) click to toggle source
# File lib/fun_with/files/file_path.rb, line 52
def / arg
  self.join( arg )
end
<<( content = nil, &block )
Alias for: append
[](*args) click to toggle source
# File lib/fun_with/files/file_path.rb, line 56
def [] *args
  self.join(*args)
end
absent?()
Alias for: doesnt_exist?
append( content = nil ) { |f| ... } click to toggle source
# File lib/fun_with/files/file_path.rb, line 280
def append( content = nil, &block )
  File.open( self, "a" ) do |f|
    f << content if content
    if block_given?
      yield f
    end
  end
end
Also aliased as: <<
ascend( ) { |path| ... } click to toggle source
# File lib/fun_with/files/file_path.rb, line 648
def ascend( &block )
  path = self.clone

  if path.root?
    yield path
  else
    yield self
    self.up.ascend( &block )
  end
end
basename_and_ext() click to toggle source

base, ext = @path.basename_and_ext

# File lib/fun_with/files/file_path.rb, line 420
def basename_and_ext
  [self.basename_no_ext, self.ext]
end
basename_no_ext() click to toggle source

Does not return a filepath

TODO: Why not?

# File lib/fun_with/files/file_path.rb, line 321
def basename_no_ext
  self.basename.to_s.split(".")[0..-2].join(".")
end
descend( ) { |path| ... } click to toggle source
# File lib/fun_with/files/file_path.rb, line 637
def descend( &block )
  path = self.clone

  if path.root?
    yield path
  else
    self.up.descend( &block )
    yield self
  end
end
directory() click to toggle source

if it's a file, returns the immediate parent directory. if it's not a file, returns itself

# File lib/fun_with/files/file_path.rb, line 437
def directory
  self.directory? ? self : self.dirname
end
dirname_and_basename() click to toggle source
# File lib/fun_with/files/file_path.rb, line 426
def dirname_and_basename
  warn("FilePath#dirname_and_basename() is deprecated.  Pathname#split() already existed, and should be used instead.")
  [self.dirname, self.basename]
end
dirname_and_basename_and_ext() click to toggle source
# File lib/fun_with/files/file_path.rb, line 431
def dirname_and_basename_and_ext
  [self.dirname, self.basename_no_ext, self.ext]
end
doesnt_exist?() click to toggle source
# File lib/fun_with/files/file_path.rb, line 61
def doesnt_exist?
  self.exist? == false
end
Also aliased as: absent?
down( *args, &block )
Alias for: join
empty?() click to toggle source

empty? has different meanings depending on whether you're talking about a file or a directory. A directory must not have any files or subdirectories. A file must not have any data in it.

# File lib/fun_with/files/file_path.rb, line 308
def empty?
  raise Exceptions::FileDoesNotExist unless self.exist?

  if self.file?
    File.size( self ) == 0
  elsif self.directory?
    self.glob( :all ).fwf_blank?
  end
end
entries() click to toggle source
# File lib/fun_with/files/file_path.rb, line 196
def entries
  self.glob( :recurse => false )
end
expand() click to toggle source
# File lib/fun_with/files/file_path.rb, line 200
def expand
  self.class.new( File.expand_path( self ) )
end
ext( *args ) click to toggle source

Two separate modes. With no arguments given, returns the current extension as a string (not a filepath) With an argument, returns the path with a .(arg) tacked onto the end. The leading period is wholly optional. Does not return a filepath. Does not include leading period

# File lib/fun_with/files/file_path.rb, line 403
def ext( *args )
  if args.length == 0
    split_basename = self.basename.to_s.split(".")
    split_basename.length > 1 ? split_basename.last : ""
  else
    
    append_to_path = args.compact.map{ |ex|
      ex.to_s.gsub( /^\./, '' )
    }.compact.join( "." )
    
    appended_to_path = "." + "#{append_to_path}" unless append_to_path.fwf_blank?
    
    self.class.new( "#{@path}#{appended_to_path}" )
  end
end
fwf_filepath() click to toggle source
# File lib/fun_with/files/file_path.rb, line 455
def fwf_filepath
  self
end
glob( *args ) { |file| ... } click to toggle source

opts:

:flags  =>  File::FNM_CASEFOLD
            File::FNM_DOTMATCH
            File::FNM_NOESCAPE
            File::FNM_PATHNAME
            File::FNM_SYSCASE
            See Dir documentation for details.
              Can be given as an integer: (File::FNM_DOTMATCH | File::FNM_NOESCAPE)
              or as an array: [File::FNM_CASEFOLD, File::FNM_DOTMATCH]

:class  =>  [self.class] The class of objects you want returned (String, FilePath, etc.)
            Should probably be a subclass of FilePath or String.  Class.initialize() must accept a string
            [representing a file path] as the sole argument.

:recurse => [defaults true]
:recursive (synonym for :recurse)

:ext => []  A single symbol, or a list containing strings/symbols representing file name extensions.
            No leading periods kthxbai.
:sensitive => true : do a case sensitive search.  I guess the default is an insensitive search, so
                     the default behaves similarly on Windows and Unix.  Not gonna fight it.

:dots => true      : include dotfiles.  Does not include . and ..s unless you also
                     specify the option :parent_and_current => true.

If opts[:recurse] / opts[:ext] not given, the user can get the same
results explicitly with arguments like .glob("**", "*.rb")

:all : if :all is the only argument, this is the same as .glob(“**”, “*”)

Examples: @path.glob( “css”, “*.css” ) # Picks up all css files in the css folder @path.glob( “css”, :ext => :css ) # same @path.glob # Picks up all directories, subdirectories, and files @path.glob(:all) # same. Note: :all cannot be used in conjunction with :ext or any other arguments. Which may be a mistake on my part. @path.glob(“**”, “*”) # same @path.entries # synonym for :all, :recursive => false

TODO: depth argument? depth should override recurse. When extention given, recursion should default to true?

the find -depth argument says depth(0) is the root of the searched directory, any files beneath would be depth(1)
# File lib/fun_with/files/file_path.rb, line 121
def glob( *args, &block )
  args.push( :all ) if args.fwf_blank?
  opts = args.last.is_a?(Hash) ? args.pop : {}

  if args.last == :all
    all_arg_given = true
    args.pop
  else
    all_arg_given = false
  end

  flags = case (flags_given = opts.delete(:flags))
          when NilClass
            0
          when Array      # should be an array of integers or File::FNM_<FLAGNAME>s
            flags_given.inject(0) do |memo, obj|
              memo | obj
            end
          when Integer
            flags_given
          end

  flags |= File::FNM_DOTMATCH if opts[:dots]
  flags |= File::FNM_CASEFOLD if opts[:sensitive]   # case sensitive.  Only applies to Windows.

  recurse = if all_arg_given
              if opts[:recursive] == false || opts[:recurse] == false
                false
              else
                true
              end
            else
              opts[:recursive] == true || opts[:recurse] == true || false
            end

  if all_arg_given
    if recurse
      args = ["**", "*"]
    else
      args = ["*"]
    end
  else
    args.push("**") if recurse

    extensions = case opts[:ext]
    when Symbol, String
      "*.#{opts[:ext]}"
    when Array
      extensions = opts[:ext].map(&:to_s).join(',')
      "*.{#{extensions}}"                            # The Dir.glob format for this is '.{ext1,ext2,ext3}'
    when NilClass
      if args.fwf_blank?
        "*"
      else
        nil
      end
    end

    args.push( extensions ) if extensions
  end

  class_to_return = opts[:class] || self.class

  files = Dir.glob( self.join(*args), flags ).map{ |f| class_to_return.new( f ) }
  files.reject!{ |f| f.basename.to_s.match( /^\.\.?$/ ) } unless opts[:parent_and_current]

  if block_given?
    for file in files
      yield file
    end
  else
    files
  end
end
grep( regex ) { |line| ... } click to toggle source

Returns a [list] of the lines in the file matching the given file. Contrast with

# File lib/fun_with/files/file_path.rb, line 293
def grep( regex, &block )
  return [] unless self.file?
  matching = []
  self.each_line do |line|
    matching.push( line ) if line.match( regex )
    yield line if block_given?
  end


  matching
end
join( *args ) { |joined_path| ... } click to toggle source
Calls superclass method
# File lib/fun_with/files/file_path.rb, line 41
def join( *args, &block )
  joined_path = self.class.new( super( *(args.map(&:to_s) ) ) )
  yield joined_path if block_given?
  joined_path
end
Also aliased as: down
join!( *args, &block ) click to toggle source
# File lib/fun_with/files/file_path.rb, line 47
def join!( *args, &block )
  @path = self.join( *args, &block ).to_str
  self
end
load() click to toggle source

TODO: succ_last : find the last existing file of the given sequence. TODO: succ_next : find the first free file of the given sequence

# File lib/fun_with/files/file_path.rb, line 580
def load
  if self.directory?
    self.glob( :recursive => true, :ext => "rb" ).map(&:load)
  else
    Kernel.load( self.expand )
  end
end
not_a_file?() click to toggle source
# File lib/fun_with/files/file_path.rb, line 65
def not_a_file?
  ! self.file?
end
original() click to toggle source
# File lib/fun_with/files/file_path.rb, line 445
def original
  self.symlink? ? self.readlink.original : self
end
original?() click to toggle source
# File lib/fun_with/files/file_path.rb, line 441
def original?
  !self.symlink?
end
relative_path_from( dir ) click to toggle source

Basically Pathname.relative_path_from, but you can pass in strings

Calls superclass method
# File lib/fun_with/files/file_path.rb, line 450
def relative_path_from( dir )
  dir = super( Pathname.new( dir ) )
  self.class.new( dir )
end
requir() click to toggle source

Require ALL THE RUBY! This may be a bad idea…

Sometimes it fails to require a file because one of the necessary prerequisites hasn't been required yet (NameError). requir catches this failure and stores the failed requirement in order to try it later. Doesn't fail until it goes through a full loop where none of the required files were successful.

# File lib/fun_with/files/file_path.rb, line 595
def requir
  if self.directory?
    requirements = self.glob( :recursive => true, :ext => "rb" )
    successfully_required = 1337  # need to break into initial loop
    failed_requirements = []
    error_messages = []

    while requirements.length > 0 && successfully_required > 0
      successfully_required = 0
      failed_requirements = []
      error_messages = []

      for requirement in requirements
        begin
          requirement.requir
          successfully_required += 1
        rescue Exception => e
          failed_requirements << requirement
          error_messages << "Error while requiring #{requirement} : #{e.message} (#{e.class})"
        end
      end

      requirements = failed_requirements
    end

    if failed_requirements.length > 0
      msg = "requiring directory #{self} failed:\n"
      for message in error_messages
        msg << "\n\terror message: #{message}"
      end

      raise NameError.new(msg)
    end
  else
    require self.expand.gsub( /\.rb$/, '' )
  end
end
root?() click to toggle source
# File lib/fun_with/files/file_path.rb, line 633
def root?
  self == self.up
end
separator() click to toggle source

TODO : Not working as intended. def separator( s = nil )

# If s is nil, then we're asking for the separator
if s.nil?
  @separator || File::SEPARATOR
else
  @separator = s
end
# otherwise we're installing a separator

end

# File lib/fun_with/files/file_path.rb, line 673
def separator
  File::SEPARATOR
end
specifier( str ) click to toggle source

puts a string between the main part of the basename and the extension or after the basename if there is no extension. Used to describe some file variant. Example “/home/docs/my_awesome_screenplay.txt”.fwf_filepath.specifier(“final_draft”)

=> FunWith::Files::FilePath:/home/docs/my_awesome_screenplay.final_draft.txt

Oh hush. I find it useful.

# File lib/fun_with/files/file_path.rb, line 526
def specifier( str )
  str = str.to_s
  chunks = self.to_s.split(".")

  if chunks.length == 1
    chunks << str
  else
    chunks = chunks[0..-2] + [str] + [chunks[-1]]
  end

  chunks.join(".").fwf_filepath
end
succ( opts = { digit_count: SUCC_DIGIT_COUNT, timestamp: false } ) click to toggle source

Gives a sequence of files. Examples: file.dat –> file.000000.dat file_without_ext –> file_without_ext.000000 If it sees a six-digit number at or near the end of the filename, it increments it.

You can change the length of the sequence string by passing in an argument, but it should always be the same value for a given set of files.

TODO: Need to get this relying on the specifier() method.

# File lib/fun_with/files/file_path.rb, line 470
def succ( opts = { digit_count: SUCC_DIGIT_COUNT, timestamp: false } )
  if timestamp = opts[:timestamp]
    timestamp_format = timestamp.is_a?(String) ? timestamp : DEFAULT_TIMESTAMP_FORMAT
    timestamp = Time.now.strftime( timestamp_format )
    digit_count = timestamp.length
  else
    timestamp = false
    digit_count = opts[:digit_count]
  end

  chunks = self.basename.to_s.split(".")
  # not yet sequence stamped, no file extension.
  if chunks.length == 1
    if timestamp
      chunks.push( timestamp )
    else
      chunks.push( "0" * digit_count )
    end
  # sequence stamp before file extension
  elsif match_data = chunks[-2].match( /^(\d{#{digit_count}})$/ )
    if timestamp
      chunks[-2] = timestamp
    else
      i = match_data[1].to_i + 1
      chunks[-2] = sprintf("%0#{digit_count}i", i)
    end
  # try to match sequence stamp to end of filename
  elsif match_data = chunks[-1].match( /^(\d{#{digit_count}})$/ )
    if timestamp
      chunks[-1] = timestamp
    else
      i = match_data[1].to_i + 1
      chunks[-1] = sprintf("%0#{digit_count}i", i)
    end
  # not yet sequence_stamped, has file extension
  else
    chunks = [chunks[0..-2], (timestamp ? timestamp : "0" * digit_count), chunks[-1]].flatten
  end

  self.up.join( chunks.join(".") )
end
succession( opts = { digit_count: SUCC_DIGIT_COUNT, timestamp: false } ) click to toggle source

TODO: succession : enumerates a sequence of files that get passed to a block in order.

# File lib/fun_with/files/file_path.rb, line 541
def succession( opts = { digit_count: SUCC_DIGIT_COUNT, timestamp: false } )
  if opts[:timestamp]
    opts[:timestamp_format] ||= "%Y%m%d%H%M%S%L"
    timestamp = Time.now.strftime( opts[:timestamp_format] )
    digit_count = timestamp.length
  else
    timestamp = false
    digit_count = opts[:digit_count]
  end

  chunks = self.basename.to_s.split(".")
  glob_stamp_matcher = '[0-9]' * digit_count

  # unstamped filename, no extension
  if chunks.length == 1
    original = chunks.first
    stamped = [original, glob_stamp_matcher].join(".")
  # stamped filename, no extension
  elsif chunks[-1].match( /^\d{#{digit_count}}$/ )
    original = chunks[0..-2].join(".")
    stamped = [original, glob_stamp_matcher].join(".")
  # stamped filename, has extension
  elsif chunks[-2].match( /^\d{#{digit_count}}$/ )
    original = [chunks[0..-3], chunks.last].flatten.join(".")
    stamped = [chunks[0..-3], glob_stamp_matcher, chunks.last].join(".")
  # unstamped filename, has extension
  else
    original = chunks.join(".")
    stamped = [ chunks[0..-2], glob_stamp_matcher, chunks[-1] ].flatten.join(".")
  end

  [self.dirname.join(original), self.dirname.glob(stamped)].flatten
end
timestamp( format = true ) { |nxt| ... } click to toggle source
# File lib/fun_with/files/file_path.rb, line 513
def timestamp( format = true, &block )
  nxt = self.succ( :timestamp => format )
  yield nxt if block_given?
  nxt
end
to_pathname() click to toggle source
# File lib/fun_with/files/file_path.rb, line 659
def to_pathname
  Pathname.new( @path )
end
touch( *args ) { |touched| ... } click to toggle source

Raises error if self is a file and args present. Raises error if the file is not accessible for writing, or cannot be created. attempts to create a directory

Takes an options hash as the last argument, allowing same options as FileUtils.touch

# File lib/fun_with/files/file_path.rb, line 209
def touch( *args, &block )
  args, opts = extract_opts_from_args( args )

  raise "Cannot create subdirectory to a file" if self.file? && args.length > 0
  touched = self.join(*args)

  dir_for_touched_file = case args.length
    when 0
      self.up
    when 1
      self
    when 2..Float::INFINITY
      self.join( *(args[0..-2] ) )
    end

  self.touch_dir( dir_for_touched_file, opts ) unless dir_for_touched_file.directory?
  FileUtils.touch( touched, narrow_options( opts, FileUtils::OPT_TABLE["touch"] ) )

  yield touched if block_given?
  return touched
end
touch_dir( *args ) { |touched| ... } click to toggle source

Takes the options of both FileUtils.touch and FileUtils.mkdir_p mkdir_p options will only matter if the directory is being created.

# File lib/fun_with/files/file_path.rb, line 233
def touch_dir( *args, &block )
  args, opts = extract_opts_from_args( args )

  touched = self.join(*args)
  if touched.directory?
    FileUtils.touch( touched, narrow_options( opts, FileUtils::OPT_TABLE["touch"] ) )    # update access time
  else
    FileUtils.mkdir_p( touched, narrow_options( opts, FileUtils::OPT_TABLE["mkdir_p"] ) )  # create directory (and any needed parents)
  end

  yield touched if block_given?
  return touched
end
up() click to toggle source

If called on a file instead of a directory, has the same effect as path.dirname

# File lib/fun_with/files/file_path.rb, line 75
def up
  self.class.new( self.join("..") ).expand
end
without_ext( ext_val = nil ) click to toggle source

Returns the path, stripped of the final extension (.ARG). The result cannot destroy a dotfile or leave an empty path

"~/.bashrc".without_ext( .bashrc )  => "~/.bashrc",

if an argument is given, the final ext must match the given argument, or a copy of the unaltered path is returned.

Also:

Don't add a leading ./ when the original didn't have one
Case InSeNSItive, because I can't think of a use case for "only"
   strip the extension if the capitalization matches
Chews up any number of leading '.'s
For the moment,
# File lib/fun_with/files/file_path.rb, line 339
def without_ext( ext_val = nil )
  ext_chopper_regex = if ext_val.fwf_blank?
                        /\.+\w+$/i             # any ending "word characters"
                      else
                        # It's okay for the caller to make the leading period explicit
                        ext_val = ext_val.to_s.gsub( /^\./, '' )
                        /\.+#{ext_val}+$/i
                      end

  chopped_str = @path.gsub( ext_chopper_regex, "" )
  
  do_we_chop = if chopped_str == @path   # no change, then sure, why not?
                 true
               elsif chopped_str.fwf_blank? || chopped_str[-1] == self.separator() || chopped_str[-1] == "."
                 false
               else
                 true
               end
               
  self.class.new( do_we_chop ? chopped_str : @path )
  
  # # If the results fail to live up to some pre-defined
  #
  #
  #
  # # do we or don't we?
  # chop_extension = true
  #
  # #   Don't if there's an extension mismatch
  # #   Don't if the remainder is a /. (original was a dot_file)
  #
  #
  #
  #
  #
  #
  # _dirname, _basename, _ext = self.dirname_and_basename_and_ext
  # debugger if _basename =~ "hello"
  # ext_val = ext_val.to_s
  #
  # # 1) Only perform if the extension match the one given (or was a blank ext given?)
  #
  #
  #
  # new_path = @path.clone
  #
  # current_ext = self.ext
  #
  # e = e.to_s
  #
  # if e.fwf_present?
  #   new_path.gsub!( /\.#{e}$/, "" )
  #   result = self.gsub(/#{ not_beginning_of_line_or_separator}\.#{self.ext}$/, '')
  # else
  #   self.clone
  # end
  #
  # self.class.new( new_path )
end
write( *args ) { |f| ... } click to toggle source
# File lib/fun_with/files/file_path.rb, line 247
def write( *args, &block )
  args, opts = extract_opts_from_args( args )

  content = args.first

  if content == :random
    self.write_random_data( opts )
  else
    File.open( self, "w" ) do |f|
      f << content if content
      if block_given?
        yield f
      end
    end
  end
end
write_random_data( sz, opts = {} ) click to toggle source

sz: number of bytes to write to the file opts => :overwrite or :append seed: What number to seed the random number generator with

FUTURE: May perform slowly on large sz inputs?

# File lib/fun_with/files/file_path.rb, line 269
def write_random_data( sz, opts = {} )
  rng = Random.new( opts[:seed] || Random::new_seed )
  mode = opts[:mode] || :overwrite

  if mode == :overwrite
    self.write( rng.bytes( sz ) )
  elsif mode == :append
    self.append( rng.bytes( sz ) )
  end
end

Protected Instance Methods

_must_be_a_directory() click to toggle source
# File lib/fun_with/files/file_path.rb, line 686
def _must_be_a_directory
  unless self.directory?
    calling_method = caller[0][/`.*'/][1..-2]
    raise Errno::EACCESS.new( "Can only call FunWith::Files::FilePath##{calling_method}() on an existing directory.")
  end
end
_must_be_a_file() click to toggle source

TODO: Need a separate API for user to call

# File lib/fun_with/files/file_path.rb, line 679
def _must_be_a_file
  unless self.file?
    calling_method = caller[0][/`.*'/][1..-2]
    raise Errno::EACCESS.new( "Can only call FunWith::Files::FilePath##{calling_method}() on an existing file.")
  end
end
_must_be_writable() click to toggle source
# File lib/fun_with/files/file_path.rb, line 693
def _must_be_writable
  unless self.writable?
    calling_method = caller[0][/`.*'/][1..-2]
    raise Errno::EACCESS.new( "Error in FunWith::Files::FilePath##{calling_method}(): #{@path} not writable.")
  end
end
extract_opts_from_args( args ) click to toggle source
# File lib/fun_with/files/file_path.rb, line 704
def extract_opts_from_args( args )
  if args.last.is_a?( Hash )
    [args[0..-2], args.last ]
  else
    [args, {}]
  end
end
narrow_options( opts, keys ) click to toggle source
# File lib/fun_with/files/file_path.rb, line 700
def narrow_options( opts, keys )
  opts.keep_if{ |k,v| keys.include?( k ) }
end
yield_and_return( obj ) { |obj| ... } click to toggle source
# File lib/fun_with/files/file_path.rb, line 712
def yield_and_return( obj, &block )
  yield obj if block_given?
  obj
end