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.


1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
# File 'lib/epitools/path.rb', line 1543

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 '/'.


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

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


1517
1518
1519
# File 'lib/epitools/path.rb', line 1517

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

.ln_s(src, dest) ⇒ Object


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

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

.ls(path) ⇒ Object


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

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

.ls_r(path) ⇒ Object


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

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

.mkcd(path, &block) ⇒ Object

Path.mkcd(path) creates a path if it doesn't exist, and changes to it (temporarily, if a block is provided)


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

def self.mkcd(path, &block)
  path = path.to_Path unless path.is_a? Path
  path.mkdir_p unless path.exists?

  raise "Error: #{path} couldn't be created." unless path.dir?

  self.cd(path, &block)
end

.popdObject


1533
1534
1535
1536
# File 'lib/epitools/path.rb', line 1533

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

.pushd(destination) ⇒ Object


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

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

.pwdObject

The current directory


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

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


1507
1508
1509
1510
1511
# File 'lib/epitools/path.rb', line 1507

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:


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

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.)

1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
# File 'lib/epitools/path.rb', line 1590

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


1329
1330
1331
# File 'lib/epitools/path.rb', line 1329

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.)


1085
1086
1087
# File 'lib/epitools/path.rb', line 1085

def backup!
  rename(backup_file)
end

#backup_fileObject

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


1069
1070
1071
# File 'lib/epitools/path.rb', line 1069

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.


979
980
981
# File 'lib/epitools/path.rb', line 979

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.

1193
1194
1195
1196
# File 'lib/epitools/path.rb', line 1193

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

#chmod_R(mode) ⇒ Object


1204
1205
1206
1207
1208
1209
1210
1211
# File 'lib/epitools/path.rb', line 1204

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

#chown(usergroup) ⇒ Object


1198
1199
1200
1201
1202
# File 'lib/epitools/path.rb', line 1198

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

#chown_R(usergroup) ⇒ Object


1213
1214
1215
1216
1217
1218
1219
1220
1221
# File 'lib/epitools/path.rb', line 1213

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


1151
1152
1153
1154
# File 'lib/epitools/path.rb', line 1151

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


1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
# File 'lib/epitools/path.rb', line 1140

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


1132
1133
1134
1135
# File 'lib/epitools/path.rb', line 1132

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


1276
1277
1278
# File 'lib/epitools/path.rb', line 1276

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


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

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.


1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
# File 'lib/epitools/path.rb', line 1312

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.


1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
# File 'lib/epitools/path.rb', line 1294

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

969
970
971
# File 'lib/epitools/path.rb', line 969

def id3
  ID3Tag.read(io)
end

#inflateObject Also known as: gunzip

gunzip the file, returning the result as a string


1285
1286
1287
# File 'lib/epitools/path.rb', line 1285

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


1156
1157
1158
1159
1160
1161
1162
# File 'lib/epitools/path.rb', line 1156

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_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

#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

#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)


1380
1381
1382
# File 'lib/epitools/path.rb', line 1380

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

#md5Object Also known as: md5sum


1261
1262
1263
# File 'lib/epitools/path.rb', line 1261

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)


1365
1366
1367
# File 'lib/epitools/path.rb', line 1365

def mimetype
  mimetype_from_ext || magic
end

#mimetype_from_extObject

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


1373
1374
1375
# File 'lib/epitools/path.rb', line 1373

def mimetype_from_ext
  MimeMagic.by_extension(ext)
end

#mkcd(&block) ⇒ Object

Path.mkcd(self)


1128
1129
1130
# File 'lib/epitools/path.rb', line 1128

def mkcd(&block)
  Path.mkcd(self, &block)
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.


1022
1023
1024
1025
1026
1027
1028
1029
# File 'lib/epitools/path.rb', line 1022

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.


1043
1044
1045
# File 'lib/epitools/path.rb', line 1043

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.)


1077
1078
1079
# File 'lib/epitools/path.rb', line 1077

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.


1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
# File 'lib/epitools/path.rb', line 1051

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.


1336
1337
1338
1339
1340
1341
1342
# File 'lib/epitools/path.rb', line 1336

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, opts = {}) ⇒ Object

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


870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
# File 'lib/epitools/path.rb', line 870

def parse(io=self.io, forced_ext=nil, opts={})
  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, opts)
  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


896
897
898
# File 'lib/epitools/path.rb', line 896

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


953
954
955
# File 'lib/epitools/path.rb', line 953

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

#read_csv(io = self.io, opts = {}) ⇒ Object Also known as: from_csv

Parse the file as CSV


932
933
934
# File 'lib/epitools/path.rb', line 932

def read_csv(io=self.io, opts={})
  open { |io| CSV.new(io.read, opts).each }
end

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


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

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


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

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

#read_marshal(io = self.io) ⇒ Object

Parse the file as a Ruby Marshal dump


943
944
945
# File 'lib/epitools/path.rb', line 943

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

#read_xml(io = self.io) ⇒ Object

Parse the file as XML


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

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


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

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


1351
1352
1353
# File 'lib/epitools/path.rb', line 1351

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

1008
1009
1010
1011
1012
1013
1014
1015
1016
# File 'lib/epitools/path.rb', line 1008

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.


1035
1036
1037
# File 'lib/epitools/path.rb', line 1035

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


1228
1229
1230
1231
1232
1233
1234
1235
1236
# File 'lib/epitools/path.rb', line 1228

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


1251
1252
1253
# File 'lib/epitools/path.rb', line 1251

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

#sha2Object Also known as: sha2sum


1256
1257
1258
# File 'lib/epitools/path.rb', line 1256

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

#sha256Object Also known as: sha256sum


1266
1267
1268
# File 'lib/epitools/path.rb', line 1266

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


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

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

1164
1165
1166
1167
1168
1169
1170
# File 'lib/epitools/path.rb', line 1164

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)


1605
1606
1607
# File 'lib/epitools/path.rb', line 1605

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


1244
1245
1246
# File 'lib/epitools/path.rb', line 1244

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?


1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
# File 'lib/epitools/path.rb', line 1394

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


958
959
960
# File 'lib/epitools/path.rb', line 958

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).


908
909
910
# File 'lib/epitools/path.rb', line 908

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


948
949
950
# File 'lib/epitools/path.rb', line 948

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).


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

def write_yaml(object)
  write object.to_yaml
end

#zopen(mode = "rb", &block) ⇒ 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
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
# File 'lib/epitools/path.rb', line 810

def zopen(mode="rb", &block)
  # 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)
      case mode
      when "w", "wb"
        # TODO: figure out how to pipe the compressor directly a file so we don't require a block
        raise "Error: Must supply a block when writing" unless block_given?

        IO.popen([bin, "-c"], "wb+") do |compressor|
          yield(compressor)
          compressor.close_write
          open("wb") { |output| IO.copy_stream(compressor, output) }
        end
      when "r", "rb"
        if block_given?
          IO.popen([bin, "-d" ,"-c", path], "rb", &block)
        else
          IO.popen([bin, "-d" ,"-c", path], "rb")
        end
      else
        raise "Error: Mode #{mode.inspect} not recognized"
      end
    else
      raise "Error: couldn't find #{bin.inspect} in the path"
    end
  else
    # io = open(path)
    raise "Error: #{ext.inspect} is an unsupported format"
  end

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

end