--pgp-sign-Multipart_Thu_Sep_25_18:42:13_2008-1
Content-Type: text/plain; charset=ISO-2022-JP

At Wed, 24 Sep 2008 02:02:59 +0900,
Tanaka Akira wrote:
> In article <86od2gcvvj.knu / iDaemons.org>,
>   "Akinori MUSHA" <knu / iDaemons.org> writes:
>
> >  似た動作の想起を条件にするのは、 realpath を含む名前にするか、
> > あるいは realpath に引数を追加するかの二択と考えてよいでしょうか。
>
> real という単語が入ってれば似ているように思います。

 しかし、 real という単語が入るのに実際に存在しないパスを返す
のは奇妙ではないでしょうか。あくまで realpath という既知の名前
からしか機能は想起されないと思います。real_path は気持ち悪いし、
*_realpath や realpath_* も適当なものが思いつきませんでした。
bsd_realpath, realpath_butlast, realpath_till_penult,
almost_realpath, not_quite_realpath, ...

 同じ機能は既存の公開および内部メソッドを使っても簡単には実装
できなかったのでなんとか入れたいと思うのですが。


 ~ の展開と相対パスの展開を行うメソッドが expand_path なので、
symlink の解決と相対パスの展開を行うメソッドが resolve_path と
いうのは悪くないと思っています。realpath との関連はさほど大事
でしょうか。

> >  であれば私は後者を推したいです。フラグ引数が廃止された過去が
> > あるので抵抗があったのですが、もう数年経っているのでトラブルは
> > ないでしょうね。
>
> フラグより違う名前のほうがいいんじゃないかという気がします。
>
> この件については動的にどちらかの挙動を選びたいということはな
> さそうな気がしますし、名前が挙動を表すとプログラムがわかりや
> すくなるので。

 それはそうですね。先の実装はフラグを受け付けるようになって
いましたが、フラグを取るメソッドは private に隠そうと思います。

