Class: Path

Inherits:
Object show all
Includes:
Comparable, Enumerable
Defined in:
lib/epitools/path.rb

Overview

Path: An object-oriented wrapper for files. (Combines useful methods from FileUtils, File, Dir, and more!)

To create a path object, or array of path objects, throw whatever you want into Path[]:

These returns a single path object:
  passwd      = Path["/etc/passwd"]
  also_passwd = Path["/etc"] / "passwd"         # joins two paths
  parent_dir  = Path["/usr/local/bin"] / ".."   # joins two paths (up one dir)

These return an array of path objects:
  pictures   = Path["photos/*.{jpg,png}"]   # globbing
  notes      = Path["notes/2014/**/*.txt"]  # recursive globbing
  everything = Path["/etc"].ls

Each Path object has the following attributes, which can all be modified:

path     => the absolute path, as a string
filename => just the name and extension
basename => just the filename (without extension)
ext      => just the extension
dir      => just the directory
dirs     => an array of directories

Some commonly used methods:

path.file?
path.exists?
path.dir?
path.mtime
path.xattrs
path.symlink?
path.broken_symlink?
path.symlink_target
path.executable?
path.chmod(0o666)

Interesting examples:

Path["*.jpeg"].each { |path| path.rename(:ext=>"jpg") } # renames .jpeg to .jpg

files     = Path["/etc"].ls         # all files in directory
morefiles = Path["/etc"].ls_R       # all files in directory tree

Path["*.txt"].each(&:gzip!)

Path["filename.txt"] << "Append data!"     # appends data to a file

string = Path["filename.txt"].read         # read all file data into a string
json   = Path["filename.json"].read_json   # read and parse JSON
doc    = Path["filename.html"].read_html   # read and parse HTML
xml    = Path["filename.xml"].parse        # figure out the format and parse it (as XML)

Path["saved_data.marshal"].write(data.marshal)   # Save your data!
data = Path["saved_data.marshal"].unmarshal      # Load your data!

Path["unknown_file"].mimetype              # sniff the file to determine its mimetype
Path["unknown_file"].mimetype.image?       # ...is this some kind of image?

Path["otherdir/"].cd do                    # temporarily change to "otherdir/"
  p Path.ls
end
p Path.ls

The `Path#dirs` attribute is a split up version of the directory (eg: Path.dirs => [“usr”, “local”, “bin”]).

You can modify the dirs array to change subsets of the directory. Here's an example that finds out if you're in a git repo:

def inside_a_git_repo?
  path = Path.pwd # start at the current directory
  while path.dirs.any?
    if (path/".git").exists?
      return true
    else
      path.dirs.pop  # go up one level
    end
  end
  false
end

Swap two files:

a, b = Path["file_a", "file_b"]
temp = a.with(:ext => a.ext+".swapping") # return a modified version of this object
a.mv(temp)
b.mv(a)
temp.mv(b)

Paths can be created for existant and non-existant files.

To create a nonexistant path object that thinks it's a directory, just add a '/' at the end. (eg: Path).

Performance has been an important factor in Path's design, so doing crazy things with Path usually doesn't kill performance. Go nuts!

Direct Known Subclasses

Relative, URI

Defined Under Namespace

Classes: Relative, URI

Constant Summary collapse

COMPRESSORS =

zopening files

{
  "gz"  => "gzip",
  "xz"  => "xz",
  "bz2" => "bzip2"
}
AUTOGENERATED_CLASS_METHODS =

FileUtils-like class-method versions of instance methods (eg: `Path.mv(src, dest)`)

Note: Methods with cardinality 1 (`method/1`) are instance methods that take one parameter, and hence, class methods that take two parameters.

%w[
  mkdir
  mkdir_p
  sha1
  sha2
  md5
  rm
  truncate
  realpath
  mv/1
  move/1
  chmod/1
  chown/1
  chown_R/1
  chmod_R/1
].each do |spec|
  meth, cardinality = spec.split("/")
  cardinality       = cardinality.to_i

  class_eval %{
    def self.#{meth}(path#{", *args" if cardinality > 0})
      Path[path].#{meth}#{"(*args)" if cardinality > 0}
    end
  }
end
PATH_SEPARATOR =
":"
BINARY_EXTENSION =
""

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Enumerable

#*, #**, #average, #blank?, #combination, #counts, #cross_product, #foldl, #group_neighbours_by, #grouped_to_h, #groups, #map_recursively, #parallel_map, #permutation, #powerset, #reverse, #reverse_each, #rle, #rzip, #select_recursively, #skip, #sort_numerically, #split_after, #split_at, #split_before, #split_between, #sum, #to_iter, #uniq, #unzip

Constructor Details

#initialize(newpath, hints = {}) ⇒ Path

Initializers


127
128
129
130
131
132
133
134
135
136
137
# File 'lib/epitools/path.rb', line 127

def initialize(newpath, hints={})
  send("path=", newpath, hints)

  # if hints[:unlink_when_garbage_collected]
  #   backup_path = path.dup
  #   puts "unlinking #{backup_path} after gc!"
  #   ObjectSpace.define_finalizer self do |object_id|
  #     File.unlink backup_path
  #   end
  # end
end

Instance Attribute Details

#baseObject Also known as: basename

The filename without an extension


117
118
119
# File 'lib/epitools/path.rb', line 117

def base
  @base
end

#dirsObject

The directories in the path, split into an array. (eg: ['usr', 'src', 'linux'])


114
115
116
# File 'lib/epitools/path.rb', line 114

def dirs
  @dirs
end

#extObject Also known as: extname, extension

The file extension, including the . (eg: “.mp3”)


120
121
122
# File 'lib/epitools/path.rb', line 120

