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

URI_RE =
%r{^[a-z\-]+://}i
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

Methods included from Array::ToCSV

#to_csv, #to_tsv

Constructor Details

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

Returns a new instance of Path.


136
137
138
139
140
141
142
143
144
145
146
# File 'lib/epitools/path.rb', line 136

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


162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/epitools/path.rb', line 162

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

    if path =~ URI_RE
      Path.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.


1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
# File 'lib/epitools/path.rb', line 1552

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


154
155
156
# File 'lib/epitools/path.rb', line 154

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

.expand_path(orig_path) ⇒ Object

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


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

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)


540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
# File 'lib/epitools/path.rb', line 540

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


158
159
160
# File 'lib/epitools/path.rb', line 158

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

.homeObject

User's current home directory


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

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

.ln_s(src, dest) ⇒ Object


1575
1576
1577
1578
# File 'lib/epitools/path.rb', line 1575

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

.ls(path) ⇒ Object


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

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

.ls_r(path) ⇒ Object


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

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)


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

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

.new(*args) ⇒ Object

Initializers


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

def self.new(*args)
  if args.first =~ URI_RE and self != Path::URI
    Path::URI.new(args.first)
  else
    super(*args)
  end
end

.popdObject


1542
1543
1544
1545
# File 'lib/epitools/path.rb', line 1542

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

.pushd(destination) ⇒ Object


1537
1538
1539
1540
# File 'lib/epitools/path.rb', line 1537

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

.pwdObject

The current directory


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

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)


568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
# File 'lib/epitools/path.rb', line 568

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


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

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:


1504
1505
1506
1507
1508
# File 'lib/epitools/path.rb', line 1504

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

1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
# File 'lib/epitools/path.rb', line 1599

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)


526
527
528
529
530
531
# File 'lib/epitools/path.rb', line 526

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


492
493
494
495
496
497
498
499
500
501
# File 'lib/epitools/path.rb', line 492

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?


503
504
505
# File 'lib/epitools/path.rb', line 503

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

#=~(pattern) ⇒ Object

Match the full path against a regular expression


1338
1339
1340
# File 'lib/epitools/path.rb', line 1338

def =~(pattern)
  to_s =~ pattern
end

#[](key) ⇒ Object

Retrieve one of this file's xattrs


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

def [](key)
  attrs[key]
end

#[]=(key, value) ⇒ Object

Set this file's xattr


627
628
629
630
# File 'lib/epitools/path.rb', line 627

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


753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
# File 'lib/epitools/path.rb', line 753

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


401
402
403
# File 'lib/epitools/path.rb', line 401

def atime
  lstat.atime
end

#atime=(new_atime) ⇒ Object


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

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


592
593
594
# File 'lib/epitools/path.rb', line 592

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


600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
# File 'lib/epitools/path.rb', line 600

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


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

def backup!
  rename(backup_file)
end

#backup_fileObject

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


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

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

#broken_symlink?Boolean

Returns:

  • (Boolean)

441
442
443
# File 'lib/epitools/path.rb', line 441

def broken_symlink?
  File.symlink?(path) and not File.exist?(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.


988
989
990
# File 'lib/epitools/path.rb', line 988

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

#child_of?(parent) ⇒ Boolean

Returns:

  • (Boolean)

464
465
466
# File 'lib/epitools/path.rb', line 464

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.

1202
1203
1204
1205
# File 'lib/epitools/path.rb', line 1202

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

#chmod_R(mode) ⇒ Object


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

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

#chown(usergroup) ⇒ Object


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

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

#chown_R(usergroup) ⇒ Object


1222
1223
1224
1225
1226
1227
1228
1229
1230
# File 'lib/epitools/path.rb', line 1222

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


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

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


1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
# File 'lib/epitools/path.rb', line 1149

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


1141
1142
1143
1144
# File 'lib/epitools/path.rb', line 1141

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

#ctimeObject


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

def ctime
  lstat.ctime
end

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

gzip the file, returning the result as a string


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

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

#dirObject Also known as: dirname, directory

The current directory (with a trailing /)


322
323
324
325
326
327
328
329
330
331
332
# File 'lib/epitools/path.rb', line 322

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=


238
239
240
241
242
243
# File 'lib/epitools/path.rb', line 238

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?

Returns:

  • (Boolean)

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

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)


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

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.


669
670
671
672
# File 'lib/epitools/path.rb', line 669

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

#endswith(s) ⇒ Object


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

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

#executable?Boolean Also known as: exe?

Returns:

  • (Boolean)

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

def executable?
  mode & 0o111 > 0
end

#exists?Boolean Also known as: exist?

fstat

Returns:

  • (Boolean)

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

def exists?
  File.exist? path
end

#extsObject


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

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

#file?Boolean

Returns:

  • (Boolean)

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

def file?
  File.file? path
end

#filenameObject


334
335
336
337
338
339
340
341
342
343
344
# File 'lib/epitools/path.rb', line 334

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

#filename=(newfilename) ⇒ Object


220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/epitools/path.rb', line 220

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)


682
683
684
685
686
687
688
# File 'lib/epitools/path.rb', line 682

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.


1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
# File 'lib/epitools/path.rb', line 1321

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.


1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
# File 'lib/epitools/path.rb', line 1303

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


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

def hash; path.hash; end

#hidden?Boolean

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

Returns:

  • (Boolean)

475
476
477
478
# File 'lib/epitools/path.rb', line 475

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

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

def id3
  ID3Tag.read(io)
end

#inflateObject Also known as: gunzip

gunzip the file, returning the result as a string


1294
1295
1296
# File 'lib/epitools/path.rb', line 1294

