[DRE-commits] [ruby-filepath] 01/07: Imported Upstream version 0.6

Gioele Barabucci gioele-guest at alioth.debian.org
Sun Sep 22 13:23:29 UTC 2013


This is an automated email from the git hooks/post-receive script.

gioele-guest pushed a commit to branch master
in repository ruby-filepath.

commit f81309203e98affa42c4faa5d088ede51ea64b0d
Author: Gioele Barabucci <gioele at svario.it>
Date:   Sun Sep 22 11:33:45 2013 +0000

    Imported Upstream version 0.6
---
 .gitignore                      |    7 +
 .travis.yml                     |    8 +
 .yardopts                       |    4 +
 COPYING                         |  121 +++++
 Gemfile                         |    3 +
 README.md                       |  138 +++++
 Rakefile                        |   13 +
 checksums.yaml.gz               |  Bin 0 -> 269 bytes
 filepath.gemspec                |   32 ++
 lib/filepath.rb                 |    7 +
 lib/filepath/core_ext/array.rb  |   45 ++
 lib/filepath/core_ext/string.rb |   21 +
 lib/filepath/filepath.rb        |  981 ++++++++++++++++++++++++++++++++++++
 lib/filepath/filepathlist.rb    |  157 ++++++
 metadata.yml                    |  112 +++++
 spec/filepath_spec.rb           | 1054 +++++++++++++++++++++++++++++++++++++++
 spec/filepathlist_spec.rb       |  280 +++++++++++
 spec/spec_helper.rb             |   12 +
 spec/tasks.rb                   |   53 ++
 19 files changed, 3048 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0698218
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+/.bundle/
+/Gemfile.lock
+/coverage/
+/doc/
+/pkg/
+/.yardoc/
+/spec/fixtures/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a35b2ab
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+language: ruby
+rvm:
+  - "1.8.7"
+  - "1.9.3"
+  - "2.0.0"
+  - jruby-18mode
+  - jruby-19mode
+  - rbx-19mode
diff --git a/.yardopts b/.yardopts
new file mode 100644
index 0000000..f25383d
--- /dev/null
+++ b/.yardopts
@@ -0,0 +1,4 @@
+-m markdown
+--no-private
+-
+COPYING
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..0e259d4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..fa75df1
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,3 @@
+source 'https://rubygems.org'
+
+gemspec
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d39262b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,138 @@
+FilePath
+========
+
+filepath is a small library that helps dealing with files, directories and
+paths in general; a modern replacement for the standard Pathname.
+
+filepath is built around two main classes: `FilePath`, that represents paths,
+and `FilePathList`, lists of paths. The instances of these classes are
+immutable objects with dozens of convience methods for common operations such
+as calculating relative paths, concatenating paths, finding all the files in
+a directory or modifing all the extensions of a list of file names at once.
+
+
+Features and examples
+---------------------
+
+The main purpose of FilePath is to able to write
+
+    require __FILE__.as_path / 'spec' / 'tasks'
+
+instad of cumbersome code like
+
+    require File.join(File.dirname(__FILE__), ['spec', 'tasks'])
+
+The main features of FilePath are…
+
+### Path concatenation
+
+    oauth_conf = ENV['HOME'].as_path / '.config' / 'myapp' / 'oauth.ini'
+    oauth_conf.to_s  #=> "/home/gioele/.config/myapp/oauth.ini"
+
+    joe_home = ENV['HOME'].as_path / '..' / 'joe'
+    joe_home.to_raw_string #=> "/home/gioele/../joe"
+    joe_home.to_s          #=> "/home/joe"
+
+    rel1 = oauth_conf.relative_to(joe_home)
+    rel1.to_s #=> "../gioele/.config/myapp/oauth.ini"
+
+    rel2 = joe_home.relative_to(oauth_conf)
+    rel2.to_s #=> "../../../joe"
+
+### Path manipulation
+
+    image = ENV['HOME'].as_path / 'Documents' / 'images' / 'cat.png'
+    image.parent_dir.to_s  #=> "/home/gioele/Documents/images"
+    image.filename.to_s    #=> "cat.png"
+    image.extension        #=> "png"
+
+    converted_img = image.replace_extension("jpeg")
+    converted_img.to_s     #=> "/home/gioele/Documents/images/cat.jpeg"
+    convert(image, converted_img)
+
+### Path traversal
+
+    file_dir = FilePath.new("/srv/example.org/web/html/")
+    file_dir.descend do |path|
+        is = path.readable? ? "is" : "is not!"
+
+        puts "#{path} #{is} readable"
+    end
+
+produces
+
+    / is readable
+    /srv is readable
+    /srv/example.org is readable
+    /srv/example.org/web is not! readable
+    /srv/example.org/web/html is not! redable
+
+
+### Shortcuts for file and directory operations
+
+    home_dir = ENV['HOME']
+
+    files = home_dir.files
+    files.count #=> 3
+    files.each { |path| puts path.filename.to_s }
+
+produces
+
+    # .bashrc
+    # .vimrc
+    # TODO.txt
+
+Similarly,
+
+    dirs = home_dir.directories
+    dirs.count  #=> 2
+    dirs.each { |path| puts path.filename.to_s + "/"}
+
+produces
+
+    # .ssh/
+    # Documents/
+
+
+Requirements
+------------
+
+The `filepath` library does not require any external library: it relies
+complitely on functionalities available in the Ruby's core classes.
+
+The `filepath` library has been tested and found compatible with Ruby 1.8.7,
+Ruby 1.9.3 and JRuby 1.6.
+
+
+Installation
+------------
+
+    gem install filepath
+
+
+Authors
+-------
+
+* Gioele Barabucci <http://svario.it/gioele> (initial author)
+
+
+Development
+-----------
+
+Code
+: <https://github.com/gioele/filepath>
+
+Report issues
+: <https://github.com/gioele/filepath/issues>
+
+Documentation
+: <http://rubydoc.info/gems/filepath>
+
+
+License
+-------
+
+This is free software released into the public domain (CC0 license).
+
+See the `COPYING` file or <http://creativecommons.org/publicdomain/zero/1.0/>
+for more details.
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..9e1a46b
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,13 @@
+# This is free software released into the public domain (CC0 license).
+
+require 'bundler/gem_tasks'
+require 'rspec/core/rake_task'
+
+require File.join(File.dirname(__FILE__), 'spec/tasks')
+
+RSpec::Core::RakeTask.new
+
+task :default => :spec
+task :release => :spec
+
+task :spec => 'spec:fixtures:gen'
diff --git a/checksums.yaml.gz b/checksums.yaml.gz
new file mode 100644
index 0000000..912eccf
Binary files /dev/null and b/checksums.yaml.gz differ
diff --git a/filepath.gemspec b/filepath.gemspec
new file mode 100644
index 0000000..f1b20e8
--- /dev/null
+++ b/filepath.gemspec
@@ -0,0 +1,32 @@
+# coding: utf-8
+
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+
+Gem::Specification.new do |spec|
+	spec.name          = "filepath"
+	spec.version       = "0.6"
+	spec.authors       = ["Gioele Barabucci"]
+	spec.email         = ["gioele at svario.it"]
+	spec.summary       = "filepath is a small library that helps dealing with files, " +
+	                     "directories and paths in general; a modern replacement for " +
+			     "the standard Pathname."
+	spec.description   = "filepath is built around two main classes: `FilePath`, that " +
+	                     "represents paths, and `FilePathList`, lists of paths. The " +
+			     "instances of these classes are immutable objects with dozens " +
+			     "of convience methods for common operations such as calculating " +
+			     "relative paths, concatenating paths, finding all the files in " +
+			     "a directory or modifing all the extensions of a list of file "
+			     "names at once."
+	spec.homepage      = "http://github.com/gioele/filepath"
+	spec.license       = "CC0"
+
+	spec.files         = `git ls-files`.split($/)
+	spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
+	spec.test_files    = spec.files.grep(%r{^(test|spec|features)/})
+	spec.require_paths = ["lib"]
+
+	spec.add_development_dependency "bundler", "~> 1.3"
+	spec.add_development_dependency "rake"
+	spec.add_development_dependency "rspec"
+end
diff --git a/lib/filepath.rb b/lib/filepath.rb
new file mode 100644
index 0000000..f016f3e
--- /dev/null
+++ b/lib/filepath.rb
@@ -0,0 +1,7 @@
+# This is free software released into the public domain (CC0 license).
+
+require 'filepath/filepath.rb'
+require 'filepath/filepathlist.rb'
+require 'filepath/core_ext/array.rb'
+require 'filepath/core_ext/string.rb'
+
diff --git a/lib/filepath/core_ext/array.rb b/lib/filepath/core_ext/array.rb
new file mode 100644
index 0000000..8b11570
--- /dev/null
+++ b/lib/filepath/core_ext/array.rb
@@ -0,0 +1,45 @@
+# This is free software released into the public domain (CC0 license).
+
+
+class Array
+	# Generates a path using the elements of an array as path segments.
+	#
+	# `[a, b, c].as_path` is equivalent to `FilePath.join(a, b, c)`.
+	#
+	# @example FilePath from an array of strings
+	#
+	#     ["/", "foo", "bar"].as_path #=> </foo/bar>
+	#
+	# @example FilePath from an array of strings and other FilePaths
+	#
+	#     server_dir = config["root_dir"] / "server"
+	#     ["..", config_dir, "secret"].as_path #=> <../config/server/secret>
+	#
+	# @return [FilePath] a new path generated using the element as path
+	#         segments
+	#
+	# @note FIXME: `#as_path` should be `#to_path` but that method name
+	#       is already used
+
+	def as_path
+		FilePath.join(self)
+	end
+
+
+	# Generates a path list from an array of paths.
+	#
+	# The elements of the array must respond to `#as_path`.
+	#
+	# `ary.as_path` is equivalent to `FilePathList.new(ary)`.
+	#
+	# @return [FilePathList] a new path list containing the elements of
+	#                        the array as FilePaths
+	#
+	# @see String#as_path
+	# @see Array#as_path
+	# @see FilePath#as_path
+
+	def as_path_list
+		FilePathList.new(self)
+	end
+end
diff --git a/lib/filepath/core_ext/string.rb b/lib/filepath/core_ext/string.rb
new file mode 100644
index 0000000..6738177
--- /dev/null
+++ b/lib/filepath/core_ext/string.rb
@@ -0,0 +1,21 @@
+# This is free software released into the public domain (CC0 license).
+
+
+class String
+	# Generates a path from a String.
+	#
+	# `"/a/b/c".as_path` is equivalent to `FilePath.new("/a/b/c")`.
+	#
+	# @example FilePath from a string
+	#
+	#     "/etc/ssl/certs".as_path #=> </etc/ssl/certs>
+	#
+	# @return [FilePath] a new path generated from the string
+	#
+	# @note FIXME: `#as_path` should be `#to_path` but that method name
+	#       is already used
+
+	def as_path
+		FilePath.new(self)
+	end
+end
diff --git a/lib/filepath/filepath.rb b/lib/filepath/filepath.rb
new file mode 100644
index 0000000..de3fbdb
--- /dev/null
+++ b/lib/filepath/filepath.rb
@@ -0,0 +1,981 @@
+# This is free software released into the public domain (CC0 license).
+
+
+class FilePath
+	SEPARATOR = '/'.freeze
+
+	def initialize(path)
+		if path.is_a? FilePath
+			@segments = path.segments
+		elsif path.is_a? Array
+			@segments = path
+		else
+			@segments = split_path_string(path.to_str)
+		end
+	end
+
+	# @private
+	attr_reader :segments
+
+
+	# Creates a FilePath joining the given segments.
+	#
+	# @return [FilePath] a FilePath created joining the given segments
+
+	def FilePath.join(*raw_paths)
+		if (raw_paths.count == 1) && (raw_paths.first.is_a? Array)
+			raw_paths = raw_paths.first
+		end
+
+		paths = raw_paths.map { |p| p.as_path }
+
+		segs = []
+		paths.each { |path| segs += path.segments }
+
+		return FilePath.new(segs)
+	end
+
+
+	# Appends another path to the current path.
+	#
+	# @example Append a string
+	#
+	#    "a/b".as_path / "c" #=> <a/b/c>
+	#
+	# @example Append another FilePath
+	#
+	#    home = (ENV["HOME"] || "/root").as_path
+	#    conf_dir = '.config'.as_path
+	#
+	#    home / conf_dir #=> </home/user/.config>
+	#
+	# @param [FilePath, String] extra_path the path to be appended to the
+	#                                      current path
+	#
+	# @return [FilePath] a new path with the given path appended
+
+	def /(extra_path)
+		return FilePath.join(self, extra_path)
+	end
+
+
+	# Append multiple paths to the current path.
+	#
+	# @return [FilePath] a new path with all the paths appended
+
+	def join(*extra_paths)
+		return FilePath.join(self, *extra_paths)
+	end
+
+
+	# An alias for {FilePath#/}.
+	#
+	# @deprecated Use the {FilePath#/} (slash) method instead. This method
+	#             does not show clearly if a path is being added or if a
+	#             string should be added to the filename
+
+	def +(extra_path)
+		warn "FilePath#+ is deprecated, use FilePath#/ instead."
+		return self / extra_path
+	end
+
+
+	# Calculates the relative path from a given directory.
+	#
+	# @example relative paths between relative paths
+	#
+	#     posts_dir = "posts".as_path
+	#     images_dir = "static/images".as_path
+	#
+	#     logo = images_dir / 'logo.png'
+	#
+	#     logo.relative_to(posts_dir) #=> <../static/images/logo.png>
+	#
+	# @example relative paths between absolute paths
+	#
+	#     home_dir = "/home/gioele".as_path
+	#     docs_dir = "/home/gioele/Documents".as_path
+	#     tmp_dir = "/tmp".as_path
+	#
+	#     docs_dir.relative_to(home_dir) #=> <Documents>
+	#     home_dir.relative_to(docs_dir) #=> <..>
+	#
+	#     tmp_dir.relative_to(home_dir) #=> <../../tmp>
+	#
+	# @param [FilePath, String] base the directory to use as base for the
+	#                                relative path
+	#
+	# @return [FilePath] the relative path
+	#
+	# @note this method operates on the normalized paths
+	#
+	# @see #relative_to_file
+
+	def relative_to(base)
+		base = base.as_path
+
+		if self.absolute? != base.absolute?
+			self_abs = self.absolute? ? "absolute" : "relative"
+			base_abs = base.absolute? ? "absolute" : "relative"
+			msg = "cannot compare: "
+			msg += "`#{self}` is #{self_abs} while "
+			msg += "`#{base}` is #{base_abs}"
+			raise ArgumentError, msg
+		end
+
+		self_segs = self.normalized_segments
+		base_segs = base.normalized_segments
+
+		base_segs_tmp = base_segs.dup
+		num_same = self_segs.find_index do |seg|
+			base_segs_tmp.delete_at(0) != seg
+		end
+
+		# find_index returns nil if `self` is a subset of `base`
+		num_same ||= self_segs.length
+
+		num_parent_dirs = base_segs.length - num_same
+		left_in_self = self_segs[num_same..-1]
+
+		segs = [".."] * num_parent_dirs + left_in_self
+		normalized_segs = normalized_relative_segs(segs)
+
+		return FilePath.join(normalized_segs)
+	end
+
+	# Calculates the relative path from a given file.
+	#
+	# @example relative paths between relative paths
+	#
+	#     post = "posts/2012-02-14-hello.html".as_path
+	#     images_dir = "static/images".as_path
+	#
+	#     rel_img_dir = images_dir.relative_to_file(post)
+	#     rel_img_dir.to_s #=> "../static/images"
+	#
+	#     logo = rel_img_dir / 'logo.png' #=> <../static/images/logo.png>
+	#
+	# @example relative paths between absolute paths
+	#
+	#     rc_file = "/home/gioele/.bashrc".as_path
+	#     tmp_dir = "/tmp".as_path
+	#
+	#     tmp_dir.relative_to_file(rc_file) #=> <../../tmp>
+	#
+	# @param [FilePath, String] base_file the file to use as base for the
+	#                                     relative path
+	#
+	# @return [FilePath] the relative path
+	#
+	# @see #relative_to
+
+	def relative_to_file(base_file)
+		return relative_to(base_file.as_path.parent_dir)
+	end
+
+
+	# The filename component of the path.
+	#
+	# The filename is the component of a path that appears after the last
+	# path separator.
+	#
+	# @return [FilePath] the filename
+
+	def filename
+		segs = self.normalized_segments
+
+		if self.root? || segs.empty?
+			return ''.as_path
+		end
+
+		filename = segs.last
+		return filename.as_path
+	end
+
+	alias :basename :filename
+
+
+	# The dir that contains the file
+	#
+	# @return [FilePath] the path of the parent dir
+
+	def parent_dir
+		return self / '..'
+	end
+
+
+	# Replace the path filename with the supplied path.
+	#
+	# @example
+	#
+	#     post = "posts/2012-02-16-hello-world/index.md".as_path
+	#     style = post.with_filename("style.css")
+	#     style.to_s #=> "posts/2012-02-16-hello-world/style.css"
+	#
+	# @param [FilePath, String] new_path the path to be put in place of
+	#                                    the current filename
+	#
+	# @return [FilePath] a path with the supplied path instead of the
+	#                    current filename
+	#
+	# @see #filename
+	# @see #with_extension
+
+	def with_filename(new_path)
+		dir = self.parent_dir
+		return dir / new_path
+	end
+
+	alias :with_basename :with_filename
+	alias :replace_filename :with_filename
+	alias :replace_basename :with_filename
+
+
+	# The extension of the file.
+	#
+	# The extension of a file are the characters after the last dot.
+	#
+	# @return [String] the extension of the file or nil if the file has no
+	#                  extension
+	#
+	# @see #extension?
+
+	def extension
+		filename = @segments.last
+
+		num_dots = filename.count('.')
+
+		if num_dots.zero?
+			ext = nil
+		elsif filename.start_with?('.') && num_dots == 1
+			ext = nil
+		elsif filename.end_with?('.')
+			ext = ''
+		else
+			ext = filename.split('.').last
+		end
+
+		return ext
+	end
+
+	alias :ext :extension
+
+
+	# @overload extension?(ext)
+	#     @param [String, Regexp] ext the extension to be matched
+	#
+	#     @return whether the file extension matches the given extension
+	#
+	# @overload extension?
+	#     @return whether the file has an extension
+
+	def extension?(ext = nil)
+		cur_ext = self.extension
+
+		if ext.nil?
+			return !cur_ext.nil?
+		else
+			if ext.is_a? Regexp
+				return !cur_ext.match(ext).nil?
+			else
+				return cur_ext == ext
+			end
+		end
+	end
+
+	alias :ext? :extension?
+
+
+	# Replaces or removes the file extension.
+	#
+	# @see #extension
+	# @see #extension?
+	# @see #without_extension
+	# @see #with_filename
+	#
+	# @overload with_extension(new_ext)
+	#     Replaces the file extension with the supplied one. If the file
+	#     has no extension it is added to the file name together with a dot.
+	#
+	#     @example Extension replacement
+	#
+	#         src_path = "pages/about.markdown".as_path
+	#         html_path = src_path.with_extension("html")
+	#         html_path.to_s #=> "pages/about.html"
+	#
+	#     @example Extension addition
+	#
+	#         base = "style/main-style".as_path
+	#         sass_style = base.with_extension("sass")
+	#         sass_style.to_s #=> "style/main-style.sass"
+	#
+	#     @param [String] new_ext the new extension
+	#
+	#     @return [FilePath] a new path with the replaced extension
+	#
+	# @overload with_extension
+	#     Removes the file extension if present.
+	#
+	#     The {#without_extension} method provides the same functionality
+	#     but has a more meaningful name.
+	#
+	#     @example
+	#
+	#         post_file = "post/welcome.html"
+	#         post_url = post_file.with_extension(nil)
+	#         post_url.to_s #=> "post/welcome"
+	#
+	#     @return [FilePath] a new path without the extension
+
+	def with_extension(new_ext) # FIXME: accept block
+		orig_filename = filename.to_s
+
+		if !self.extension?
+			if new_ext.nil?
+				new_filename = orig_filename
+			else
+				new_filename = orig_filename + '.' + new_ext
+			end
+		else
+			if new_ext.nil?
+				pattern = /\.[^.]*?\Z/
+				new_filename = orig_filename.sub(pattern, '')
+			else
+				pattern = Regexp.new('.' + extension + '\\Z')
+				new_filename = orig_filename.sub(pattern, '.' + new_ext)
+			end
+		end
+
+		segs = @segments[0..-2]
+		segs << new_filename
+
+		return FilePath.new(segs)
+	end
+
+	alias :replace_extension :with_extension
+	alias :replace_ext :with_extension
+	alias :sub_ext :with_extension
+
+
+	# Removes the file extension if present.
+	#
+	# @example
+	#
+	#     post_file = "post/welcome.html"
+	#     post_url = post_file.without_extension
+	#     post_url.to_s #=> "post/welcome"
+	#
+	# @return [FilePath] a new path without the extension
+	#
+	# @see #with_extension
+
+	def without_extension
+		return with_extension(nil)
+	end
+
+	alias :remove_ext :without_extension
+	alias :remove_extension :without_extension
+
+
+	# Matches a pattern against this path.
+	#
+	# @param [Regexp, Object] pattern the pattern to match against
+	#                                 this path
+	#
+	# @return [Fixnum, nil] the position of the pattern in the path, or
+	#                       nil if there is no match
+	#
+	# @note this method operates on the normalized path
+
+	def =~(pattern)
+		return self.to_s =~ pattern
+	end
+
+
+	# Is this path pointing to the root directory?
+	#
+	# @return whether the path points to the root directory
+	#
+	# @note this method operates on the normalized paths
+
+	def root?
+		return self.normalized_segments == [SEPARATOR] # FIXME: windows, mac
+	end
+
+
+	# Is this path absolute?
+	#
+	# @example
+	#
+	#     "/tmp".absolute?   #=> true
+	#     "tmp".absolute?    #=> false
+	#     "../tmp".absolute? #=> false
+	#
+	# FIXME: document what an absolute path is.
+	#
+	# @return whether the current path is absolute
+	#
+	# @see #relative?
+
+	def absolute?
+		return @segments.first == SEPARATOR # FIXME: windows, mac
+	end
+
+
+	# Is this path relative?
+	#
+	# @example
+	#
+	#     "/tmp".relative?   #=> false
+	#     "tmp".relative?    #=> true
+	#     "../tmp".relative? #=> true
+	#
+	# FIXME: document what a relative path is.
+	#
+	# @return whether the current path is relative
+	#
+	# @see #absolute?
+
+	def relative?
+		return !self.absolute?
+	end
+
+
+	# Simplify paths that contain `.` and `..`.
+	#
+	# The resulting path will be in normal form.
+	#
+	# @example
+	#
+	#     path = $ENV["HOME"] / ".." / "jack" / "."
+	#
+	#     path #=> </home/gioele/../jack/.>
+	#     path.normalized #=> </home/jack>
+	#
+	# FIXME: document what normal form is.
+	#
+	# @return [FilePath] a new path that does not contain `.` or `..`
+	#                    segments.
+
+	def normalized
+		return FilePath.join(self.normalized_segments)
+	end
+
+	alias :normalised :normalized
+
+	# Iterates over all the path segments, from the leftmost to the
+	# rightmost.
+	#
+	# @example
+	#
+	#     web_dir = "/srv/example.org/web/html".as_path
+	#     web_dir.each_segment do |seg|
+	#         puts seg
+	#     end
+	#
+	#     # produces
+	#     #
+	#     # /
+	#     # srv
+	#     # example.org
+	#     # web
+	#     # html
+	#
+	# @yield [path] TODO
+	#
+	# @return [FilePath] the path itself.
+	#
+	# @see #ascend
+	# @see #descend
+
+	def each_segment(&block)
+		@segments.each(&block)
+		return self
+	end
+
+
+	# Iterates over all the path directories, from the current path to
+	# the root.
+	#
+	# @example
+	#
+	#     web_dir = "/srv/example.org/web/html/".as_path
+	#     web_dir.ascend do |path|
+	#         is = path.readable? ? "is" : "is NOT"
+	#
+	#         puts "#{path} #{is} readable"
+	#     end
+	#
+	#     # produces
+	#     #
+	#     # /srv/example.org/web/html is NOT redable
+	#     # /srv/example.org/web is NOT readable
+	#     # /srv/example.org is readable
+	#     # /srv is readable
+	#     # / is readable
+	#
+	# @param max_depth the maximum depth to ascend to, nil to ascend
+	#                  without limits.
+	#
+	# @yield [path] TODO
+	#
+	# @return [FilePath] the path itself.
+	#
+	# @see #each_segment
+	# @see #descend
+
+	def ascend(max_depth = nil, &block)
+		iterate(max_depth, :reverse_each, &block)
+	end
+
+
+	# Iterates over all the directory that lead to the current path.
+	#
+	# @example
+	#
+	#     web_dir = "/srv/example.org/web/html/".as_path
+	#     web_dir.descend do |path|
+	#         is = path.readable? ? "is" : "is NOT"
+	#
+	#         puts "#{path} #{is} readable"
+	#     end
+	#
+	#     # produces
+	#     #
+	#     # / is readable
+	#     # /srv is readable
+	#     # /srv/example.org is readable
+	#     # /srv/example.org/web is NOT readable
+	#     # /srv/example.org/web/html is NOT redable
+	#
+	# @param max_depth the maximum depth to descent to, nil to descend
+	#                  without limits.
+	#
+	# @yield [path] TODO
+	#
+	# @return [FilePath] the path itself.
+	#
+	# @see #each_segment
+	# @see #ascend
+
+	def descend(max_depth = nil, &block)
+		iterate(max_depth, :each, &block)
+	end
+
+
+	# @private
+	def iterate(max_depth, method, &block)
+		max_depth ||= @segments.length
+		(1..max_depth).send(method) do |limit|
+			segs = @segments.take(limit)
+			yield FilePath.join(segs)
+		end
+
+		return self
+	end
+
+
+	# This path converted to a String.
+	#
+	# @example differences between #to_raw_string and #to_s
+	#
+	#    path = "/home/gioele/.config".as_path / ".." / ".cache"
+	#    path.to_raw_string #=> "/home/gioele/config/../.cache"
+	#    path.to_s #=> "/home/gioele/.cache"
+	#
+	# @return [String] this path converted to a String
+	#
+	# @see #to_s
+
+	def to_raw_string
+		@to_raw_string ||= join_segments(@segments)
+	end
+
+	alias :to_raw_str :to_raw_string
+
+
+	# @return [String] this path converted to a String
+	#
+	# @note this method operates on the normalized path
+
+	def to_s
+		to_str
+	end
+
+
+	# @private
+	def to_str
+		@to_str ||= join_segments(self.normalized_segments)
+	end
+
+
+	# @return [FilePath] the path itself.
+	def as_path
+		self
+	end
+
+
+	# @private
+	def inspect
+		return '<' +  self.to_raw_string + '>'
+	end
+
+
+	# Checks whether two paths are equivalent.
+	#
+	# Two paths are equivalent when they have the same normalized segments.
+	#
+	# A relative and an absolute path will always be considered different.
+	# To compare relative paths to absolute path, expand first the relative
+	# path using {#absolute_path} or {#real_path}.
+	#
+	# @example
+	#
+	#     path1 = "foo/bar".as_path
+	#     path2 = "foo/bar/baz".as_path
+	#     path3 = "foo/bar/baz/../../bar".as_path
+	#
+	#     path1 == path2            #=> false
+	#     path1 == path2.parent_dir #=> true
+	#     path1 == path3            #=> true
+	#
+	# @param [FilePath, String] other the other path to compare
+	#
+	# @return [boolean] whether the other path is equivalent to the current path
+	#
+	# @note this method compares the normalized versions of the paths
+
+	def ==(other)
+		return self.normalized_segments == other.as_path.normalized_segments
+	end
+
+
+	# @private
+	def eql?(other)
+		if self.equal?(other)
+			return true
+		elsif self.class != other.class
+			return false
+		end
+
+		return @segments == other.segments
+	end
+
+	# @private
+	def <=>(other)
+		return self.normalized_segments <=> other.normalized_segments
+	end
+
+	# @private
+	def hash
+		return @segments.hash
+	end
+
+	# @private
+	def split_path_string(raw_path)
+		segments = raw_path.split(SEPARATOR) # FIXME: windows, mac
+
+		if raw_path == SEPARATOR
+			segments << SEPARATOR
+		end
+
+		if !segments.empty? && segments.first.empty?
+			segments[0] = SEPARATOR
+		end
+
+		return segments
+	end
+
+	# @private
+	def normalized_segments
+		@normalized_segments ||= normalized_relative_segs(@segments)
+	end
+
+	# @private
+	def normalized_relative_segs(orig_segs)
+		segs = orig_segs.dup
+
+		i = 0
+		while (i < segs.length)
+			if segs[i] == '..' && segs[i-1] == SEPARATOR
+				# remove '..' segments following a root delimiter
+				segs.delete_at(i)
+				i -= 1
+			elsif segs[i] == '..' && segs[i-1] != '..' && i >= 1
+				# remove every segment followed by a ".." marker
+				segs.delete_at(i)
+				segs.delete_at(i-1)
+				i -= 2
+			elsif segs[i] == '.'
+				# remove "current dir" markers
+				segs.delete_at(i)
+				i -= 1
+			end
+			i += 1
+		end
+
+		return segs
+	end
+
+	# @private
+	def join_segments(segs)
+		# FIXME: windows, mac
+		# FIXME: avoid string substitutions and regexen
+		return segs.join(SEPARATOR).sub(%r{^//}, SEPARATOR).sub(/\A\Z/, '.')
+	end
+
+	module MethodDelegation
+		# @private
+		def define_io_method(filepath_method, io_method = nil)
+			io_method ||= filepath_method
+			define_method(filepath_method) do |*args, &block|
+				return File.send(io_method, self, *args, &block)
+			end
+		end
+
+		# @private
+		def define_file_method(filepath_method, file_method = nil)
+			file_method ||= filepath_method
+			define_method(filepath_method) do |*args|
+				all_args = args + [self]
+				return File.send(file_method, *all_args)
+			end
+		end
+
+		# @private
+		def define_filetest_method(filepath_method, filetest_method = nil)
+			filetest_method ||= filepath_method
+			define_method(filepath_method) do
+				return FileTest.send(filetest_method, self)
+			end
+		end
+	end
+
+	module MetadataInfo
+		extend MethodDelegation
+
+		define_file_method :stat
+
+		define_file_method :lstat
+
+		define_file_method :atime
+
+		define_file_method :ctime
+
+		define_file_method :mtime
+	end
+
+	module MetadataChanges
+		extend MethodDelegation
+
+		# utime(atime, mtime)
+		define_file_method :utime
+		alias :chtime :utime
+
+		# chmod(mode)
+		define_file_method :chmod
+
+		# lchmod(mode)
+		define_file_method :lchmod
+
+		# chown(owner_id, group_id)
+		define_file_method :chown
+
+		# lchown(owner_id, group_id)
+		define_file_method :lchown
+	end
+
+	module MetadataTests
+		extend MethodDelegation
+
+		define_filetest_method :file?
+
+		define_filetest_method :link?, :symlink?
+		alias :symlink? :link?
+
+		define_filetest_method :directory?
+
+		define_filetest_method :pipe?
+
+		define_filetest_method :socket?
+
+		define_filetest_method :blockdev?
+
+		define_filetest_method :chardev?
+
+		define_filetest_method :exists?
+		alias :exist? :exists?
+
+		define_filetest_method :readable?
+
+		define_filetest_method :writeable?
+
+		define_filetest_method :executable?
+
+		define_filetest_method :setgid?
+
+		define_filetest_method :setuid?
+
+		define_filetest_method :sticky?
+
+		def hidden?
+			@segments.last.start_with?('.') # FIXME: windows, mac
+		end
+	end
+
+	module FilesystemInfo
+		def absolute_path(base_dir = Dir.pwd) # FIXME: rename to `#absolute`?
+			if self.absolute?
+				return self
+			end
+
+			return base_dir.as_path / self
+		end
+
+		def real_path(base_dir = Dir.pwd)
+			path = absolute_path(base_dir)
+
+			return path.resolve_link
+		end
+
+		alias :realpath :real_path
+
+		def resolve_link
+			return File.readlink(self).as_path
+		end
+	end
+
+	module FilesystemChanges
+		def touch
+			self.open('a') do ; end
+			File.utime(File.atime(self), Time.now, self)
+		end
+	end
+
+	module FilesystemTests
+		def mountpoint?
+			if !directory? || !exists?
+				return false
+			end
+
+			if root?
+				return true
+			end
+
+			return self.lstat.dev != parent_dir.lstat.dev
+		end
+	end
+
+	module ContentInfo
+		extend MethodDelegation
+
+		define_io_method :read
+
+		if IO.respond_to? :binread
+			define_io_method :binread
+		else
+			alias :binread :read
+		end
+
+		define_io_method :readlines
+
+		define_io_method :size
+	end
+
+	module ContentChanges
+		extend MethodDelegation
+
+		define_io_method :open
+
+		def write(content)
+			open('w') do |file|
+				file.write(content)
+			end
+		end
+
+		def append(content)
+			open('a') do |file|
+				file.write(content)
+			end
+		end
+
+		define_io_method :file_truncate, :truncate
+
+		def truncate(*args)
+			if args.empty?
+				args << 0
+			end
+
+			file_truncate(*args)
+		end
+	end
+
+	module ContentTests
+		extend MethodDelegation
+
+		define_file_method :empty?, :zero?
+		alias :zero? :empty?
+	end
+
+	module SearchMethods
+		def entries(pattern = '*', recursive = false)
+			if !self.directory?
+				raise Errno::ENOTDIR.new(self)
+			end
+
+			glob = self
+			glob /= '**' if recursive
+			glob /= pattern
+
+			raw_entries = Dir.glob(glob)
+			entries = FilePathList.new(raw_entries)
+
+			return entries
+		end
+		alias :glob :entries
+
+		def find(pattern = nil, recursive = true, &block)
+			if !pattern.nil? && pattern.respond_to?(:to_str)
+				return entries(pattern, recursive)
+			end
+
+			if !block_given?
+				block = proc { |e| e =~ pattern }
+			end
+
+			return entries('*', true).select { |e| block.call(e) }
+		end
+
+		def files(recursive = false)
+			entries('*', recursive).select_entries(:file)
+		end
+
+		def links(recursive = false)
+			entries('*', recursive).select_entries(:link)
+		end
+
+		def directories(recursive = false)
+			entries('*', recursive).select_entries(:directory)
+		end
+	end
+
+	module EnvironmentInfo
+		def FilePath.getwd
+			return Dir.getwd.as_path
+		end
+	end
+
+	include MetadataInfo
+	include MetadataChanges
+	include MetadataTests
+
+	include FilesystemInfo
+	include FilesystemChanges
+	include FilesystemTests
+
+	include ContentInfo
+	include ContentChanges
+	include ContentTests
+
+	include SearchMethods
+end
diff --git a/lib/filepath/filepathlist.rb b/lib/filepath/filepathlist.rb
new file mode 100644
index 0000000..8925d25
--- /dev/null
+++ b/lib/filepath/filepathlist.rb
@@ -0,0 +1,157 @@
+# This is free software released into the public domain (CC0 license).
+
+
+class FilePathList
+	include Enumerable
+
+	SEPARATOR = ':'.freeze
+
+	def initialize(raw_entries = nil)
+		raw_entries ||= []
+		@entries = raw_entries.map { |e| e.as_path }
+	end
+
+	def select_entries(type)
+		raw_entries = @entries.delete_if { |e| !e.send(type.to_s + '?') }
+		return FilePathList.new(raw_entries)
+	end
+
+	def files
+		return select_entries(:file)
+	end
+
+	def links
+		return select_entries(:link)
+	end
+
+	def directories
+		return select_entries(:directory)
+	end
+
+	def /(extra_path)
+		return self.map { |path| path / extra_path }
+	end
+
+	def +(extra_entries)
+		return FilePathList.new(@entries + extra_entries.to_a)
+	end
+
+	def -(others)
+		remaining_entries = @entries - others.as_path_list.to_a
+
+		return FilePathList.new(remaining_entries)
+	end
+
+	def <<(extra_path)
+		return FilePathList.new(@entries + [extra_path.as_path])
+	end
+
+	def *(other_list)
+		if !other_list.is_a? FilePathList
+			other_list = FilePathList.new(Array(other_list))
+		end
+		other_entries = other_list.entries
+		paths = @entries.product(other_entries).map { |p1, p2| p1 / p2 }
+		return FilePathList.new(paths)
+	end
+
+	def remove_common_segments
+		all_segs = @entries.map(&:segments)
+		max_length = all_segs.map(&:length).min
+
+		idx_different = nil
+
+		(0..max_length).each do |i|
+			segment = all_segs.first[i]
+
+			different = all_segs.any? { |segs| segs[i] != segment }
+			if different
+				idx_different = i
+				break
+			end
+		end
+
+		idx_different ||= max_length
+
+		remaining_segs = all_segs.map { |segs| segs[idx_different..-1] }
+
+		return FilePathList.new(remaining_segs)
+	end
+
+	# @return [FilePathList] the path list itself
+
+	def as_path_list
+		self
+	end
+
+	def to_a
+		@entries
+	end
+
+	def to_s
+		@to_s ||= @entries.map(&:to_str).join(SEPARATOR)
+	end
+
+
+	# @private
+	def inspect
+		@entries.inspect
+	end
+
+	def ==(other)
+		@entries == other.as_path_list.to_a
+	end
+
+	module ArrayMethods
+		# @private
+		def self.define_array_method(name)
+			define_method(name) do |*args, &block|
+				return @entries.send(name, *args, &block)
+			end
+		end
+
+		define_array_method :[]
+
+		define_array_method :empty?
+
+		define_array_method :include?
+
+		define_array_method :each
+
+		define_array_method :all?
+
+		define_array_method :any?
+
+		define_array_method :none?
+
+		define_array_method :size
+	end
+
+	module EntriesMethods
+		def map(&block)
+			mapped_entries = @entries.map(&block)
+			return FilePathList.new(mapped_entries)
+		end
+
+		def select(pattern = nil, &block)
+			if !block_given?
+				block = proc { |e| e =~ pattern }
+			end
+
+			remaining_entries = @entries.select { |e| block.call(e) }
+
+			return FilePathList.new(remaining_entries)
+		end
+
+		def exclude(pattern = nil, &block)
+			if block_given?
+				select { |e| !block.call(e) }
+			else
+				select { |e| !(e =~ pattern) }
+			end
+		end
+	end
+
+	include ArrayMethods
+	include EntriesMethods
+end
diff --git a/metadata.yml b/metadata.yml
new file mode 100644
index 0000000..176f704
--- /dev/null
+++ b/metadata.yml
@@ -0,0 +1,112 @@
+--- !ruby/object:Gem::Specification
+name: filepath
+version: !ruby/object:Gem::Version
+  version: '0.6'
+platform: ruby
+authors:
+- Gioele Barabucci
+autorequire: 
+bindir: bin
+cert_chain: []
+date: 2013-09-15 00:00:00.000000000 Z
+dependencies:
+- !ruby/object:Gem::Dependency
+  name: bundler
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ~>
+      - !ruby/object:Gem::Version
+        version: '1.3'
+  type: :development
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ~>
+      - !ruby/object:Gem::Version
+        version: '1.3'
+- !ruby/object:Gem::Dependency
+  name: rake
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - '>='
+      - !ruby/object:Gem::Version
+        version: '0'
+  type: :development
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - '>='
+      - !ruby/object:Gem::Version
+        version: '0'
+- !ruby/object:Gem::Dependency
+  name: rspec
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - '>='
+      - !ruby/object:Gem::Version
+        version: '0'
+  type: :development
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - '>='
+      - !ruby/object:Gem::Version
+        version: '0'
+description: 'filepath is built around two main classes: `FilePath`, that represents
+  paths, and `FilePathList`, lists of paths. The instances of these classes are immutable
+  objects with dozens of convience methods for common operations such as calculating
+  relative paths, concatenating paths, finding all the files in a directory or modifing
+  all the extensions of a list of file '
+email:
+- gioele at svario.it
+executables: []
+extensions: []
+extra_rdoc_files: []
+files:
+- .gitignore
+- .travis.yml
+- .yardopts
+- COPYING
+- Gemfile
+- README.md
+- Rakefile
+- filepath.gemspec
+- lib/filepath.rb
+- lib/filepath/core_ext/array.rb
+- lib/filepath/core_ext/string.rb
+- lib/filepath/filepath.rb
+- lib/filepath/filepathlist.rb
+- spec/filepath_spec.rb
+- spec/filepathlist_spec.rb
+- spec/spec_helper.rb
+- spec/tasks.rb
+homepage: http://github.com/gioele/filepath
+licenses:
+- CC0
+metadata: {}
+post_install_message: 
+rdoc_options: []
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+  requirements:
+  - - '>='
+    - !ruby/object:Gem::Version
+      version: '0'
+required_rubygems_version: !ruby/object:Gem::Requirement
+  requirements:
+  - - '>='
+    - !ruby/object:Gem::Version
+      version: '0'
+requirements: []
+rubyforge_project: 
+rubygems_version: 2.0.4
+signing_key: 
+specification_version: 4
+summary: filepath is a small library that helps dealing with files, directories and
+  paths in general; a modern replacement for the standard Pathname.
+test_files:
+- spec/filepath_spec.rb
+- spec/filepathlist_spec.rb
+- spec/spec_helper.rb
+- spec/tasks.rb
diff --git a/spec/filepath_spec.rb b/spec/filepath_spec.rb
new file mode 100644
index 0000000..a35e300
--- /dev/null
+++ b/spec/filepath_spec.rb
@@ -0,0 +1,1054 @@
+# This is free software released into the public domain (CC0 license).
+
+require File.join(File.dirname(__FILE__), 'spec_helper')
+
+describe FilePath do
+	before(:all) do
+		@root = FilePath.new(FIXTURES_DIR)
+	end
+
+	it "can be created from a string" do
+		FilePath.new("foo").should be_a FilePath
+	end
+
+	it "can be created from another FilePath" do
+		orig = FilePath.new("foo")
+		FilePath.new(orig).should be_a FilePath
+	end
+
+	describe "#/" do
+		test_data = [
+			['foo', 'bar', 'foo/bar'],
+			['foo', '.', 'foo'],
+			['foo', '..', '.'],
+			['foo/bar', 'baz', 'foo/bar/baz'],
+			['', 'foo/bar', './foo/bar'],
+		]
+		test_data.each do |base, extra, result|
+			it "concatenates `#{base}` and `#{extra}` (as String) into `#{result}`" do
+				ph = FilePath.new(base) / extra
+				ph.should == result
+			end
+		end
+
+		test_data.each do |base, extra, result|
+			it "concatenates `#{base}` and `#{extra}` (as FilePath) into `#{result}`" do
+				ph = FilePath.new(base) / FilePath.new(extra)
+				ph.should == result
+			end
+		end
+	end
+
+	describe "#+" do
+		it "is deprecated but performs as FilePath#/" do
+			p1 = FilePath.new("a")
+			p2 = FilePath.new("b")
+
+			p1.should_receive(:warn).with(/is deprecated/)
+			(p1 + p2).should == (p1 / p2)
+		end
+	end
+
+	describe "#join" do
+		test_data = [
+			['', ['bar'], './bar'],
+			['foo/quux', ['bar', 'baz'], 'foo/quux/bar/baz'],
+			['/', ['a', 'b', 'c'], '/a/b/c'],
+		]
+		test_data.each do |base, extra, result|
+			args = extra.map { |x| x.inspect }.join(',')
+			it "appends #{args} to '#{base}' to get <#{result}>" do
+				base.as_path.join(*extra).should == result
+			end
+		end
+	end
+
+	describe "filename" do
+		test_data = [
+			['/foo/bar', 'bar'],
+			['foo', 'foo'],
+			['/', ''],
+			['a/b/../../', ''],
+			['/foo/bar/.', 'bar'],
+			['a/b/../c', 'c'],
+		]
+		test_data.each do |path, result|
+			it "says that `#{result}` is the filename of `#{path}`" do
+				ph = FilePath.new(path)
+				ph.filename.should == result
+			end
+		end
+	end
+
+	describe "parent_dir" do
+		test_data = [
+			['/foo/bar', '/foo'],
+			['foo', '.'],
+			['/', '/'],
+			['/foo/bar/.', '/foo'],
+			['a/b/../c', 'a'],
+		]
+		test_data.each do |path, result|
+			it "says that `#{result}` is the parent dir of `#{path}`" do
+				ph = FilePath.new(path)
+				ph.parent_dir.should == result
+			end
+		end
+	end
+
+	describe "#relative_to" do
+		test_data = [
+			['/a/b/c', '/a/b', 'c'],
+			['/a/b/c', '/a/d', '../b/c'],
+			['/a/b/c', '/a/b/c/d', '..'],
+			['/a/b/c', '/a/b/c', '.'],
+			['a/d', 'a/b/c', '../../d'],
+			['a/e/f', 'a/b/c/d', '../../../e/f'],
+			['a/c', 'a/b/..', 'c'],
+		]
+		test_data.each do |path, base, result|
+			it "says that `#{path}` relative to `#{base}` is `#{result}`" do
+				ph = FilePath.new(path)
+				ph.relative_to(base).should == result
+			end
+		end
+
+		test_data2 = [
+			# FIXME: testare /a/b/c con ../d (bisogna prima rendere assoluto quel path)
+			['../e', '/a/b/c'],
+			['g', '/a/b/c'],
+			['/a/b/c', 'm'],
+		]
+		test_data2.each do |path, base|
+			it "raise an exception because `#{path}` and `#{base}` have different prefixes" do
+				ph = FilePath.new(path)
+				expect { ph.relative_to(base) }.to raise_error(ArgumentError)
+			end
+		end
+	end
+
+	describe "#relative_to_file" do
+				test_data = [
+			['/a/b/c', '/a/d', 'b/c'],
+			['/a/b/c', '/a/b/c/d', '.'],
+			['/a/b/c', '/a/b/c', 'c'],
+			['a/d', 'a/b/c', '../d'],
+			['a/e/f', 'a/b/c/d', '../../e/f'],
+		]
+		test_data.each do |path, base, result|
+			it "says that `#{path}` relative to the file `#{base}` is `#{result}`" do
+				ph = FilePath.new(path)
+				ph.relative_to_file(base).should == result
+			end
+		end
+	end
+
+	describe "#with_filename" do
+		test_data = [
+			['foo/bar', 'quux', 'foo/quux'],
+			['foo/baz/..', 'quux', 'quux'],
+			['/', 'foo', '/foo'],
+		]
+		test_data.each do |base, new, result|
+			it "changes `#{base}` + `#{new}` into `#{result}`" do
+				ph = FilePath.new(base)
+				ph.with_filename(new).should == result
+			end
+		end
+	end
+
+	describe "#extension" do
+		test_data = [
+			['foo.bar', 'bar'],
+			['foo.', ''],
+			['foo', nil],
+			['foo.bar/baz.buz', 'buz'],
+			['foo.bar/baz', nil],
+			['.foo', nil],
+			['.foo.conf', 'conf'],
+		]
+		test_data.each do |path, ext|
+			it "says that `#{path}` has extension `#{ext}`" do
+				FilePath.new(path).extension.should == ext
+			end
+		end
+	end
+
+	describe "#extension?" do
+		with_extension = [
+			'foo.bar',
+			'foo.',
+			'.foo.conf',
+		]
+		with_extension.each do |path|
+			it "says that <#{path}> has an extension" do
+				FilePath.new(path).extension?.should be_true
+			end
+		end
+
+		no_extension = [
+			'foo',
+			'foo.bar/baz',
+			'.foo',
+		]
+		no_extension.each do |path|
+			it "says that <#{path}> has no extension" do
+				FilePath.new(path).extension?.should be_false
+			end
+		end
+
+		extension_data = [
+			['foo.bar', 'bar'],
+			['/foo/bar.', ''],
+			['foo/bar.baz.conf', 'conf'],
+			['foo.bar.boom', /oo/],
+		]
+		extension_data.each do |path, ext|
+			it "says that <#{path}> extesions is #{ext.inspect}" do
+				FilePath.new(path).extension?(ext).should be_true
+			end
+		end
+
+		it "says that `foo.bar` extension is not `baz`" do
+			FilePath.new('foo.bar').extension?('baz').should be_false
+		end
+	end
+
+	describe "#with_extension(String)" do
+		test_data = [
+			['foo.bar', 'foo.baz'],
+			['foo.', 'foo.baz'],
+			['foo', 'foo.baz'],
+			['foo.bar/baz.buz', 'baz.baz'],
+			['foo.bar/baz', 'baz.baz'],
+		]
+		test_data.each do |path, result|
+			it "replaces `#{path}` with `baz` into `#{result}`" do
+				new = FilePath.new(path).with_extension('baz')
+				new.basename.to_s.should == result
+			end
+		end
+	end
+
+	describe "#without_extension" do
+		test_data = [
+			['foo.bar', 'foo'],
+			['foo.', 'foo'],
+			['foo', 'foo'],
+			['foo.bar/baz.buz', 'baz'],
+			['foo.bar/baz', 'baz'],
+		]
+		test_data.each do |path, result|
+			it "turns `#{path}` into `#{result}`" do
+				new = FilePath.new(path).without_extension
+				new.basename.to_s.should == result
+			end
+		end
+	end
+
+	describe "=~" do
+		it "matches `/foo/bar` with /foo/" do
+			FilePath.new('/foo/bar').should =~ /foo/
+		end
+
+		it "does not match `/foo/bar` with /baz/" do
+			FilePath.new('/foo/bar').should_not =~ /baz/
+		end
+
+		it "matches `/foo/bar` with /o\\/ba" do
+			FilePath.new('/foo/bar').should =~ /o\/b/
+		end
+
+		it "matches `/foo/bar/../quux` with /foo\\/quux/" do
+			FilePath.new('/foo/bar/../quux').should =~ /foo\/quux/
+		end
+	end
+
+	describe "#root?" do
+		it "says that </> points to the root directory" do
+			FilePath.new('/').should be_root
+		end
+
+		it "says that </..> points to the root directory" do
+			FilePath.new('/..').should be_root
+		end
+
+		it "says that <a/b> does not point to the root directory" do
+			FilePath.new('a/b').should_not be_root
+		end
+
+		it "says that </foo> does not point to the root directory" do
+			FilePath.new('/foo/bar').should_not be_root
+		end
+	end
+
+	describe "#absolute?" do
+		it "says that `/foo/bar` is absolute" do
+			FilePath.new('/foo/bar').should be_absolute
+		end
+
+		it "sasys that `foo/bar` is not absolute" do
+			FilePath.new('foo/bar').should_not be_absolute
+		end
+	end
+
+	describe "#normalized" do
+		test_data = [
+			['a', 'a'],
+			['a/b/c', 'a/b/c'],
+			['a/../c', 'c'],
+			['a/b/..', 'a'],
+			['../a', '../a'],
+			['../../a', '../../a'],
+			['../a/..', '..'],
+			['/', '/'],
+			['/..', '/'],
+			['/../../../a', '/a'],
+			['a/b/../..', '.'],
+		]
+		test_data.each do |path, result|
+			it "turns `#{path}` into `#{result}`" do
+				FilePath.new(path).normalized.to_raw_string.should == result
+			end
+		end
+	end
+
+	describe "#each_segment" do
+		it "goes through all the segments of an absolute path" do
+			steps = []
+			FilePath.new("/a/b/c").each_segment do |seg|
+				steps << seg
+			end
+
+			steps.should have(4).items
+			steps[0].should eq("/")
+			steps[1].should eq("a")
+			steps[2].should eq("b")
+			steps[3].should eq("c")
+		end
+
+		it "goes through all the segments of a relative path" do
+			steps = []
+			FilePath.new("a/b/c").each_segment do |seg|
+				steps << seg
+			end
+
+			steps.should have(3).items
+			steps[0].should eq("a")
+			steps[1].should eq("b")
+			steps[2].should eq("c")
+		end
+
+		it "returns the path itself" do
+			path = FilePath.new("/a/b/c/")
+			path.each_segment { }.should be(path)
+		end
+	end
+
+	describe "#ascend" do
+		it "goes through all the segments of an absolute path" do
+			steps = []
+			FilePath.new("/a/b/c").ascend do |seg|
+				steps << seg
+			end
+
+			steps.should have(4).items
+			steps[0].should eq("/a/b/c")
+			steps[1].should eq("/a/b")
+			steps[2].should eq("/a")
+			steps[3].should eq("/")
+		end
+
+		it "goes through all the segments of a relative path" do
+			steps = []
+			FilePath.new("a/b/c").ascend do |seg|
+				steps << seg
+			end
+
+			steps.should have(3).items
+			steps[0].should eq("a/b/c")
+			steps[1].should eq("a/b")
+			steps[2].should eq("a")
+		end
+
+		it "returns the path itself" do
+			path = FilePath.new("/a/b/c/")
+			path.ascend { }.should be(path)
+		end
+	end
+
+	describe "#descend" do
+		it "goes through all the segments of an absolute path" do
+			steps = []
+			FilePath.new("/a/b/c").descend do |seg|
+				steps << seg
+			end
+
+			steps.should have(4).items
+			steps[0].should eq("/")
+			steps[1].should eq("/a")
+			steps[2].should eq("/a/b")
+			steps[3].should eq("/a/b/c")
+		end
+
+		it "goes through all the segments of a relative path" do
+			steps = []
+			FilePath.new("a/b/c").descend do |seg|
+				steps << seg
+			end
+
+			steps.should have(3).items
+			steps[0].should eq("a")
+			steps[1].should eq("a/b")
+			steps[2].should eq("a/b/c")
+		end
+
+		it "returns the path itself" do
+			path = FilePath.new("/a/b/c/")
+			path.descend { }.should be(path)
+		end
+	end
+
+	describe "#to_s" do
+		it "works on computed absolute paths" do
+			(FilePath.new('/') / 'a' / 'b').to_s.should eql('/a/b')
+		end
+
+		it "works on computed relative paths" do
+			(FilePath.new('a') / 'b').to_s.should eql('a/b')
+		end
+
+		it "returns normalized paths" do
+			FilePath.new("/foo/bar/..").to_s.should eql('/foo')
+		end
+
+		it "returns '.' for empty paths" do
+			FilePath.new('').to_s.should eql('.')
+		end
+	end
+
+	describe "#as_path" do
+		it "returns the path itself" do
+			@root.as_path.should be(@root)
+		end
+	end
+
+	describe "#==(String)" do
+		test_data = [
+			['./', '.'],
+			['a/../b', 'b'],
+			['a/.././b', 'b'],
+			['a/./../b', 'b'],
+			['./foo', 'foo'],
+			['a/./b/c', 'a/b/c'],
+			['a/b/.', 'a/b'],
+			['a/b/', 'a/b'],
+			['../a/../b/c/d/../../e', '../b/e'],
+		]
+		test_data.each do |ver1, ver2|
+			it "says that `#{ver1}` is equivalent to `#{ver2}`" do
+				ph = FilePath.new(ver1)
+				ph.should == ver2
+			end
+		end
+	end
+
+	describe "#eql?" do
+		it "is always true when an object is compared to itself" do
+			ph = 'foo/bar/baz'.as_path
+
+			ph.should eql(ph)
+		end
+
+		it "matches two different object representing the same path" do
+			p1 = '/foo/bar'.as_path
+			p2 = '/foo/bar'.as_path
+
+			p1.should eql(p2)
+		end
+
+		it "does not match different objects representing different paths" do
+			p1 = '/foo/bar'.as_path
+			p2 = '/foo/bar/baz'.as_path
+
+			p1.should_not eql(p2)
+		end
+
+		it "does not match objects that are not FilePaths" do
+			p1 = '/foo/bar/baz'.as_path
+			p2 = '/foo/bar/baz'
+
+			p1.should eq(p2)
+			p1.should_not eql(p2)
+		end
+	end
+
+	describe "#<=>" do
+		test_data = [
+			['a/', 'b'],
+			['/a', 'a'],
+			['../b', 'a'],
+		]
+		test_data.each do |path1, path2|
+			it "says that `#{path1}` precedes `#{path2}`" do
+				p1 = path1.as_path
+				p2 = path2.as_path
+
+				order = p1 <=> p2
+				order.should == -1
+			end
+		end
+	end
+
+	describe "#hash" do
+		it "has the same value for similar paths" do
+			p1 = '/foo/bar'.as_path
+			p2 = '/foo/bar'.as_path
+
+			p1.hash.should == p2.hash
+		end
+
+		it "has different values for different paths" do
+			p1 = '/foo/bar'.as_path
+			p2 = 'foo/quuz'.as_path
+
+			p1.hash.should_not == p2.hash
+		end
+
+		it "has different values for different paths with same normalized path" do
+			p1 = '/foo/bar/..'.as_path
+			p2 = '/foo'.as_path
+
+			p1.should eq(p2)
+			p1.hash.should_not eq(p2.hash)
+		end
+	end
+
+	describe FilePath::MetadataInfo do
+		describe "#stat" do
+			it "returns a stat for the file" do
+				(@root / 'd1').stat.should be_directory
+				(@root / 'f1').stat.size.should be_zero
+			end
+
+			it "follows links" do
+				(@root / 'd1' / 'l11').stat.should == '/dev/null'.as_path.stat
+			end
+
+			it "raises Errno::ENOENT for non-existing files" do
+				expect { (@root / 'foobar').stat }.to raise_error(Errno::ENOENT)
+			end
+		end
+
+		describe "#lstat" do
+			it "does not follow links" do
+				link_lstat = (@root / 'd1' / 'l11').lstat
+
+				link_lstat.should_not eq('/dev/null'.as_path.stat)
+				link_lstat.should be_symlink
+			end
+		end
+	end
+
+	describe FilePath::MetadataChanges do
+		describe "#chtime" do
+			it "change mtime" do
+				ph = @root / 'f1'
+				orig_mtime = ph.mtime
+
+				ph.chtime(Time.now, 0)
+				ph.mtime.to_i.should eq(0)
+
+				ph.chtime(Time.now, orig_mtime)
+				ph.mtime.should eq(orig_mtime)
+			end
+		end
+
+		describe "#chmod" do
+			it "changes file permissions" do
+				ph = @root / 'f1'
+				orig_mode = ph.stat.mode
+
+				ph.should be_readable
+
+				ph.chmod(000)
+				ph.should_not be_readable
+
+				ph.chmod(orig_mode)
+				ph.should be_readable
+			end
+		end
+	end
+
+	describe FilePath::MetadataTests do
+		describe "#file?" do
+			it "says that `f1` is a file" do
+				(@root / 'f1').should be_file
+			end
+
+			it "says that `d1/l11` is not a file" do
+				(@root / 'd1' / 'l11').should_not be_file
+			end
+
+			it "says that the fixture root directory is not a file" do
+				@root.should_not be_file
+			end
+		end
+
+		describe "#link?" do
+			it "says that `f1` is not a link" do
+				(@root / 'f1').should_not be_link
+			end
+
+			it "says that `d1/l11` is a link" do
+				(@root / 'd1' / 'l11').should be_link
+			end
+
+			it "says that the fixture root directory is not a link" do
+				@root.should_not be_link
+			end
+		end
+
+		describe "#directory?" do
+			it "says that `f1` is not a directory" do
+				(@root / 'f1').should_not be_directory
+			end
+
+			it "says that `d1/l11` is not a directory" do
+				(@root / 'd1' / 'l11').should_not be_directory
+			end
+
+			it "says that the fixture root directory is a directory" do
+				@root.should be_directory
+			end
+		end
+
+		describe "#pipe?" do
+			it "says that `p1` is a pipe" do
+				(@root / 'p1').should be_pipe
+			end
+
+			it "says that `f1` is not a pipe" do
+				(@root / 'f1').should_not be_pipe
+			end
+
+			it "says that the fixture root directory is not a pipe" do
+				@root.should_not be_pipe
+			end
+		end
+
+		describe "#socket?" do
+			it "says that `s1` is a socket" do
+				(@root / 's1').should be_socket
+			end
+
+			it "says that `f1` is not a socket" do
+				(@root / 'f1').should_not be_socket
+			end
+
+			it "says that the fixture root directory is not a socket" do
+				@root.should_not be_socket
+			end
+		end
+
+		describe "#hidden?" do
+			hidden_paths = [
+				'.foorc',
+				'foo/.bar',
+				'.foo.bar',
+			]
+			hidden_paths.each do |path|
+				it "says that <#{path}> is an hidden file" do
+					path.as_path.should be_hidden
+				end
+			end
+
+			non_hidden_paths = [
+				'foo.bar',
+				'foo/.bar/baz',
+			]
+			non_hidden_paths.each do |path|
+				it "says that <#{path}> not an hidden file" do
+					path.as_path.should_not be_hidden
+				end
+			end
+		end
+	end
+
+	describe FilePath::FilesystemInfo do
+		describe "#absolute_path" do
+			test_data = [
+				['d1/l11', File.expand_path('d1/l11', FIXTURES_DIR), FIXTURES_DIR],
+				['/foo/bar', '/foo/bar', '.'],
+			]
+			test_data.each do |path, abs_path, cwd|
+				it "resolves <#{path}> to <#{abs_path}> (in #{cwd})" do
+					Dir.chdir(cwd) do # FIXME
+						FilePath.new(path).absolute_path.should == abs_path
+					end
+				end
+			end
+		end
+
+		describe "#real_path" do
+			it "resolves <d1/l11> to </dev/null>" do
+				(@root / 'd1' / 'l11').real_path.should == '/dev/null'
+			end
+		end
+	end
+
+	describe FilePath::FilesystemChanges do
+		let(:ph) { @root / 'd1' / 'test-file' }
+
+		before(:each) do
+			ph.should_not exist
+		end
+
+		after(:each) do
+			File.delete(ph) if File.exists?(ph)
+		end
+
+		describe "#touch" do
+			it "creates an empty file" do
+				ph.touch
+				ph.should exist
+			end
+
+			it "updates the modification date of an existing file", :broken => true do
+				File.open(ph, "w+") { |file| file << "abc" }
+				File.utime(0, Time.now - 3200, ph)
+
+				before_stat = File.stat(ph)
+				before_time = Time.now
+
+				#sleep(5) # let Ruby flush its stat buffer to the disk
+				ph.touch
+
+				after_time = Time.now
+				after_stat = File.stat(ph)
+
+				before_stat.should_not eq(after_stat)
+
+				after_stat.size.should eq(before_stat.size)
+				after_stat.mtime.should be_between(before_time, after_time)
+			end
+		end
+	end
+
+	describe FilePath::FilesystemTests do
+		describe "mountpoint?" do
+			it "says that </proc> is a mount point" do
+				"/proc".as_path.should be_mountpoint
+			end
+
+			it "says that this RSpec file is not a mount point" do
+				__FILE__.as_path.should_not be_mountpoint
+			end
+
+			it "says that an non-existing file is not a mount point" do
+				"/foo/bar".as_path.should_not be_mountpoint
+			end
+
+			it "says that </> is a mount point" do
+				"/".as_path.should be_mountpoint
+			end
+		end
+	end
+
+	describe FilePath::ContentInfo do
+		let(:ph) { @root / 'd1' / 'test-file' }
+
+		before(:each) do
+			ph.should_not exist
+		end
+
+		after(:each) do
+			File.delete(ph) if File.exists?(ph)
+		end
+
+		describe "#read" do
+			let(:content) { "a"*20 + "b"*10 + "c"*5 }
+
+			before(:each) do
+				ph.open('w') { |f| f << content }
+			end
+
+			it "reads the complete content of a file" do
+				c = ph.read
+				c.should == content
+			end
+
+			it "reads the content in chunks of arbitrary sizes" do
+				sum = ""
+				len = 8
+
+				num_chunks = (content.length.to_f / len).ceil
+				num_chunks.times do |i|
+					c = ph.read(len, len*i)
+					sum += c
+					c.should == content[len*i, len]
+				end
+
+				sum.should == content
+			end
+		end
+
+		describe "#readlines" do
+			let(:line) { "abcd12" }
+			let(:lines) { Array.new(3) { line } }
+
+			it "reads all the lines in the file" do
+				ph.open('w') { |file| file << lines.join("\n") }
+				readlines = ph.readlines
+
+				readlines.should have(3).lines
+				readlines.all? { |l| l.chomp.should == line }
+			end
+
+			it "read lines separated by arbitrary separators" do
+				sep = ','
+
+				ph.open('w') { |file| file << lines.join(sep) }
+				readlines = ph.readlines(sep)
+
+				readlines.should have(3).lines
+				readlines[0..-2].all? { |l| l.should == line + sep}
+				readlines.last.should == line
+			end
+		end
+
+		describe "#size" do
+			before(:each) do
+				ph.touch
+			end
+
+			it "says that an empty file contains 0 bytes" do
+				ph.size.should be_zero
+			end
+
+			it "reports the size of a non-empty file" do
+				ph.size.should be_zero
+
+				ph.open("a") { |f| f << "abc" }
+				ph.size.should eq(3)
+
+				ph.open("a") { |f| f << "defg" }
+				ph.size.should eq(3+4)
+			end
+		end
+	end
+
+	describe FilePath::ContentChanges do
+		let(:ph) { @root / 'd1' / 'test-file' }
+		let(:content) { "a"*20 + "b"*10 + "c"*5 }
+
+		before(:each) do
+			ph.should_not exist
+		end
+
+		after(:each) do
+			File.delete(ph) if File.exists?(ph)
+		end
+
+		describe "#open" do
+			before(:each) do
+				ph.touch
+			end
+
+			it "opens files" do
+				file = ph.open
+				file.should be_a(File)
+			end
+
+			it "opens files in read-only mode" do
+				ph.open do |file|
+					expect { file << "abc" }.to raise_error(IOError)
+				end
+			end
+
+			it "opens files in read-write mode" do
+				ph.open('w') do |file|
+					file << "abc"
+				end
+
+				ph.size.should == 3
+			end
+		end
+
+		describe "#write" do
+			it "writes data passed as argument" do
+				ph.write(content)
+
+				ph.read.should == content
+			end
+
+			it "overwrites an existing file" do
+				ph.write(content * 2)
+				ph.size.should eq(content.length * 2)
+
+				ph.write(content)
+				ph.size.should eq(content.length)
+
+				ph.read.should == content
+			end
+		end
+
+		describe "#append" do
+			it "appends data to an existing file" do
+				ph.write(content)
+				ph.append(content)
+
+				ph.size.should eq(content.length * 2)
+				ph.read.should == content * 2
+			end
+		end
+
+		describe "#truncate" do
+			before(:each) do
+				ph.open('w') { |f| f << content }
+			end
+
+			it "truncates a file to 0 bytes" do
+				ph.size.should_not be_zero
+				ph.truncate
+				ph.size.should be_zero
+			end
+
+			it "truncates a file to an arbitrary size" do
+				ph.size.should_not be_zero
+				ph.truncate(2)
+				ph.size.should == 2
+			end
+		end
+	end
+
+	describe FilePath::ContentTests do
+		let(:ph) { @root / 'd1' / 'test-file' }
+
+		before(:each) do
+			ph.should_not exist
+		end
+
+		after(:each) do
+			File.delete(ph) if File.exists?(ph)
+		end
+
+		describe "#empty?" do
+			before(:each) do
+				ph.touch
+			end
+
+			it "says that an empty file is empty" do
+				ph.should be_empty
+			end
+
+			it "says that a non-empyt file is not empty" do
+				ph.open('w') { |f| f << "abc" }
+				ph.should_not be_empty
+			end
+
+			it "says that </dev/null> is empty" do
+				'/dev/null'.as_path.should be_empty
+			end
+		end
+	end
+
+	describe FilePath::SearchMethods do
+		describe "#entries" do
+			it "raises when path is not a directory" do
+				expect { (@root / 'f1').entries(:files) }.to raise_error(Errno::ENOTDIR)
+			end
+		end
+
+		describe "#find" do
+			it "finds all paths matching a glob string" do
+				list = @root.find('*1')
+
+				list.should have(8).items
+				list.each { |path| path.should =~ /1/ }
+			end
+
+			it "finds all paths matching a Regex" do
+				list = @root.find(/2/)
+
+				list.should have(6).items
+				list.each { |path| path.should =~ /2/ }
+			end
+
+			it "finds all paths for which the block returns true" do
+				list = @root.find { |path| path.directory? }
+
+				list.should have(9).items
+				list.each { |path| path.filename.should =~ /^d/ }
+			end
+		end
+
+		describe "#files" do
+			it "finds 1 file in the root directory" do
+				@root.files.should have(1).item
+			end
+
+			it "finds 3 files in the root directory and its sub directories" do
+				@root.files(true).should have(3).item
+			end
+
+			it "finds 2 files in directory <d1>" do
+				(@root / 'd1').files.should have(2).items
+			end
+
+			it "finds no files in directory <d1/d12>" do
+				(@root / 'd1' / 'd12').files.should have(0).items
+			end
+		end
+
+		describe "#directories" do
+			it "finds 4 directories in the root directory" do
+				@root.directories.should have(4).items
+			end
+
+			it "finds 9 directories in the root directory and its sub directories" do
+				@root.directories(true).should have(9).item
+			end
+
+			it "finds 2 directories in directory <d2>" do
+				(@root / 'd2').directories.should have(2).items
+			end
+
+			it "finds no directories in directory <d1/d13>" do
+				(@root / 'd1' / 'd13').directories.should have(0).items
+			end
+		end
+
+		describe "#links" do
+			it "finds no links in the root directory" do
+				@root.links.should have(0).items
+			end
+
+			it "finds 1 link in directory <d1>" do
+				(@root / 'd1').links.should have(1).item
+			end
+		end
+	end
+
+	describe FilePath::EnvironmentInfo
+end
+
+describe String do
+	describe "#as_path" do
+		it "generates a FilePath from a String" do
+			path = "/a/b/c".as_path
+			path.should be_a(FilePath)
+			path.should eq("/a/b/c")
+		end
+	end
+end
+
+describe Array do
+	describe "#as_path" do
+		it "generates a FilePath from a String" do
+			path = ['/', 'a', 'b', 'c'].as_path
+			path.should be_a(FilePath)
+			path.should eq("/a/b/c")
+		end
+	end
+end
diff --git a/spec/filepathlist_spec.rb b/spec/filepathlist_spec.rb
new file mode 100644
index 0000000..4ded8f0
--- /dev/null
+++ b/spec/filepathlist_spec.rb
@@ -0,0 +1,280 @@
+# This is free software released into the public domain (CC0 license).
+
+require File.join(File.dirname(__FILE__), 'spec_helper')
+
+describe FilePathList do
+	describe "#initialize" do
+		it "creates an empty FilePathList" do
+			list = FilePathList.new()
+
+			list.should be_empty
+		end
+
+		it "creates a FilePathList from an Array of Strings" do
+			paths = %w{a/b c/d e/f}
+			list = FilePathList.new(paths)
+
+			list.should have(3).items
+			list.each { |path| path.should be_a(FilePath) }
+		end
+
+		it "creates a FilePathList from an Array of FilePaths" do
+			paths = %w{a/b c/d e/f}.map(&:as_path)
+			list = FilePathList.new(paths)
+
+			list.should have(3).items
+			list.each { |path| path.should be_a(FilePath) }
+		end
+
+		it "creates a FilePathList from an Array of Arrays" do
+			paths = [%w{a b}, %w{c d}, %w{e f}]
+			list = FilePathList.new(paths)
+
+			list.should have(3).items
+			list.each { |path| path.should be_a(FilePath) }
+		end
+	end
+
+	describe "#/" do
+		it "adds the same string to all the paths" do
+			list = FilePathList.new(%w{foo faa}) / 'bar'
+			list[0].should eq 'foo/bar'
+			list[1].should eq 'faa/bar'
+		end
+	end
+
+	describe "#+" do
+		it "concatenates two FilePathLists" do
+			list1 = FilePathList.new(%w{a b c})
+			list2 = FilePathList.new(%w{d e})
+
+			list = list1 + list2
+			list.should have(5).items
+			list[0].should eq('a')
+			list[1].should eq('b')
+			list[2].should eq('c')
+			list[3].should eq('d')
+			list[4].should eq('e')
+		end
+	end
+
+	describe "#-" do
+		it "removes a list (as array of strings) from another list" do
+			list1 = FilePathList.new(%w{a/b /a/c e/d})
+			list2 = list1 - %w{a/b e/d}
+
+			list2.should have(1).item
+			list2[0].should eq('/a/c')
+		end
+	end
+
+	describe "#<<" do
+		it "adds a new to path to a existing FilePathList" do
+			list1 = FilePathList.new(%w{a/b /c/d})
+			list2 = list1 << "e/f"
+
+			list1.should have(2).items
+			list2.should have(3).items
+
+			list2[0].should eq('a/b')
+			list2[1].should eq('/c/d')
+			list2[2].should eq('e/f')
+		end
+	end
+
+	describe "#*" do
+		describe "calculates the cartesian product between" do
+			it "two FilePathLists" do
+				p1 = %w{a b c}
+				p2 = %w{1 2}
+				list1 = FilePathList.new(p1)
+				list2 = FilePathList.new(p2)
+
+				all_paths = p1.product(p2).map { |x| x.join('/') }
+
+				list = list1 * list2
+				list.should have(6).items
+				list.should include(*all_paths)
+			end
+
+			it "a FilePathList and a string" do
+				p1 = %w{a b c}
+				p2 = "abc"
+
+				list = FilePathList.new(p1) * p2
+				list.should have(3).items
+				list.should include(*%w{a/abc b/abc c/abc})
+			end
+
+			it "a FilePathList and a FilePath" do
+				p1 = %w{a b c}
+				p2 = FilePath.new("x")
+
+				list = FilePathList.new(p1) * p2
+				list.should have(3).items
+				list.should include(*%w{a/x b/x c/x})
+			end
+
+			it "a FilePath and an array of strings" do
+				p1 = %w{a b c}
+				p2 = ["1", "2"]
+
+				list = FilePathList.new(p1) * p2
+				list.should have(6).items
+				list.should include(*%w{a/1 b/1 a/2 b/2 c/1 c/2})
+			end
+		end
+	end
+
+	describe "#remove_common_segments" do
+		it "works on lists of files from the same dir" do
+			paths = %w{a/b/x1 a/b/x2 a/b/x3}
+			list = FilePathList.new(paths).remove_common_segments
+
+			list.should have(3).items
+			list.should include(*%w{x1 x2 x3})
+		end
+
+		it "works on lists of files from different dirs" do
+			list1 = FilePathList.new(%w{a/b/x1 a/b/c/x2 a/b/d/e/x3})
+			list2 = list1.remove_common_segments
+
+			list2.should have(3).items
+			list2.should include(*%w{x1 c/x2 d/e/x3})
+		end
+
+		it "works on lists of files with no common segments" do
+			paths = %w{a/b a/d g/f}
+			list1 = FilePathList.new(paths)
+			list2 = list1.remove_common_segments
+
+			list1.should == list2
+		end
+
+		it "works on lists that contain duplicates only" do
+			paths = %w{a/b a/b a/b}
+			list1 = FilePathList.new(paths)
+			list2 = list1.remove_common_segments
+
+			list2.should == FilePathList.new(['.', '.', '.'])
+		end
+	end
+
+	describe "#include?" do
+		it "says that 'a/c' is included in [<a/b>, <a/c>, </a/d>]" do
+			list = FilePathList.new(%w{a/b a/c /a/d})
+			list.should include("a/c")
+		end
+	end
+
+	describe "#to_s" do
+		it "returns files separated by a comma`" do
+			list = FilePathList.new(%w{a/b a/c /a/d})
+			list.to_s.should == "a/b:a/c:/a/d"
+		end
+	end
+
+	describe "#==" do
+		let(:list) { ['a/b', 'c/d', 'e/f'].as_path_list }
+
+		it "compares a FilePathList to another FilePathList" do
+			list2 = FilePathList.new << 'a/b' << 'c/d' << 'e/f'
+			list3 = list2 << 'g/h'
+
+			list.should eq(list2)
+			list.should_not eq(list3)
+		end
+
+		it "compares a FilePathList to an Array of Strings" do
+			list.should eq(%w{a/b c/d e/f})
+			list.should_not eq(%w{a/a b/b c/c})
+		end
+	end
+
+	describe FilePathList::ArrayMethods do
+		let(:list) { FilePathList.new(%w{a.foo b.bar c.foo d.foo b.bar}) }
+
+		describe "#all?" do
+			it "checks whether a block applies to a list" do
+				ok = list.all? { |path| path.extension? }
+				ok.should be_true
+			end
+		end
+
+		describe "#any?" do
+			it "check whether a block does not apply to any path" do
+				ok = list.any? { |path| path.basename == "a.foo" }
+				ok.should be_true
+			end
+		end
+
+		describe "#none?" do
+			it "check whether a block does not apply to any path" do
+				ok = list.none? { |path| path.absolute? }
+				ok.should be_true
+			end
+		end
+	end
+
+	describe FilePathList::EntriesMethods do
+		let(:list) { FilePathList.new(%w{a.foo b.bar c.foo d.foo b.bar}) }
+
+		describe "#select" do
+			it "keeps paths matching a Regex" do
+				remaining = list.select(/bar$/)
+
+				remaining.should be_a FilePathList
+				remaining.should have(2).items
+				remaining.each { |path| path.extension.should == 'bar' }
+			end
+
+			it "keeps all the paths for which the block returns true" do
+				remaining = list.select { |ph| ph.extension?('bar') }
+
+				remaining.should have(2).items
+				remaining.each { |ph| ph.extension.should == 'bar' }
+			end
+		end
+
+		describe "#exclude" do
+			it "excludes paths matching a Regex" do
+				remaining = list.exclude(/bar$/)
+
+				remaining.should be_a FilePathList
+				remaining.should have(3).items
+				remaining.each { |path| path.extension.should == 'foo' }
+			end
+
+			it "excludes all the paths for which the block returns true" do
+				remaining = list.exclude { |path| path.extension?('bar') }
+
+				remaining.should be_a FilePathList
+				remaining.should have(3).items
+				remaining.each { |path| path.extension.should == 'foo' }
+			end
+		end
+
+		describe "#map" do
+			it "applies a block to each path" do
+				mapped = list.map { |path| path.remove_extension }
+
+				mapped.should be_a FilePathList
+				mapped.should have(list.size).items
+				mapped.each { |path| path.extension?.should be_false }
+			end
+		end
+	end
+end
+
+
+describe Array do
+	describe "#as_path_list" do
+		it "generates a FilePathList from an Array" do
+			paths = %w{/a/b c/d /f/g}
+			list = paths.as_path_list
+
+			list.should be_a(FilePathList)
+			list.should include(*paths)
+		end
+	end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..059e2e4
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,12 @@
+# This is free software released into the public domain (CC0 license).
+
+LIB_DIR = File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib]))
+$LOAD_PATH.unshift(LIB_DIR) unless $LOAD_PATH.include?(LIB_DIR)
+
+require 'filepath'
+
+RSpec.configure do |config|
+	config.filter_run_excluding :broken => true
+end
+
+FIXTURES_DIR = File.join(%w{spec fixtures})
diff --git a/spec/tasks.rb b/spec/tasks.rb
new file mode 100644
index 0000000..87a4377
--- /dev/null
+++ b/spec/tasks.rb
@@ -0,0 +1,53 @@
+# This is free software released into the public domain (CC0 license).
+
+require 'rake/clean'
+
+FIXTURES_DIR = File.join(%w{spec fixtures})
+FIXTURES_FAKE_ENTRIES = [
+	'd1',
+		['d1', 'd11'],
+		['d1', 'd12'],
+		['d1', 'd13'],
+		['d1', 'f11'],
+		['d1', 'f12'],
+		['d1', 'l11'],
+	'd2',
+		['d2', 'd21'],
+		['d2', 'd22'],
+	'd3',
+	'f1',
+	'dx',
+	'p1',
+	'p2',
+	's1',
+].map { |entry| File.join(FIXTURES_DIR, *Array(entry)) }
+
+CLEAN.concat FIXTURES_FAKE_ENTRIES
+
+namespace :spec do
+	namespace :fixtures do
+		rule %r{/d[0-9x]+$} do |t|
+			mkdir_p t.name
+		end
+
+		rule %r{/f[0-9]+$} do |t|
+			touch t.name
+		end
+
+		rule %r{/l[0-9]+$} do |t|
+			ln_s '/dev/null', t.name
+		end
+
+		rule %r{/p[0-9]+$} do |t|
+			system "mkfifo #{t.name}"
+		end
+
+		rule %r{/s[0-9]+$} do |t|
+			require 'socket'
+			UNIXServer.new(t.name)
+		end
+
+		desc "Generate fake dirs and files"
+		task :gen => FIXTURES_FAKE_ENTRIES
+	end
+end

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-filepath.git



More information about the Pkg-ruby-extras-commits mailing list