def ext
  @ext
end

Class Method Details

.[](path) ⇒ Object


153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/epitools/path.rb', line 153

def self.[](path)
  case path
  when Path
    path
  when String

    if path =~ %r{^[a-z\-]+://}i # URL?
      Path::URI.new(path)

    else
      # TODO: highlight backgrounds of codeblocks to show indent level & put boxes (or rules?) around (between?) double-spaced regions
      path = Path.expand_path(path)
      unless path =~ /(^|[^\\])[\?\*\{\}]/ # contains unescaped glob chars?
        new(path)
      else
        glob(path)
      end

    end

  end
end

.cd(dest) ⇒ Object

Change into the directory “dest”. If a block is given, it changes into the directory for the duration of the block, then puts you back where you came from once the block is finished.


1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
# File 'lib/epitools/path.rb', line 1505

def self.cd(dest)
  dest = Path[dest]

  raise "Can't 'cd' into #{dest}" unless dest.dir?

  if block_given?
    orig = pwd

    Dir.chdir(dest)
    result = yield dest
    Dir.chdir(orig)

    result
  else
    Dir.chdir(dest)
    dest
  end
end

.escape(str) ⇒ Object


145
146
147
# File 'lib/epitools/path.rb', line 145

def self.escape(str)
  Shellwords.escape(str)
end

.expand_path(orig_path) ⇒ Object

Same as File.expand_path, except preserves the trailing '/'.


1448
1449
1450
1451
1452
# File 'lib/epitools/path.rb', line 1448

def self.expand_path(orig_path)
  new_path = File.expand_path orig_path
  new_path << "/" if orig_path.endswith "/"
  new_path
end

.getfattr(path) ⇒ Object

Read xattrs from file (requires “getfattr” to be in the path)


531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
# File 'lib/epitools/path.rb', line 531

def self.getfattr(path)
  # # file: Scissor_Sisters_-_Invisible_Light.flv
  # user.m.options="-c"

  cmd = %w[getfattr -d -m - -e base64] + [path]

  attrs = {}

  IO.popen(cmd, "rb", :err=>[:child, :out]) do |io|
    io.each_line do |line|
      if line =~ /^([^=]+)=0s(.+)/
        key   = $1
        value = $2.from_base64 # unpack base64 string
        # value = value.encode("UTF-8", "UTF-8") # set string's encoding to UTF-8
        value = value.force_encoding("UTF-8").scrub  # set string's encoding to UTF-8
        # value = value.encode("UTF-8", "UTF-8")  # set string's encoding to UTF-8

        attrs[key] = value
      end
    end
  end

  attrs
end

.glob(str, hints = {}) ⇒ Object


149
150
151
# File 'lib/epitools/path.rb', line 149

def self.glob(str, hints={})
  Dir[str].map { |entry| new(entry, hints) }
end

.homeObject

User's current home directory


1479
1480
1481
# File 'lib/epitools/path.rb', line 1479

def self.home
  Path[ENV['HOME']]
end

.ln_s(src, dest) ⇒ Object


1528
1529
1530
1531
# File 'lib/epitools/path.rb', line 1528

def self.ln_s(src, dest)
  FileUtils.ln_s(src, dest)
  Path[dest]
end

.ls(path) ⇒ Object


1524
# File 'lib/epitools/path.rb', line 1524

def self.ls(path); Path[path].ls  end

.ls_r(path) ⇒ Object


1526
# File 'lib/epitools/path.rb', line 1526

def self.ls_r(path); Path[path].ls_r; end

.popdObject


1495
1496
1497
1498
# File 'lib/epitools/path.rb', line 1495

def self.popd
  @@dir_stack ||= [pwd]
  @@dir_stack.pop
end

.pushd(destination) ⇒ Object


1490
1491
1492
1493
# File 'lib/epitools/path.rb', line 1490

def self.pushd(destination)
  @@dir_stack ||= []
  @@dir_stack.push pwd
end

.pwdObject

The current directory


1486
1487
1488
# File 'lib/epitools/path.rb', line 1486

def self.pwd
  Path.new expand_path(Dir.pwd)
end

.setfattr(path, key, value) ⇒ Object

Set xattrs on a file (requires “setfattr” to be in the path)


559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
# File 'lib/epitools/path.rb', line 559

def self.setfattr(path, key, value)
  cmd = %w[setfattr]

  if value == nil
    # delete
    cmd += ["-x", key]
  else
    # set
    cmd += ["-n", key, "-v", value.to_s.strip]
  end

  cmd << path

  IO.popen(cmd, "rb", :err=>[:child, :out]) do |io|
    result = io.each_line.to_a
    error = {:cmd => cmd, :result => result.to_s}.inspect
    raise error if result.any?
  end
end

.tmpdir(prefix = "tmp") ⇒ Object

Create a uniqely named directory in /tmp


1469
1470
1471
1472
1473
# File 'lib/epitools/path.rb', line 1469

def self.tmpdir(prefix="tmp")
  t = tmpfile
  t.rm; t.mkdir # FIXME: These two operations should be made atomic
  t
end

.tmpfile(prefix = "tmp") {|path| ... } ⇒ Object

TODO: Remove the tempfile when the Path object is garbage collected or freed.

Yields:


1457
1458
1459
1460
1461
# File 'lib/epitools/path.rb', line 1457

def self.tmpfile(prefix="tmp")
  path = Path.new(Tempfile.new(prefix).path, unlink_when_garbage_collected: true)
  yield path if block_given?
  path
end

.which(bin, *extras) ⇒ Object

A clone of `/usr/bin/which`: pass in the name of a binary, and it'll search the PATH returning the absolute location of the binary if it exists, or `nil` otherwise.

(Note: If you pass more than one argument, it'll return an array of `Path`s instead of

a single path.)

1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
# File 'lib/epitools/path.rb', line 1552

def self.which(bin, *extras)
  if extras.empty?
    ENV["PATH"].split(PATH_SEPARATOR).find do |path|
      result = (Path[path] / (bin + BINARY_EXTENSION))
      return result if result.exists?
    end
    nil
  else
    ([bin] + extras).map { |bin| which(bin) }
  end
end

Instance Method Details

#/(other) ⇒ Object

Path/“passwd” == Path (globs permitted)


517
518
519
520
521
522
# File 'lib/epitools/path.rb', line 517

def /(other)
  # / <- fixes jedit syntax highlighting bug.
  # TODO: make it work for "/dir/dir"/"/dir/file"
  #Path.new( File.join(self, other) )
  Path[ File.join(self, other) ]
end

#<=>(other) ⇒ Object


483
484
485
486
487
488
489
490
491
492
# File 'lib/epitools/path.rb', line 483

def <=>(other)
  case other
  when Path
    sort_attrs <=> other.sort_attrs
  when String
    path <=> other
  else
    raise "Invalid comparison: Path to #{other.class}"
  end
end

#==(other) ⇒ Object Also known as: eql?


494
495
496
# File 'lib/epitools/path.rb', line 494

def ==(other)
  self.path == other.to_s
end

#=~(pattern) ⇒ Object

Match the full path against a regular expression


1291
1292
1293
# File 'lib/epitools/path.rb', line 1291

def =~(pattern)
  to_s =~ pattern
end

#[](key) ⇒ Object

Retrieve one of this file's xattrs


611
612
613
# File 'lib/epitools/path.rb', line 611

def [](key)
  attrs[key]
end

#[]=(key, value) ⇒ Object

Set this file's xattr


618
619
620
621
# File 'lib/epitools/path.rb', line 618

def []=(key, value)
  Path.setfattr(path, key, value)
  @attrs = nil # clear cached xattrs
end

#append(data = nil) ⇒ Object Also known as: <<

Append data to this file (accepts a string, an IO, or it can yield the file handle to a block.)


744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
# File 'lib/epitools/path.rb', line 744

def append(data=nil)
  # FIXME: copy_stream might be inefficient if you're calling it a lot. Investigate!
  self.open("ab") do |f|
    if data and not block_given?
      if data.is_an? IO
        IO.copy_stream(data, f)
      else
        f.write(data)
      end
    else
      yield f
    end
  end
  self
end

#atimeObject


392
393
394
# File 'lib/epitools/path.rb', line 392

def atime
  lstat.atime
end

#atime=(new_atime) ⇒ Object


396
397
398
399
400
# File 'lib/epitools/path.rb', line 396

def atime=(new_atime)
  File.utime(new_atime, mtime, path)
  @lstat = nil
  new_atime
end

#attrsObject Also known as: xattrs

Return a hash of all of this file's xattrs. (Metadata key=>valuse pairs, supported by most modern filesystems.)


583
584
585
# File 'lib/epitools/path.rb', line 583

def attrs
  @attrs ||= Path.getfattr(path)
end

#attrs=(new_attrs) ⇒ Object

Set this file's xattrs. (Optimized so that only changed attrs are written to disk.)


591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
# File 'lib/epitools/path.rb', line 591

def attrs=(new_attrs)
  changes = attrs.diff(new_attrs)

  changes.each do |key, (old, new)|
    case new
    when String, Numeric, true, false, nil
      self[key] = new
    else
      if new.respond_to? :to_str
        self[key] = new.to_str
      else
        raise "Error: Can't use a #{new.class} as an xattr value. Try passing a String."
      end
    end
  end
end

#backup!Object

Rename this file, “filename.ext”, to “filename.ext.bak”. (Does not modify this Path object.)


1066
1067
1068
# File 'lib/epitools/path.rb', line 1066

def backup!
  rename(backup_file)
end

#backup_fileObject

Return a copy of this Path with “.bak” at the end


1050
1051
1052
# File 'lib/epitools/path.rb', line 1050

def backup_file
  with(:filename => filename+".bak")
end

#broken_symlink?Boolean


432
433
434
# File 'lib/epitools/path.rb', line 432

def broken_symlink?
  File.symlink?(path) and not File.exists?(path)
end

#cd(&block) ⇒ Object

Change into the directory. If a block is given, it changes into the directory for the duration of the block, then puts you back where you came from once the block is finished.


960
961
962
# File 'lib/epitools/path.rb', line 960

def cd(&block)
  Path.cd(path, &block)
end

#child_of?(parent) ⇒ Boolean


455
456
457
# File 'lib/epitools/path.rb', line 455

def child_of?(parent)
  parent.parent_of? self
end

#chmod(mode) ⇒ Object

Same usage as `FileUtils.chmod` (because it just calls `FileUtils.chmod`)

eg:

path.chmod(0600) # mode bits in octal (can also be 0o600 in ruby)
path.chmod "u=wrx,go=rx", 'somecommand'
path.chmod "u=wr,go=rr", "my.rb", "your.rb", "his.rb", "her.rb"
path.chmod "ugo=rwx", "slutfile"
path.chmod "u=wrx,g=rx,o=rx", '/usr/bin/ruby', :verbose => true

Letter things:

"a" :: is user, group, other mask.
"u" :: is user's mask.
"g" :: is group's mask.
"o" :: is other's mask.
"w" :: is write permission.
"r" :: is read permission.
"x" :: is execute permission.
"X" :: is execute permission for directories only, must be used in conjunction with "+"
"s" :: is uid, gid.
"t" :: is sticky bit.
"+" :: is added to a class given the specified mode.
"-" :: Is removed from a given class given mode.
"=" :: Is the exact nature of the class will be given a specified mode.

1155
1156
1157
1158
# File 'lib/epitools/path.rb', line 1155

def chmod(mode)
  FileUtils.chmod(mode, self)
  self
end

#chmod_R(mode) ⇒ Object


1166
1167
1168
1169
1170
1171
1172
1173
# File 'lib/epitools/path.rb', line 1166

def chmod_R(mode)
  if directory?
    FileUtils.chmod_R(mode, self)
    self
  else
    raise "Not a directory."
  end
end

#chown(usergroup) ⇒ Object


1160
1161
1162
1163
1164
# File 'lib/epitools/path.rb', line 1160

def chown(usergroup)
  user, group = usergroup.split(":")
  FileUtils.chown(user, group, self)
  self
end

#chown_R(usergroup) ⇒ Object


1175
1176
1177
1178
1179
1180
1181
1182
1183
# File 'lib/epitools/path.rb', line 1175

def chown_R(usergroup)
  user, group = usergroup.split(":")
  if directory?
    FileUtils.chown_R(user, group, self)
    self
  else
    raise "Not a directory."
  end
end

#cp(dest) ⇒ Object


1113
1114
1115
1116
# File 'lib/epitools/path.rb', line 1113

def cp(dest)
  FileUtils.cp(path, dest)
  dest
end

#cp_p(dest) ⇒ Object

Copy a file to a destination, creating all intermediate directories if they don't already exist


1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
# File 'lib/epitools/path.rb', line 1102

def cp_p(dest)
  FileUtils.mkdir_p(dest.dir) unless File.directory? dest.dir
  if file?
    FileUtils.cp(path, dest)
  elsif dir?
    FileUtils.cp_r(path, dest)
  end

  dest
end

#cp_r(dest) ⇒ Object


1094
1095
1096
1097
# File 'lib/epitools/path.rb', line 1094

def cp_r(dest)
  FileUtils.cp_r(path, dest) #if Path[dest].exists?
  dest
end

#ctimeObject


388
389
390
# File 'lib/epitools/path.rb', line 388

def ctime
  lstat.ctime
end

#deflate(level = nil) ⇒ Object Also known as: gzip

gzip the file, returning the result as a string


1238
1239
1240
# File 'lib/epitools/path.rb', line 1238

def deflate(level=nil)
  Zlib.deflate(read, level)
end

#dirObject Also known as: dirname, directory

The current directory (with a trailing /)


313
314
315
316
317
318
319
320
321
322
323
# File 'lib/epitools/path.rb', line 313

def dir
  if dirs
    if relative?
      File.join(*dirs)
    else
      File.join("", *dirs)
    end
  else
    nil
  end
end

#dir=(newdir) ⇒ Object Also known as: dirname=, directory=


229
230
231
232
233
234
# File 'lib/epitools/path.rb', line 229

def dir=(newdir)
  dirs  = File.expand_path(newdir).split(File::SEPARATOR)
  dirs  = dirs[1..-1] if dirs.size > 0

  @dirs = dirs
end

#dir?Boolean Also known as: directory?


420
421
422
# File 'lib/epitools/path.rb', line 420

def dir?
  File.directory? path
end

#each_chunk(chunk_size = 2**14) ⇒ Object

Read the contents of a file one chunk at a time (default chunk size is 16k)


650
651
652
653
654
# File 'lib/epitools/path.rb', line 650

def each_chunk(chunk_size=2**14)
  open do |io|
    yield io.read(chunk_size) until io.eof?
  end
end

#each_lineObject Also known as: each, lines, nicelines, nice_lines

All the lines in this file, chomped.


660
661
662
663
# File 'lib/epitools/path.rb', line 660

def each_line
  return to_enum(:each_line) unless block_given?
  open { |io| io.each_line { |line| yield line.chomp } }
end

#endswith(s) ⇒ Object


1307
# File 'lib/epitools/path.rb', line 1307

def endswith(s); path.endswith(s); end

#executable?Boolean Also known as: exe?


407
408
409
# File 'lib/epitools/path.rb', line 407

def executable?
  mode & 0o111 > 0
end

#exists?Boolean Also known as: exist?

fstat


359
360
361
# File 'lib/epitools/path.rb', line 359

def exists?
  File.exists? path
end

#extsObject


341
342
343
344
345
# File 'lib/epitools/path.rb', line 341

def exts
  extensions = basename.split('.')[1..-1]
  extensions += [@ext] if @ext
  extensions
end

#file?Boolean


424
425
426
# File 'lib/epitools/path.rb', line 424

def file?
  File.file? path
end

#filenameObject


325
326
327
328
329
330
331
332
333
334
335
# File 'lib/epitools/path.rb', line 325

def filename
  if base
    if ext
      base + "." + ext
    else
      base
    end
  else
    nil
  end
end

#filename=(newfilename) ⇒ Object


211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/epitools/path.rb', line 211

def filename=(newfilename)
  if newfilename.nil?
    @ext, @base = nil, nil
  else
    ext = File.extname(newfilename)

    if ext.blank?
      @ext = nil
      @base = newfilename
    else
      self.ext = ext
      if pos = newfilename.rindex(ext)
        @base = newfilename[0...pos]
      end
    end
  end
end

#grep(pat) ⇒ Object

Yields all matching lines in the file (by returning an Enumerator, or receiving a block)


673
674
675
676
677
678
679
# File 'lib/epitools/path.rb', line 673

def grep(pat)
  return to_enum(:grep, pat).to_a unless block_given?

  each_line do |line|
    yield line if line[pat]
  end
end

#gunzip!Object

Quickly gunzip a file, creating a new file, without removing the original, and returning a Path to that new file.


1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
# File 'lib/epitools/path.rb', line 1274

def gunzip!
  raise "Not a .gz file" unless ext == "gz"

  regular_file = self.with(:ext=>nil)

  regular_file.open("wb") do |output|
    Zlib::GzipReader.open(self) do |gzreader|
      IO.copy_stream(gzreader, output)
    end
  end

  update(regular_file)
end

#gzip!(level = nil) ⇒ Object

Quickly gzip a file, creating a new .gz file, without removing the original, and returning a Path to that new file.


1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
# File 'lib/epitools/path.rb', line 1256

def gzip!(level=nil)
  gz_file = self.with(:filename=>filename+".gz")

  raise "#{gz_file} already exists" if gz_file.exists?

  open("rb") do |input|
    Zlib::GzipWriter.open(gz_file) do |gzwriter|
      IO.copy_stream(input, gzwriter)
    end
  end

  update(gz_file)
end

#hashObject


499
# File 'lib/epitools/path.rb', line 499

def hash; path.hash; end

#hidden?Boolean

Does the file or directory name start with a “.”?


466
467
468
469
# File 'lib/epitools/path.rb', line 466

def hidden?
  thing = filename ? filename : dirs.last
  !!thing[/^\../]
end

#id3Object Also known as: id3tags

Read ID3 tags (requires 'id3tag' gem)

Available fields:

tag.artist, tag.title, tag.album, tag.year, tag.track_nr, tag.genre, tag.get_frame(:TIT2)&.content,
tag.get_frames(:COMM).first&.content, tag.get_frames(:COMM).last&.language

950
951
952
# File 'lib/epitools/path.rb', line 950

def id3
  ID3Tag.read(io)
end

#inflateObject Also known as: gunzip

gunzip the file, returning the result as a string


1247
1248
1249
# File 'lib/epitools/path.rb', line 1247

def inflate
  Zlib.inflate(read)
end

#initialize_copy(other) ⇒ Object


139
140
141
142
143
# File 'lib/epitools/path.rb', line 139

def initialize_copy(other)
  @dirs = other.dirs && other.dirs.dup
  @base = other.base && other.base.dup
  @ext  = other.ext  && other.ext.dup
end

#inspectObject

inspect


351
352
353
# File 'lib/epitools/path.rb', line 351

def inspect
  "#<Path:#{path}>"
end

#join(other) ⇒ Object

Path.join(“anything{}”).path == “/etc/anything{}” (globs ignored)


509
510
511
# File 'lib/epitools/path.rb', line 509

def join(other)
  Path.new File.join(self, other)
end

#ln_s(dest) ⇒ Object


1118
1119
1120
1121
1122
1123
1124
# File 'lib/epitools/path.rb', line 1118

def ln_s(dest)
  if dest.startswith("/")
    Path.ln_s(self, dest)
  else
    Path.ln_s(self, self / dest)
  end
end

#lsObject

Returns all the files in the directory that this path points to


688
689
690
691
692
# File 'lib/epitools/path.rb', line 688

def ls
  Dir.foreach(path).
    reject {|fn| fn == "." or fn == ".." }.
    flat_map {|fn| self / fn }
end

#ls_dirsObject

Returns all the directories in this path


707
708
709
710
# File 'lib/epitools/path.rb', line 707

def ls_dirs
  ls.select(&:dir?)
  #Dir.glob("#{path}*/", File::FNM_DOTMATCH).map { |s| Path.new(s, :type=>:dir) }
end

#ls_filesObject

Returns all the files in this path


715
716
717
718
# File 'lib/epitools/path.rb', line 715

def ls_files
  ls.select(&:file?)
  #Dir.glob("#{path}*", File::FNM_DOTMATCH).map { |s| Path.new(s, :type=>:file) }
end

#ls_r(symlinks = false) ⇒ Object

Returns all files in this path's directory and its subdirectories


697
698
699
700
701
# File 'lib/epitools/path.rb', line 697

def ls_r(symlinks=false)
  # glob = symlinks ? "**{,/*/**}/*" : "**/*"
  # Path[File.join(path, glob)]
  Find.find(path).drop(1).map {|fn| Path.new(fn) }
end

#ls_RObject

Returns all files in this path's directory and its subdirectories


702
703
704
705
706
# File 'lib/epitools/path.rb', line 702

def ls_r(symlinks=false)
  # glob = symlinks ? "**{,/*/**}/*" : "**/*"
  # Path[File.join(path, glob)]
  Find.find(path).drop(1).map {|fn| Path.new(fn) }
end

#lstatObject


369
370
371
372
# File 'lib/epitools/path.rb', line 369

def lstat
  @lstat ||= File.lstat self    # to cache, or not to cache? that is the question.
  # File.lstat self                 # ...answer: not to cache!
end

#magicObject

Find the file's mimetype (by magic)


1342
1343
1344
# File 'lib/epitools/path.rb', line 1342

def magic
  open { |io| MimeMagic.by_magic(io) }
end

#md5Object Also known as: md5sum


1223
1224
1225
# File 'lib/epitools/path.rb', line 1223

def md5
  Digest::MD5.file(self).hexdigest
end

#mimetypeObject Also known as: identify

Find the file's mimetype (first from file extension, then by magic)


1327
1328
1329
# File 'lib/epitools/path.rb', line 1327

def mimetype
  mimetype_from_ext || magic
end

#mimetype_from_extObject

Find the file's mimetype (only using the file extension)


1335
1336
1337
# File 'lib/epitools/path.rb', line 1335

def mimetype_from_ext
  MimeMagic.by_extension(ext)
end

#modeObject


374
375
376
# File 'lib/epitools/path.rb', line 374

def mode
  lstat.mode
end

#mtimeObject


378
379
380
# File 'lib/epitools/path.rb', line 378

def mtime
  lstat.mtime
end

#mtime=(new_mtime) ⇒ Object


382
383
384
385
386
# File 'lib/epitools/path.rb', line 382

def mtime=(new_mtime)
  File.utime(atime, new_mtime, path)
  @lstat = nil
  new_mtime
end

#mv(arg) ⇒ Object Also known as: move

Works the same as “rename”, but the destination can be on another disk.


1003
1004
1005
1006
1007
1008
1009
1010
# File 'lib/epitools/path.rb', line 1003

def mv(arg)
  dest = arg_to_path(arg)

  raise "Error: can't move #{self.inspect} because source location doesn't exist." unless exists?

  FileUtils.mv(path, dest)
  dest
end

#mv!(arg) ⇒ Object Also known as: move!

Moves the file (overwriting the destination if it already exists). Also points the current Path object at the new destination.


1024
1025
1026
# File 'lib/epitools/path.rb', line 1024

def mv!(arg)
  update(mv(arg))
end

#nameObject


337
338
339
# File 'lib/epitools/path.rb', line 337

def name
  filename || "#{dirs.last}/"
end

#numbered_backup!Object

Rename this file, “filename.ext”, to “filename (1).ext” (or (2), or (3), or whatever number is available.) (Does not modify this Path object.)


1058
1059
1060
# File 'lib/epitools/path.rb', line 1058

def numbered_backup!
  rename(numbered_backup_file)
end

#numbered_backup_fileObject

Find a backup filename that doesn't exist yet by appending “(1)”, “(2)”, etc. to the current filename.


1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
# File 'lib/epitools/path.rb', line 1032

def numbered_backup_file
  return self unless exists?

  n = 1
  loop do
    if dir?
      new_file = with(:dirs => dirs[0..-2] + ["#{dirs.last} (#{n})"])
    else
      new_file = with(:basename => "#{basename} (#{n})")
    end
    return new_file unless new_file.exists?
    n += 1
  end
end

#open(mode = "rb", &block) ⇒ Object Also known as: io, stream

Open the file (default: read-only + binary mode)


630
631
632
633
634
635
636
# File 'lib/epitools/path.rb', line 630

def open(mode="rb", &block)
  if block_given?
    File.open(path, mode, &block)
  else
    File.open(path, mode)
  end
end

#owner?Boolean

FIXME: Does the current user own this file?


403
404
405
# File 'lib/epitools/path.rb', line 403

def owner?
  raise "STUB"
end

#parentObject

Find the parent directory. If the `Path` is a filename, it returns the containing directory.


1298
1299
1300
1301
1302
1303
1304
# File 'lib/epitools/path.rb', line 1298

def parent
  if file?
    with(:filename=>nil)
  else
    with(:dirs=>dirs[0...-1])
  end
end

#parent_of?(child) ⇒ Boolean


459
460
461
# File 'lib/epitools/path.rb', line 459

def parent_of?(child)
  dirs == child.dirs[0...dirs.size]
end

#parse(io = self.io, forced_ext = nil) ⇒ Object

Parse the file based on the file extension. (Handles json, html, yaml, xml, csv, marshal, and bson.)


851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
# File 'lib/epitools/path.rb', line 851

def parse(io=self.io, forced_ext=nil)
  case (forced_ext or ext.downcase)
  when 'gz', 'bz2', 'xz'
    parse(zopen, exts[-2])
  when 'json'
    read_json(io)
  when 'html', 'htm'
    read_html(io)
  when 'yaml', 'yml'
    read_yaml(io)
  when 'xml', 'rdf', 'rss'
    read_xml(io)
  when 'csv'
    read_csv(io)
  when 'marshal'
    read_marshal(io)
  when 'bson'
    read_bson(io)
  else
    raise "Unrecognized format: #{ext}"
  end
end

#parse_linesObject

Treat each line of the file as a json object, and parse them all, returning an array of hashes


877
878
879
# File 'lib/epitools/path.rb', line 877

def parse_lines
  each_line.map { |line| JSON.parse line }
end

#pathObject Also known as: to_path, to_str, to_s, pathname

Joins and returns the full path


282
283
284
285
286
287
288
# File 'lib/epitools/path.rb', line 282

def path
  if d = dir
    File.join(d, (filename || "") )
  else
    ""
  end
end

#path=(newpath, hints = {}) ⇒ Object

This is the core that initializes the whole class.

Note: The `hints` parameter contains options so `path=` doesn't have to touch the filesytem as much.


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

def path=(newpath, hints={})
  if hints[:type] or File.exists? newpath
    if hints[:type] == :dir or File.directory? newpath
      self.dir = newpath
    else
      self.dir, self.filename = File.split(newpath)
    end
  else
    if newpath.endswith(File::SEPARATOR) # ends in '/'
      self.dir = newpath
    else
      self.dir, self.filename = File.split(newpath)
    end
  end

  # FIXME: Make this work with globs.
  if hints[:relative]
    update(relative_to(Path.pwd))
  elsif hints[:relative_to]
    update(relative_to(hints[:relative_to]))
  end
end

#puts(data = nil) ⇒ Object

Append data, with a newline at the end


764
765
766
767
# File 'lib/epitools/path.rb', line 764

def puts(data=nil)
  append data
  append "\n" unless data and data[-1] == "\n"
end

#read(length = nil, offset = nil) ⇒ Object

Read bytes from the file (just a wrapper around File.read)


643
644
645
# File 'lib/epitools/path.rb', line 643

def read(length=nil, offset=nil)
  File.read(path, length, offset)
end

#read_bson(io = self.io) ⇒ Object

Parse the file as BSON


934
935
936
# File 'lib/epitools/path.rb', line 934

def read_bson(io=self.io)
  BSON.deserialize(read)
end

#read_csv(opts = {}) ⇒ Object Also known as: from_csv

Parse the file as CSV


913
914
915
# File 'lib/epitools/path.rb', line 913

def read_csv(opts={})
  CSV.open(io, opts).each
end

#read_html(io = self.io) ⇒ Object Also known as: from_html


894
895
896
# File 'lib/epitools/path.rb', line 894

def read_html(io=self.io)
  Nokogiri::HTML(io)
end

#read_json(io = self.io) ⇒ Object Also known as: from_json

Parse the file as JSON


883
884
885
# File 'lib/epitools/path.rb', line 883

def read_json(io=self.io)
  JSON.load(io)
end

#read_marshal(io = self.io) ⇒ Object

Parse the file as a Ruby Marshal dump


924
925
926
# File 'lib/epitools/path.rb', line 924

def read_marshal(io=self.io)
  Marshal.load(io)
end

#read_xml(io = self.io) ⇒ Object

Parse the file as XML


919
920
921
# File 'lib/epitools/path.rb', line 919

def read_xml(io=self.io)
  Nokogiri::XML(io)
end

#read_yaml(io = self.io) ⇒ Object Also known as: from_yaml

Parse the file as YAML


906
907
908
# File 'lib/epitools/path.rb', line 906

def read_yaml(io=self.io)
  YAML.load(io)
end

#readable?Boolean


416
417
418
# File 'lib/epitools/path.rb', line 416

def readable?
  mode & 0o444 > 0
end

#realpathObject


1313
1314
1315
# File 'lib/epitools/path.rb', line 1313

def realpath
  Path.new File.realpath(path)
end

#relativeObject

Path relative to current directory (Path.pwd)


302
303
304
# File 'lib/epitools/path.rb', line 302

def relative
  relative_to(pwd)
end

#relative?Boolean

Is this a relative path?


293
294
295
296
297
# File 'lib/epitools/path.rb', line 293

def relative?
  # FIXME: Need a Path::Relative subclass, so that "dir/filename" can be valid.
  #        (If the user changes dirs, the relative path should change too.)
  dirs.first == ".."
end

#relative_to(anchor) ⇒ Object


306
307
308
309
310
# File 'lib/epitools/path.rb', line 306

def relative_to(anchor)
  anchor = anchor.to_s
  anchor += "/" unless anchor[/\/$/]
  to_s.gsub(/^#{Regexp.escape(anchor)}/, '')
end

#reload!Object

Reload this path (updates cached values.)


264
265
266
267
268
269
270
271
# File 'lib/epitools/path.rb', line 264

def reload!
  temp = path
  reset!
  self.path = temp
  @attrs = nil

  self
end

#rename(arg) ⇒ Object Also known as: ren

Renames the file, but doesn't change the current Path object, and returns a Path that points at the new filename.

Examples:

Path["file"].rename("newfile") #=> Path["newfile"]
Path["SongySong.mp3"].rename(:basename=>"Songy Song")
Path["Songy Song.mp3"].rename(:ext=>"aac")
Path["Songy Song.aac"].rename(:dir=>"/music2")
Path["/music2/Songy Song.aac"].exists? #=> true

989
990
991
992
993
994
995
996
997
# File 'lib/epitools/path.rb', line 989

def rename(arg)
  dest = arg_to_path(arg)

  raise "Error: destination (#{dest.inspect}) already exists" if dest.exists?
  raise "Error: can't rename #{self.inspect} because source location doesn't exist." unless exists?

  File.rename(path, dest)
  dest
end

#rename!(arg) ⇒ Object Also known as: ren!

Rename the file and change this Path object so that it points to the destination file.


1016
1017
1018
# File 'lib/epitools/path.rb', line 1016

def rename!(arg)
  update(rename(arg))
end

#reset!Object

Clear out the internal state of this object, so that it can be reinitialized.


256
257
258
259
# File 'lib/epitools/path.rb', line 256

def reset!
  [:@dirs, :@base, :@ext].each { |var| remove_instance_variable(var) rescue nil  }
  self
end

#rmObject Also known as: delete!, unlink!, remove!

Remove a file or directory


1190
1191
1192
1193
1194
1195
1196
1197
1198
# File 'lib/epitools/path.rb', line 1190

def rm
  raise "Error: #{self} does not exist" unless symlink? or exists?

  if directory? and not symlink?
    Dir.rmdir(self) == 0
  else
    File.unlink(self) == 1
  end
end

#sha1Object Also known as: sha1sum

Checksums


1213
1214
1215
# File 'lib/epitools/path.rb', line 1213

def sha1
  Digest::SHA1.file(self).hexdigest
end

#sha2Object Also known as: sha2sum


1218
1219
1220
# File 'lib/epitools/path.rb', line 1218

def sha2
  Digest::SHA2.file(self).hexdigest
end

#sha256Object Also known as: sha256sum


1228
1229
1230
# File 'lib/epitools/path.rb', line 1228

def sha256
  Digest::SHA256.file(self).hexdigest
end

#siblingsObject

Returns all neighbouring directories to this path


723
724
725
# File 'lib/epitools/path.rb', line 723

def siblings
  Path[dir].ls - [self]
end

#sizeObject


363
364
365
366
367
# File 'lib/epitools/path.rb', line 363

def size
  File.size(path)
rescue Errno::ENOENT
  -1
end

#sort_attrsObject

An array of attributes which will be used sort paths (case insensitive, directories come first)


479
480
481
# File 'lib/epitools/path.rb', line 479

def sort_attrs
  [(filename ? 1 : 0), path.downcase]
end

#startswith(s) ⇒ Object


1306
# File 'lib/epitools/path.rb', line 1306

def startswith(s); path.startswith(s); end

#symlink?Boolean


428
429
430
# File 'lib/epitools/path.rb', line 428

def symlink?
  File.symlink? path
end

436
437
438
439
440
441
442
443
# File 'lib/epitools/path.rb', line 436

def symlink_target
  target = File.readlink(path.gsub(/\/$/, ''))
  if target.startswith("/")
    Path[target]
  else
    Path[dir] / target
  end
end

1126
1127
1128
1129
1130
1131
1132
# File 'lib/epitools/path.rb', line 1126

def ln_s(dest)
  if dest.startswith("/")
    Path.ln_s(self, dest)
  else
    Path.ln_s(self, self / dest)
  end
end

#to_PathObject

No-op (returns self)


1567
1568
1569
# File 'lib/epitools/path.rb', line 1567

def to_Path
  self
end

#touchObject

Like the unix `touch` command (if the file exists, update its timestamp, otherwise create a new file)


731
732
733
734
# File 'lib/epitools/path.rb', line 731

def touch
  open("a") { }
  self
end

#truncate(offset = 0) ⇒ Object

Shrink or expand the size of a file in-place


1206
1207
1208
# File 'lib/epitools/path.rb', line 1206

def truncate(offset=0)
  File.truncate(self, offset) if exists?
end

#typeObject

Returns the filetype (as a standard file extension), verified with Magic.

(In other words, this will give you the true extension, even if the file's extension is wrong.)

Note: Prefers long extensions (eg: jpeg over jpg)

TODO: rename type => magicext?


1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
# File 'lib/epitools/path.rb', line 1356

def type
  @cached_type ||= begin

    if file? or symlink?

      ext   = self.ext
      magic = self.magic

      if ext and magic
        if magic.extensions.include? ext
          ext
        else
          magic.ext # in case the supplied extension is wrong...
        end
      elsif !ext and magic
        magic.ext
      elsif ext and !magic
        ext
      else # !ext and !magic
        :unknown
      end

    elsif dir?
      :directory
    end

  end
end

#unmarshalObject


681
682
683
# File 'lib/epitools/path.rb', line 681

def unmarshal
  read.unmarshal
end

#update(other) ⇒ Object


273
274
275
276
277
# File 'lib/epitools/path.rb', line 273

def update(other)
  @dirs = other.dirs
  @base = other.base
  @ext  = other.ext
end

#uri?Boolean


452
# File 'lib/epitools/path.rb', line 452

def uri?; false; end

#url?Boolean


453
# File 'lib/epitools/path.rb', line 453

def url?; uri?; end

#writable?Boolean


412
413
414
# File 'lib/epitools/path.rb', line 412

def writable?
  mode & 0o222 > 0
end

#write(data = nil) ⇒ Object

Overwrite the data in this file (accepts a string, an IO, or it can yield the file handle to a block.)


772
773
774
775
776
777
778
779
780
781
782
783
784
# File 'lib/epitools/path.rb', line 772

def write(data=nil)
  self.open("wb") do |f|
    if data and not block_given?
      if data.is_an? IO
        IO.copy_stream(data, f)
      else
        f.write(data)
      end
    else
      yield f
    end
  end
end

#write_bson(object) ⇒ Object

Serilize an object to BSON format and write it to this path


939
940
941
# File 'lib/epitools/path.rb', line 939

def write_bson(object)
  write BSON.serialize(object)
end

#write_json(object) ⇒ Object

Convert the object to JSON and write it to the file (overwriting the existing file).


889
890
891
# File 'lib/epitools/path.rb', line 889

def write_json(object)
  write object.to_json
end

#write_marshal(object) ⇒ Object

Serilize an object to Ruby Marshal format and write it to this path


929
930
931
# File 'lib/epitools/path.rb', line 929

def write_marshal(object)
  write object.marshal
end

#write_yaml(object) ⇒ Object

Convert the object to YAML and write it to the file (overwriting the existing file).


901
902
903
# File 'lib/epitools/path.rb', line 901

def write_yaml(object)
  write object.to_yaml
end

#zopen(mode = "rb") ⇒ Object

A mutation of “open” that lets you read/write gzip files, as well as regular files.

(NOTE: gzip detection is based on the filename, not the contents.)

It accepts a block just like open()!

Example:

zopen("test.txt")          #=> #<File:test.txt>
zopen("test.txt.gz")       #=> #<Zlib::GzipReader:0xb6c79424>
zopen("otherfile.gz", "w") #=> #<Zlib::GzipWriter:0x7fe30448>>
zopen("test.txt.gz") { |f| f.read } # read the contents of the .gz file, then close the file handle automatically.

810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
# File 'lib/epitools/path.rb', line 810

def zopen(mode="rb")
  # if ext == "gz"
  #   io = open(mode)
  #   case mode
  #   when "r", "rb"
  #     io = Zlib::GzipReader.new(io)
  #     def io.to_str; read; end
  #   when "w", "wb"
  #     io = Zlib::GzipWriter.new(io)
  #   else
  #     raise "Unknown mode: #{mode.inspect}. zopen only supports 'r' and 'w'."
  #   end
  # elsif bin = COMPRESSORS[ext]
  if bin = COMPRESSORS[ext]
    if which(bin)
      io = IO.popen([bin, "-d" ,"-c", path])
    else
      raise "Error: couln't find #{bin.inspect} in the path"
    end
  else
    io = open(path)
  end

  if block_given?
    result = yield(io)
    io.close
    result
  else
    io
  end

end