Class: FunWith::Files::FilePath

Inherits:
Pathname
  • Object
show all
Extended by:
FilePathClassMethods
Defined in:
lib/fun_with/files/file_path.rb

Direct Known Subclasses

RemotePath

Constant Summary collapse

SUCC_DIGIT_COUNT =
6
DEFAULT_TIMESTAMP_FORMAT =
"%Y%m%d%H%M%S%L"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from FilePathClassMethods

config_dir, cwd, data_dir, home, pwd, root

Constructor Details

#initialize(*args) ⇒ FilePath

Returns a new instance of FilePath.



7
8
9
# File 'lib/fun_with/files/file_path.rb', line 7

def initialize( *args )
  super( File.join( *args ) )
end

Instance Attribute Details

#pathObject

Returns the value of attribute path.



11
12
13
# File 'lib/fun_with/files/file_path.rb', line 11

def path
  @path
end

Class Method Details

.tmpdir(&block) ⇒ Object

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.



18
19
20
21
22
23
24
25
26
# 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

Instance Method Details

#/(arg) ⇒ Object



39
40
41
# File 'lib/fun_with/files/file_path.rb', line 39

def / arg
  self.join( arg )
end

#[](*args) ⇒ Object



43
44
45
# File 'lib/fun_with/files/file_path.rb', line 43

def [] *args
  self.join(*args)
end

#append(content = nil, &block) ⇒ Object



236
237
238
239
240
241
242
243
# File 'lib/fun_with/files/file_path.rb', line 236

def append( content = nil, &block )
  File.open( self, "a" ) do |f|
    f << content if content
    if block_given?
      yield f
    end
  end
end

#ascend(&block) ⇒ Object



524
525
526
527
528
529
530
531
532
533
# File 'lib/fun_with/files/file_path.rb', line 524

def ascend( &block )
  path = self.clone
  
  if path.root?
    yield path
  else
    yield self
    self.up.ascend( &block )
  end
end

#basename_and_extObject

base, ext = @path.basename_and_ext



296
297
298
# File 'lib/fun_with/files/file_path.rb', line 296

def basename_and_ext
  [self.basename_no_ext, self.ext]
end

#basename_no_extObject

Does not return a filepath



273
274
275
# File 'lib/fun_with/files/file_path.rb', line 273

def basename_no_ext
  self.basename.to_s.split(".")[0..-2].join(".")
end

#descend(&block) ⇒ Object



513
514
515
516
517
518
519
520
521
522
# File 'lib/fun_with/files/file_path.rb', line 513

def descend( &block )
  path = self.clone
  
  if path.root?
    yield path
  else
    self.up.descend( &block )
    yield self
  end
end

#directoryObject

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



313
314
315
# File 'lib/fun_with/files/file_path.rb', line 313

def directory
  self.directory? ? self : self.dirname
end

#dirname_and_basenameObject



302
303
304
305
# File 'lib/fun_with/files/file_path.rb', line 302

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_extObject



307
308
309
# File 'lib/fun_with/files/file_path.rb', line 307

def dirname_and_basename_and_ext
  [self.dirname, self.basename_no_ext, self.ext]
end

#doesnt_exist?Boolean

Returns:



49
50
51
# File 'lib/fun_with/files/file_path.rb', line 49

def doesnt_exist?
  self.exist? == false
end

#empty?Boolean

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.

Returns:

Raises:



262
263
264
265
266
267
268
269
270
# File 'lib/fun_with/files/file_path.rb', line 262

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

#entriesObject



176
177
178
# File 'lib/fun_with/files/file_path.rb', line 176

def entries
  self.glob( :recurse => false )
end

#expandObject



180
181
182
# File 'lib/fun_with/files/file_path.rb', line 180

def expand
  self.class.new( File.expand_path( self ) )
end

#ext(*args) ⇒ Object

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



285
286
287
288
289
290
291
292
293
# File 'lib/fun_with/files/file_path.rb', line 285