--
Akinori MUSHA / http://akinori.org/

	* lib/pathname.rb (Pathname#realpath_rec, Pathname#resolve_path):
	  Add Pathname#resolve_path, a variant of #realpath that allows
	  the last component of pathname to be nonexistent.

Index: lib/pathname.rb
===================================================================
--- lib/pathname.rb	(revision 19545)
+++ lib/pathname.rb	(working copy)
@@ -76,9 +76,9 @@
 #
 # === Core methods
 #
-# These methods are effectively manipulating a String, because that's all a path
-# is.  Except for #mountpoint?, #children, and #realpath, they don't access the
-# filesystem.
+# These methods are effectively manipulating a String, because that's
+# all a path is.  Except for #mountpoint?, #children, #resolve_path
+# and #realpath, they don't access the filesystem.
 #
 # - +
 # - #join
@@ -90,6 +90,7 @@
 # - #each_filename
 # - #cleanpath
 # - #realpath
+# - #resolve_path
 # - #children
 # - #mountpoint?
 #
@@ -411,7 +412,7 @@ class Pathname
   end
   private :cleanpath_conservative

-  def realpath_rec(prefix, unresolved, h)
+  def realpath_rec(prefix, unresolved, h, strict, last = true)
     resolved = []
     until unresolved.empty?
       n = unresolved.shift
@@ -428,14 +429,20 @@ class Pathname
             prefix, *resolved = h[path]
           end
         else
-          s = File.lstat(path)
+          begin
+            s = File.lstat(path)
+          rescue Errno::ENOENT => e
+            raise e if strict || !last || !unresolved.empty?
+            resolved << n
+            break
+          end
           if s.symlink?
             h[path] = :resolving
             link_prefix, link_names = split_names(File.readlink(path))
             if link_prefix == ''
-              prefix, *resolved = h[path] = realpath_rec(prefix, resolved + link_names, h)
+              prefix, *resolved = h[path] = realpath_rec(prefix, resolved + link_names, h, strict, unresolved.empty?)
             else
-              prefix, *resolved = h[path] = realpath_rec(link_prefix, link_names, h)
+              prefix, *resolved = h[path] = realpath_rec(link_prefix, link_names, h, strict, unresolved.empty?)
             end
           else
             resolved << n
@@ -448,22 +455,39 @@ class Pathname
   end
   private :realpath_rec

-  #
-  # Returns a real (absolute) pathname of +self+ in the actual filesystem.
-  # The real pathname doesn't contain symlinks or useless dots.
-  #
-  # No arguments should be given; the old behaviour is *obsoleted*.
-  #
-  def realpath
+  def resolve_path_internal(strict = false)
     path = @path
     prefix, names = split_names(path)
     if prefix == ''
       prefix, names2 = split_names(Dir.pwd)
       names = names2 + names
     end
-    prefix, *names = realpath_rec(prefix, names, {})
+    prefix, *names = realpath_rec(prefix, names, {}, strict)
     self.class.new(prepend_prefix(prefix, File.join(*names)))
   end
+  private :resolve_path_internal
+  #
+  # Returns the real (absolute) pathname of +self+ in the actual
+  # filesystem not containing symlinks or useless dots.
+  #
+  # All components of the pathname must exist when this method is
+  # called.
+  #
+  # No arguments should be given; the old behaviour is *obsoleted*.
+  #
+  def realpath
+    resolve_path_internal(true)
+  end
+
+  #
+  # Returns the real (absolute) pathname of +self+ in the actual filesystem.
+  # The real pathname doesn't contain symlinks or useless dots.
+  #
+  # The last component of the pathname can be nonexistent.
+  #
+  def resolve_path
+    resolve_path_internal(false)
+  end

   # #parent returns the parent directory.
   #
Index: test/pathname/test_pathname.rb
===================================================================
--- test/pathname/test_pathname.rb	(revision 19545)
+++ test/pathname/test_pathname.rb	(working copy)
@@ -273,6 +273,8 @@ class TestPathname < Test::Unit::TestCas
     Pathname.new(path).realpath.to_s
   end

+  class NoException < Exception; end
+
   def test_realpath
     begin
       File.symlink(nil, nil)
@@ -281,10 +283,42 @@ class TestPathname < Test::Unit::TestCas
     rescue TypeError
     end
     Dir.mktmpdir('rubytest-pathname') {|dir|
+      assert_raises(Errno::ENOENT) { realpath("#{dir}/not-exist") }
       File.symlink("not-exist-target", "#{dir}/not-exist")
-      assert_raise(Errno::ENOENT) { realpath("#{dir}/not-exist") }
+      assert_raises(Errno::ENOENT) { realpath("#{dir}/not-exist") }
+      File.symlink("../#{File.basename(dir)}/./not-exist-target", "#{dir}/not-exist2")
+      assert_raises(Errno::ENOENT) { realpath("#{dir}/not-exist2") }
+      File.open("#{dir}/exist-target", "w") {}
+      File.symlink("../#{File.basename(dir)}/./exist-target", "#{dir}/exist")
+      assert_raises(NoException) { realpath("#{dir}/exist"); raise NoException }
       File.symlink("loop", "#{dir}/loop")
-      assert_raise(Errno::ELOOP) { realpath("#{dir}/loop") }
+      assert_raises(Errno::ELOOP) { realpath("#{dir}/loop") }
+    }
+  end
+
+  def resolve_path(path)
+    Pathname.new(path).resolve_path.to_s
+  end
+
+  def test_resolve_path
+    begin
+      File.symlink(nil, nil)
+    rescue NotImplementedError
+      return
+    rescue TypeError
+    end
+    Dir.mktmpdir('rubytest-pathname') {|dir|
+      assert_raises(NoException) { resolve_path("#{dir}/not-exist"); raise NoException }
+      assert_raises(Errno::ENOENT) { realpath("#{dir}/not-exist/not-exist-child") }
+      File.symlink("not-exist-target", "#{dir}/not-exist")
+      assert_raises(NoException) { resolve_path("#{dir}/not-exist"); raise NoException }
+      File.symlink("../#{File.basename(dir)}/./not-exist-target", "#{dir}/not-exist2")
+      assert_raises(NoException) { resolve_path("#{dir}/not-exist2"); raise NoException }
+      File.open("#{dir}/exist-target", "w") {}
+      File.symlink("../#{File.basename(dir)}/./exist-target", "#{dir}/exist")
+      assert_raises(NoException) { resolve_path("#{dir}/exist"); raise NoException }
+      File.symlink("loop", "#{dir}/loop")
+      assert_raises(Errno::ELOOP) { resolve_path("#{dir}/loop") }
     }
   end


--pgp-sign-Multipart_Thu_Sep_25_18:42:13_2008-1
Content-Type: application/pgp-signature
Content-Transfer-Encoding: 7bit

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (FreeBSD)

iEYEABECAAYFAkjbXPUACgkQkgvvx5/Z4e4SQACgjTRAr1J/cZwFw2vQGFE5CFuF
ObUAoK862PLRBypREPvi7dRwEoAiNqnW
=vadq
-----END PGP SIGNATURE-----

--pgp-sign-Multipart_Thu_Sep_25_18:42:13_2008-1--