Class: FunWith::Files::FilePath

Inherits:
Pathname 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"

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

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.



16
17
18
19
20
21
22
23
24
# File 'lib/fun_with/files/file_path.rb', line 16

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

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



200
201
202
203
204
205
206
207
# File 'lib/fun_with/files/file_path.rb', line 200

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



489
490
491
492
493
494
495
496
497
498
# File 'lib/fun_with/files/file_path.rb', line 489

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



254
255
256
# File 'lib/fun_with/files/file_path.rb', line 254

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

#basename_no_extObject

Does not return a filepath



231
232
233
# File 'lib/fun_with/files/file_path.rb', line 231

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

#descend(&block) ⇒ Object



478
479
480
481
482
483
484
485
486
487
# File 'lib/fun_with/files/file_path.rb', line 478

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



268
269
270
# File 'lib/fun_with/files/file_path.rb', line 268

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

#dirname_and_basenameObject



258
259
260
# File 'lib/fun_with/files/file_path.rb', line 258

def dirname_and_basename
  [self.dirname, self.basename]
end

#dirname_and_basename_and_extObject



262
263
264
# File 'lib/fun_with/files/file_path.rb', line 262

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

#empty?Boolean

Not the same as zero?

Returns:

  • (Boolean)

Raises:

  • (Exceptions::FileDoesNotExist)


220
221
222
223
224
225
226
227
228
# File 'lib/fun_with/files/file_path.rb', line 220

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



151
152
153
# File 'lib/fun_with/files/file_path.rb', line 151

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

#expandObject



155
156
157
# File 'lib/fun_with/files/file_path.rb', line 155

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



243
244
245
246
247
248
249
250
251
# File 'lib/fun_with/files/file_path.rb', line 243

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



286
287
288
# File 'lib/fun_with/files/file_path.rb', line 286

def fwf_filepath
  self
end

#glob(*args) ⇒ 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 @path.glob(“**”, “*”) # same



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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
# File 'lib/fun_with/files/file_path.rb', line 82

def glob( *args )
  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
            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]
    
  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]
  
  files
end

#grep(regex) ⇒ Object

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



210
211
212
213
214
215
216
217
# File 'lib/fun_with/files/file_path.rb', line 210

def grep( regex )
  return [] unless self.file?
  matching = []
  self.each_line do |line|
    matching.push( line ) if line.match( regex )
  end
  matching
end

#join(*args, &block) ⇒ Object Also known as: down



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

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

#loadObject



421
422
423
424
425
426
427
# File 'lib/fun_with/files/file_path.rb', line 421

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

#originalObject



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

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

#original?Boolean

Returns:

  • (Boolean)


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

def original?
  !self.symlink?
end

#relative_path_from(dir) ⇒ Object

Basically Pathname.relative_path_from, but you can pass in strings



281
282
283
284
# File 'lib/fun_with/files/file_path.rb', line 281

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

#rename(filename) ⇒ Object

File manipulation



404
405
406
# File 'lib/fun_with/files/file_path.rb', line 404

def rename( filename )

end

#rename_all(pattern, gsubbed) ⇒ Object



408
409
410
# File 'lib/fun_with/files/file_path.rb', line 408

def rename_all( pattern, gsubbed )

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.



436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/fun_with/files/file_path.rb', line 436

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

#rm(secure = false) ⇒ Object



412
413
414
415
416
417
418
# File 'lib/fun_with/files/file_path.rb', line 412

def rm( secure = false )
  if self.file?
    FileUtils.rm( self )
  elsif self.directory?
    FileUtils.rmtree( self )
  end
end

#root?Boolean

Returns:

  • (Boolean)


474
475
476
# File 'lib/fun_with/files/file_path.rb', line 474

def root?
  self == self.up
end

#specifier(str) ⇒ Object



348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/fun_with/files/file_path.rb', line 348

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.



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/fun_with/files/file_path.rb', line 301

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.



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/fun_with/files/file_path.rb', line 363

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



344
345
346
# File 'lib/fun_with/files/file_path.rb', line 344

def timestamp( format = true )
  self.succ( :timestamp => format )
end

#touch(*args) ⇒ 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



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/fun_with/files/file_path.rb', line 162

def touch( *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..Infinity
      self.join( *(args[0..-2] ) )
    end
  
  self.touch_dir( dir_for_touched_file ) unless dir_for_touched_file.directory?
  FileUtils.touch( touched )
  return touched
end

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

Yields:

  • (touched)


179
180
181
182
183
184
185
186
187
188
189
# File 'lib/fun_with/files/file_path.rb', line 179

def touch_dir( *args, &block )
  touched = self.join(*args)
  if touched.directory?
    FileUtils.touch( touched )    # update access time
  else
    FileUtils.mkdir_p( touched )  # 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



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

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

#without_extObject



235
236
237
# File 'lib/fun_with/files/file_path.rb', line 235

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

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



191
192
193
194
195
196
197
198
# File 'lib/fun_with/files/file_path.rb', line 191

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