def ext( *args )
  if args.length == 0
    split_basename = self.basename.to_s.split(".")
    split_basename.length > 1 ? split_basename.last : ""
  elsif args.length == 1
    ext = args.first.to_s.gsub(/^\./,'')
    self.class.new( @path.dup + ".#{ext}" )
  end
end

#fwf_filepathObject



331
332
333
# File 'lib/fun_with/files/file_path.rb', line 331

def fwf_filepath
  self
end

#glob(*args, &block) ⇒ Object

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(: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 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)


101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/fun_with/files/file_path.rb', line 101

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, &block) ⇒ Object

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



247
248
249
250
251
252
253
254
255
256
257
# File 'lib/fun_with/files/file_path.rb', line 247

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| ... } ⇒ Object Also known as: down

Yields:

  • (joined_path)


28
29
30
31
32
# File 'lib/fun_with/files/file_path.rb', line 28

def join( *args, &block )
  joined_path = self.class.new( super(*args) )
  yield joined_path if block_given?
  joined_path
end

#join!(*args, &block) ⇒ Object



34
35
36
37
# File 'lib/fun_with/files/file_path.rb', line 34

def join!( *args, &block )
  @path = self.join( *args, &block ).to_str
  self
end

#loadObject

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



456
457
458
459
460
461
462
# File 'lib/fun_with/files/file_path.rb', line 456

def load
  if self.directory?
    self.glob( :recursive => true, :ext => "rb" ).map(&:load)
  else
    Kernel.load( self.expand )
  end
end

#originalObject



321
322
323
# File 'lib/fun_with/files/file_path.rb', line 321

def original
  self.symlink? ? self.readlink.original : self
end

#original?Boolean

Returns:



317
318
319
# File 'lib/fun_with/files/file_path.rb', line 317

def original?
  !self.symlink?
end

#relative_path_from(dir) ⇒ Object

Basically Pathname.relative_path_from, but you can pass in strings



326
327
328
329
# File 'lib/fun_with/files/file_path.rb', line 326

def relative_path_from( dir )
  dir = super( Pathname.new( dir ) )
  self.class.new( dir )
end

#requirObject

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.



471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'lib/fun_with/files/file_path.rb', line 471

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?Boolean

Returns:



509
510
511
# File 'lib/fun_with/files/file_path.rb', line 509

def root?
  self == self.up
end

#specifier(str) ⇒ Object

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.



402
403
404
405
406
407
408
409
410
411
412
413
# File 'lib/fun_with/files/file_path.rb', line 402

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 }) ⇒ Object

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.



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/fun_with/files/file_path.rb', line 346

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 }) ⇒ Object

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



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/fun_with/files/file_path.rb', line 417

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| ... } ⇒ Object

Yields:

  • (nxt)


389
390
391
392
393
# File 'lib/fun_with/files/file_path.rb', line 389

def timestamp( format = true, &block )
  nxt = self.succ( :timestamp => format )
  yield nxt if block_given?
  nxt
end

#to_pathnameObject



535
536
537
# File 'lib/fun_with/files/file_path.rb', line 535

def to_pathname
  Pathname.new( @path )
end

#touch(*args) {|touched| ... } ⇒ Object

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

Yields:

  • (touched)


189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/fun_with/files/file_path.rb', line 189

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| ... } ⇒ Object

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

Yields:

  • (touched)


213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/fun_with/files/file_path.rb', line 213

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

#upObject

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



55
56
57
# File 'lib/fun_with/files/file_path.rb', line 55

def up
  self.class.new( self.join("..") ).expand
end

#without_extObject



277
278
279
# File 'lib/fun_with/files/file_path.rb', line 277

def without_ext
  self.gsub(/\.#{self.ext}$/, '')
end

#write(content = nil, &block) ⇒ Object



227
228
229
230
231
232
233
234
# File 'lib/fun_with/files/file_path.rb', line 227

def write( content = nil, &block )
  File.open( self, "w" ) do |f|
    f << content if content
    if block_given?
      yield f
    end
  end
end