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.



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

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



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

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.



1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
# File 'lib/epitools/path.rb', line 1567

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



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

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

.expand_path(orig_path) ⇒ Object

Same as File.expand_path, except preserves the trailing ‘/’.



1509
1510
1511
1512
1513
# File 'lib/epitools/path.rb', line 1509

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)



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

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



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

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

.homeObject

User’s current home directory



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

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

.ln_s(src, dest) ⇒ Object



1590
1591
1592
1593
# File 'lib/epitools/path.rb', line 1590

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

.ls(path) ⇒ Object



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

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

.ls_r(path) ⇒ Object



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

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)



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

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



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

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

.popdObject



1557
1558
1559
1560
# File 'lib/epitools/path.rb', line 1557

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

.pushd(destination) ⇒ Object



1552
1553
1554
1555
# File 'lib/epitools/path.rb', line 1552

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

.pwdObject

The current directory



1548
1549
1550
# File 'lib/epitools/path.rb', line 1548

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)



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

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



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

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:



1518
1519
1520
1521
1522
1523
# File 'lib/epitools/path.rb', line 1518

def self.tmpfile(prefix="tmp")
  # path = Path.new(Tempfile.new(prefix).path, unlink_when_garbage_collected: true)
  path = Path.new(Tempfile.new(prefix).path)
  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.)


1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
# File 'lib/epitools/path.rb', line 1614

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

.zopen(filename, mode, &block) ⇒ Object



872
873
874
# File 'lib/epitools/path.rb', line 872

def self.zopen(filename, mode, &block)
  Path.new(filename).zopen(mode, &block)
end

Instance Method Details

#/(other) ⇒ Object

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



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

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



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

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?



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

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

#=~(pattern) ⇒ Object

Match the full path against a regular expression



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

def =~(pattern)
  to_s =~ pattern
end

#[](key) ⇒ Object

Retrieve one of this file’s xattrs



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

def [](key)
  attrs[key]
end

#[]=(key, value) ⇒ Object

Set this file’s xattr



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

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



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

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



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

def atime
  lstat.atime
end

#atime=(new_atime) ⇒ Object



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

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



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

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



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

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



1108
1109
1110
# File 'lib/epitools/path.rb', line 1108

def backup!
  rename(backup_file)
end

#backup_fileObject

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



1092
1093
1094
# File 'lib/epitools/path.rb', line 1092

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

#broken_symlink?Boolean

Returns:

  • (Boolean)


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

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.



1001
1002
1003
# File 'lib/epitools/path.rb', line 1001

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

#child_of?(parent) ⇒ Boolean

Returns:

  • (Boolean)


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

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.


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

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

#chmod_R(mode) ⇒ Object



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

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

#chown(usergroup) ⇒ Object



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

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

#chown_R(usergroup) ⇒ Object



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

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



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

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



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

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



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

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

#ctimeObject



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

def ctime
  lstat.ctime
end

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

gzip the file, returning the result as a string



1299
1300
1301
# File 'lib/epitools/path.rb', line 1299

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

#dirObject Also known as: dirname, directory

The current directory (with a trailing /)



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

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=



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

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)


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

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)



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

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.



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

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

#endswith(s) ⇒ Object



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

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

#executable?Boolean Also known as: exe?

Returns:

  • (Boolean)


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

def executable?
  mode & 0o111 > 0
end

#exists?Boolean Also known as: exist?

fstat

Returns:

  • (Boolean)


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

def exists?
  File.exist? path
end

#extsObject



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

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

#file?Boolean

Returns:

  • (Boolean)


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

def file?
  File.file? path
end

#filenameObject



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

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

#filename=(newfilename) ⇒ Object



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

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)



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

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.



1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
# File 'lib/epitools/path.rb', line 1335

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.



1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
# File 'lib/epitools/path.rb', line 1317

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



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

def hash; path.hash; end

#hidden?Boolean

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

Returns:

  • (Boolean)


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

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


991
992
993
# File 'lib/epitools/path.rb', line 991

def id3
  ID3Tag.read(io)
end

#inflateObject Also known as: gunzip

gunzip the file, returning the result as a string



1308
1309
1310
# File 'lib/epitools/path.rb', line 1308

def inflate
  Zlib.inflate(read)
end

#initialize_copy(other) ⇒ Object



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

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



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

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

#join(other) ⇒ Object

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



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

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

#ln_s(dest) ⇒ Object



1179
1180
1181
1182
1183
1184
1185
# File 'lib/epitools/path.rb', line 1179

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



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

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



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

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



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

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



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

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



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

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