def inflate
  Zlib.inflate(read)
end

#initialize_copy(other) ⇒ Object


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

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


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

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

#join(other) ⇒ Object

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


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

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

#ln_s(dest) ⇒ Object


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

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


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

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


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

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


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

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


711
712
713
714
715
# File 'lib/epitools/path.rb', line 711

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


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

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

#lstatObject


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

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)


1389
1390
1391
# File 'lib/epitools/path.rb', line 1389

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

#md5Object Also known as: md5sum


1270
1271
1272
# File 'lib/epitools/path.rb', line 1270

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)


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

def mimetype
  mimetype_from_ext || magic
end

#mimetype_from_extObject

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


1382
1383
1384
# File 'lib/epitools/path.rb', line 1382

def mimetype_from_ext
  MimeMagic.by_extension(ext)
end

#mkcd(&block) ⇒ Object

Path.mkcd(self)


1137
1138
1139
# File 'lib/epitools/path.rb', line 1137

def mkcd(&block)
  Path.mkcd(self, &block)
end

#modeObject


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

def mode
  lstat.mode
end

#mtimeObject


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

def mtime
  lstat.mtime
end

#mtime=(new_mtime) ⇒ Object


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

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.


1031
1032
1033
1034
1035
1036
1037
1038
# File 'lib/epitools/path.rb', line 1031

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.


1052
1053
1054
# File 'lib/epitools/path.rb', line 1052

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

#nameObject


346
347
348
# File 'lib/epitools/path.rb', line 346

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


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

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.


1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
# File 'lib/epitools/path.rb', line 1060

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)


639
640
641
642
643
644
645
# File 'lib/epitools/path.rb', line 639

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?

Returns:

  • (Boolean)

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

def owner?
  raise "STUB"
end

#parentObject

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


1345
1346
1347
1348
1349
1350
1351
# File 'lib/epitools/path.rb', line 1345

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

#parent_of?(child) ⇒ Boolean

Returns:

  • (Boolean)

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

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


879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
# File 'lib/epitools/path.rb', line 879

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


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

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


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

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.


197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/epitools/path.rb', line 197

def path=(newpath, hints={})
  if hints[:type] or File.exist? 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


773
774
775
776
# File 'lib/epitools/path.rb', line 773

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)


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

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

#read_bson(io = self.io) ⇒ Object

Parse the file as BSON


962
963
964
# File 'lib/epitools/path.rb', line 962

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


941
942
943
# File 'lib/epitools/path.rb', line 941

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

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


922
923
924
# File 'lib/epitools/path.rb', line 922

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


911
912
913
# File 'lib/epitools/path.rb', line 911

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

#read_marshal(io = self.io) ⇒ Object

Parse the file as a Ruby Marshal dump


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

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

#read_xml(io = self.io) ⇒ Object

Parse the file as XML


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

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


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

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

#readable?Boolean

Returns:

  • (Boolean)

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

def readable?
  mode & 0o444 > 0
end

#realpathObject


1360
1361
1362
# File 'lib/epitools/path.rb', line 1360

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

#relativeObject

Path relative to current directory (Path.pwd)


311
312
313
# File 'lib/epitools/path.rb', line 311

def relative
  relative_to(pwd)
end

#relative?Boolean

Is this a relative path?

Returns:

  • (Boolean)

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

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


315
316
317
318
319
# File 'lib/epitools/path.rb', line 315

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


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

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

1017
1018
1019
1020
1021
1022
1023
1024
1025
# File 'lib/epitools/path.rb', line 1017

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.


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

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

#reset!Object

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


265
266
267
268
# File 'lib/epitools/path.rb', line 265

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


1237
1238
1239
1240
1241
1242
1243
1244
1245
# File 'lib/epitools/path.rb', line 1237

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


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

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

#sha2Object Also known as: sha2sum


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

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

#sha256Object Also known as: sha256sum


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

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

#siblingsObject

Returns all neighbouring directories to this path


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

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

#sizeObject


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

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)


488
489
490
# File 'lib/epitools/path.rb', line 488

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

#startswith(s) ⇒ Object


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

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

#symlink?Boolean

Returns:

  • (Boolean)

437
438
439
# File 'lib/epitools/path.rb', line 437

def symlink?
  File.symlink? path
end

445
446
447
448
449
450
451
452
# File 'lib/epitools/path.rb', line 445

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

1173
1174
1175
1176
1177
1178
1179
# File 'lib/epitools/path.rb', line 1173

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)


1614
1615
1616
# File 'lib/epitools/path.rb', line 1614

def to_Path
  self
end

#touchObject

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


740
741
742
743
# File 'lib/epitools/path.rb', line 740

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

#truncate(offset = 0) ⇒ Object

Shrink or expand the size of a file in-place


1253
1254
1255
# File 'lib/epitools/path.rb', line 1253

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?


1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
# File 'lib/epitools/path.rb', line 1403

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


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

def unmarshal
  read.unmarshal
end

#update(other) ⇒ Object


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

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

#uri?Boolean

Returns:

  • (Boolean)

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

def uri?; false; end

#url?Boolean

Returns:

  • (Boolean)

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

def url?; uri?; end

#writable?Boolean

Returns:

  • (Boolean)

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

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


781
782
783
784
785
786
787
788
789
790
791
792
793
# File 'lib/epitools/path.rb', line 781

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


967
968
969
# File 'lib/epitools/path.rb', line 967

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


917
918
919
# File 'lib/epitools/path.rb', line 917

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


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

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


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

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.

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
861
862
863
864
865
866
867
868
869
# File 'lib/epitools/path.rb', line 819

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