summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNobuyoshi Nakada <nobu@ruby-lang.org>2022-11-25 10:03:51 +0900
committergit <svn-admin@ruby-lang.org>2022-11-25 01:03:57 +0000
commitc6330cd32b7d02b4603080a2c53d64be9e05773c (patch)
treeb75deec779274891f9ae99015a1dfc8bf71b0214
parent1a47521c4459fd61384c51ee58bc422ec69310d6 (diff)
[ruby/fileutils] [Feature #18925] Add `ln_sr` method and `relative:` option to `ln_s`
https://github.com/ruby/fileutils/commit/5116088d5c
-rw-r--r--lib/fileutils.rb105
-rw-r--r--test/fileutils/test_fileutils.rb37
2 files changed, 138 insertions, 4 deletions
diff --git a/lib/fileutils.rb b/lib/fileutils.rb
index a33f086a3e..d45aa4e2de 100644
--- a/lib/fileutils.rb
+++ b/lib/fileutils.rb
@@ -36,6 +36,7 @@ end
# - ::ln, ::link: Creates hard links.
# - ::ln_s, ::symlink: Creates symbolic links.
# - ::ln_sf: Creates symbolic links, overwriting if necessary.
+# - ::ln_sr: Creates symbolic links relative to targets
#
# === Deleting
#
@@ -690,6 +691,7 @@ module FileUtils
# Keyword arguments:
#
# - <tt>force: true</tt> - overwrites +dest+ if it exists.
+ # - <tt>relative: false</tt> - create links relative to +dest+.
# - <tt>noop: true</tt> - does not create links.
# - <tt>verbose: true</tt> - prints an equivalent command:
#
@@ -709,7 +711,10 @@ module FileUtils
#
# Related: FileUtils.ln_sf.
#
- def ln_s(src, dest, force: nil, noop: nil, verbose: nil)
+ def ln_s(src, dest, force: nil, relative: false, target_directory: true, noop: nil, verbose: nil)
+ if relative
+ return ln_sr(src, dest, force: force, noop: noop, verbose: verbose)
+ end
fu_output_message "ln -s#{force ? 'f' : ''} #{[src,dest].flatten.join ' '}" if verbose
return if noop
fu_each_src_dest0(src, dest) do |s,d|
@@ -729,6 +734,48 @@ module FileUtils
end
module_function :ln_sf
+ # Like FileUtils.ln_s, but create links relative to +dest+.
+ #
+ def ln_sr(src, dest, target_directory: true, force: nil, noop: nil, verbose: nil)
+ options = "#{force ? 'f' : ''}#{target_directory ? '' : 'T'}"
+ dest = File.path(dest)
+ srcs = Array(src)
+ link = proc do |s, target_dir_p = true|
+ s = File.path(s)
+ if target_dir_p
+ d = File.join(destdirs = dest, File.basename(s))
+ else
+ destdirs = File.dirname(d = dest)
+ end
+ destdirs = fu_split_path(File.realpath(destdirs))
+ if fu_starting_path?(s)
+ srcdirs = fu_split_path((File.realdirpath(s) rescue File.expand_path(s)))
+ base = fu_relative_components_from(srcdirs, destdirs)
+ s = File.join(*base)
+ else
+ srcdirs = fu_clean_components(*fu_split_path(s))
+ base = fu_relative_components_from(fu_split_path(Dir.pwd), destdirs)
+ while srcdirs.first&. == ".." and base.last&.!=("..") and !fu_starting_path?(base.last)
+ srcdirs.shift
+ base.pop
+ end
+ s = File.join(*base, *srcdirs)
+ end
+ fu_output_message "ln -s#{options} #{s} #{d}" if verbose
+ next if noop
+ remove_file d, true if force
+ File.symlink s, d
+ end
+ case srcs.size
+ when 0
+ when 1
+ link[srcs[0], target_directory && File.directory?(dest)]
+ else
+ srcs.each(&link)
+ end
+ end
+ module_function :ln_sr
+
# Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link]; returns +nil+.
#
# Arguments +src+ and +dest+
@@ -2436,15 +2483,15 @@ module FileUtils
end
private_module_function :fu_each_src_dest
- def fu_each_src_dest0(src, dest) #:nodoc:
+ def fu_each_src_dest0(src, dest, target_directory = true) #:nodoc:
if tmp = Array.try_convert(src)
tmp.each do |s|
s = File.path(s)
- yield s, File.join(dest, File.basename(s))
+ yield s, (target_directory ? File.join(dest, File.basename(s)) : dest)
end
else
src = File.path(src)
- if File.directory?(dest)
+ if target_directory and File.directory?(dest)
yield src, File.join(dest, File.basename(src))
else
yield src, File.path(dest)
@@ -2468,6 +2515,56 @@ module FileUtils
end
private_module_function :fu_output_message
+ def fu_split_path(path)
+ path = File.path(path)
+ list = []
+ until (parent, base = File.split(path); parent == path or parent == ".")
+ list << base
+ path = parent
+ end
+ list << path
+ list.reverse!
+ end
+ private_module_function :fu_split_path
+
+ def fu_relative_components_from(target, base) #:nodoc:
+ i = 0
+ while target[i]&.== base[i]
+ i += 1
+ end
+ Array.new(base.size-i, '..').concat(target[i..-1])
+ end
+ private_module_function :fu_relative_components_from
+
+ def fu_clean_components(*comp)
+ comp.shift while comp.first == "."
+ return comp if comp.empty?
+ clean = [comp.shift]
+ path = File.join(*clean, "") # ending with File::SEPARATOR
+ while c = comp.shift
+ if c == ".." and clean.last != ".." and !(fu_have_symlink? && File.symlink?(path))
+ clean.pop
+ path.chomp!(%r((?<=\A|/)[^/]+/\z), "")
+ else
+ clean << c
+ path << c << "/"
+ end
+ end
+ clean
+ end
+ private_module_function :fu_clean_components
+
+ if fu_windows?
+ def fu_starting_path?(path)
+ path&.start_with?(%r(\w:|/))
+ end
+ else
+ def fu_starting_path?(path)
+ path&.start_with?("/")
+ end
+ end
+ private_module_function :fu_starting_path?
+
# This hash table holds command options.
OPT_TABLE = {} #:nodoc: internal use only
(private_instance_methods & methods(false)).inject(OPT_TABLE) {|tbl, name|
diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb
index 2748bd247f..3994da7433 100644
--- a/test/fileutils/test_fileutils.rb
+++ b/test/fileutils/test_fileutils.rb
@@ -1002,6 +1002,43 @@ class TestFileUtils < Test::Unit::TestCase
}
end if have_symlink?
+ def test_ln_sr
+ check_singleton :ln_sr
+
+ TARGETS.each do |fname|
+ begin
+ lnfname = 'tmp/lnsdest'
+ ln_sr fname, lnfname
+ assert FileTest.symlink?(lnfname), 'not symlink'
+ assert_equal "../#{fname}", File.readlink(lnfname), fname
+ ensure
+ rm_f lnfname
+ end
+ end
+ mkdir 'data/src'
+ File.write('data/src/xxx', 'ok')
+ File.symlink '../data/src', 'tmp/src'
+ ln_sr 'tmp/src/xxx', 'data'
+ assert File.symlink?('data/xxx')
+ assert_equal 'ok', File.read('data/xxx')
+ end if have_symlink?
+
+ def test_ln_sr_broken_symlink
+ assert_nothing_raised {
+ ln_sr 'tmp/symlink', 'tmp/symlink'
+ }
+ end if have_symlink? and !no_broken_symlink?
+
+ def test_ln_sr_pathname
+ # pathname
+ touch 'tmp/lns_dest'
+ assert_nothing_raised {
+ ln_sr Pathname.new('tmp/lns_dest'), 'tmp/symlink_tmp1'
+ ln_sr 'tmp/lns_dest', Pathname.new('tmp/symlink_tmp2')
+ ln_sr Pathname.new('tmp/lns_dest'), Pathname.new('tmp/symlink_tmp3')
+ }
+ end if have_symlink?
+
def test_mkdir
check_singleton :mkdir