#lstatObject



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

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)



1403
1404
1405
# File 'lib/epitools/path.rb', line 1403

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

#md5Object Also known as: md5sum



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

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)



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

def mimetype
  mimetype_from_ext || magic
end

#mimetype_from_extObject

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



1396
1397
1398
# File 'lib/epitools/path.rb', line 1396

def mimetype_from_ext
  MimeMagic.by_extension(ext)
end

#mkcd(&block) ⇒ Object

Path.mkcd(self)



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

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

#modeObject



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

def mode
  lstat.mode
end

#mtimeObject



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

def mtime
  lstat.mtime
end

#mtime=(new_mtime) ⇒ Object



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

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.



1045
1046
1047
1048
1049
1050
1051
1052
# File 'lib/epitools/path.rb', line 1045

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.



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

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

#nameObject



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

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



1100
1101
1102
# File 'lib/epitools/path.rb', line 1100

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.



1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
# File 'lib/epitools/path.rb', line 1074

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)



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

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)


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

def owner?
  raise "STUB"
end

#parentObject

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



1359
1360
1361
1362
1363
1364
1365
# File 'lib/epitools/path.rb', line 1359

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

#parent_of?(child) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#parse(io = self.io, **opts) ⇒ Object

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

The “format” option lets you specify the file format (eg: ‘Path.parse(format: “yaml”)`) You can also pass CSV parsing options (eg: `Path.parse(col_sep: “t”)`)



887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
# File 'lib/epitools/path.rb', line 887

def parse(io=self.io, **opts)
  case (opts[:format] || ext.downcase)
  when 'gz', 'bz2', 'xz'
    parse(zopen, format: 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 'tsv'
    opts[:col_sep] ||= "\t"
    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



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

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



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

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.



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

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



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

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)



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

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

#read_bson(io = self.io) ⇒ Object

Parse the file as BSON



975
976
977
# File 'lib/epitools/path.rb', line 975

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



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

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



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

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

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

Parse the file as JSON



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

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

#read_marshal(io = self.io) ⇒ Object

Parse the file as a Ruby Marshal dump



965
966
967
# File 'lib/epitools/path.rb', line 965

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

#read_xml(io = self.io) ⇒ Object

Parse the file as XML



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

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

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

Parse the file as YAML



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

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

#readable?Boolean

Returns:

  • (Boolean)


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

def readable?
  mode & 0o444 > 0
end

#realpathObject



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

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

#relativeObject

Path relative to current directory (Path.pwd)



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

def relative
  relative_to(pwd)
end

#relative?Boolean

Is this a relative path?

Returns:

  • (Boolean)


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

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



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

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



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

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

  self
end

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

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


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

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.



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

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

#reset!Object

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



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

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



1251
1252
1253
1254
1255
1256
1257
1258
1259
# File 'lib/epitools/path.rb', line 1251

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



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

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

#sha2Object Also known as: sha2sum



1279
1280
1281
# File 'lib/epitools/path.rb', line 1279

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

#sha256Object Also known as: sha256sum



1289
1290
1291
# File 'lib/epitools/path.rb', line 1289

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

#siblingsObject

Returns all neighbouring directories to this path



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

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

#sizeObject



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

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)



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

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

#startswith(s) ⇒ Object



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

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

#symlink?Boolean

Returns:

  • (Boolean)


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

def symlink?
  File.symlink? path
end


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

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


1187
1188
1189
1190
1191
1192
1193
# File 'lib/epitools/path.rb', line 1187

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)



1629
1630
1631
# File 'lib/epitools/path.rb', line 1629

def to_Path
  self
end

#touchObject

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



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

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

#truncate(offset = 0) ⇒ Object

Shrink or expand the size of a file in-place



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

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?



1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
# File 'lib/epitools/path.rb', line 1417

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



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

def unmarshal
  read.unmarshal
end

#update(other) ⇒ Object



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

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

#uri?Boolean

Returns:

  • (Boolean)


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

def uri?; false; end

#url?Boolean

Returns:

  • (Boolean)


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

def url?; uri?; end

#writable?Boolean

Returns:

  • (Boolean)


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

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



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

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



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

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



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

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



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

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



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

def write_yaml(object)
  write object.to_yaml
end

#zopen(mode = "rb", **opts, &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.


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
870
# File 'lib/epitools/path.rb', line 820

def zopen(mode="rb", **opts, &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 = (opts[:format] || 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