From 2836a164ed2af5914637886ae8a1b307e8e322b9 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 19 Jul 2025 12:03:32 +0900 Subject: [PATCH 1/2] Fix up `FileUtils#ln_sr` --- lib/fileutils.rb | 42 +++++++++++++++----------------- test/fileutils/test_fileutils.rb | 8 +++++- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/fileutils.rb b/lib/fileutils.rb index d07c608..6af93b2 100644 --- a/lib/fileutils.rb +++ b/lib/fileutils.rb @@ -731,30 +731,22 @@ def ln_sf(src, dest, noop: nil, verbose: nil) # Like FileUtils.ln_s, but create links relative to +dest+. # def ln_sr(src, dest, target_directory: true, force: nil, noop: nil, verbose: nil) - fu_output_message "ln -sr#{force ? 'f' : ''}#{ - target_directory ? '' : 'T'} #{[src,dest].flatten.join ' '}" if verbose - return if noop + cmd = "ln -s#{force ? 'f' : ''}#{target_directory ? '' : 'T'}" if verbose unless target_directory - destdirs = fu_split_path(File.realdirpath(dest)) + destdirs = fu_split_path(dest) end fu_each_src_dest0(src, dest, target_directory) do |s,d| if target_directory - destdirs = fu_split_path(File.realdirpath(File.dirname(d))) + destdirs = fu_split_path(File.dirname(d)) # else d == dest end - 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 + srcdirs = fu_split_path(s) + i = fu_common_components(srcdirs, destdirs) + n = destdirs.size - i + n -= 1 unless target_directory + s = File.join(fu_clean_components(*Array.new(n, '..'), *srcdirs[i..-1])) + fu_output_message [cmd, s, d].flatten.join(' ') if verbose + next if noop remove_file d, true if force File.symlink s, d end @@ -2504,7 +2496,11 @@ def fu_split_path(path) #:nodoc: path = File.path(path) list = [] until (parent, base = File.split(path); parent == path or parent == ".") - list << base + if base != '..' and list.last == '..' and !(fu_have_symlink? && File.symlink?(path)) + list.pop + else + list << base + end path = parent end list << path @@ -2512,14 +2508,14 @@ def fu_split_path(path) #:nodoc: end private_module_function :fu_split_path - def fu_relative_components_from(target, base) #:nodoc: + def fu_common_components(target, base) #:nodoc: i = 0 while target[i]&.== base[i] i += 1 end - Array.new(base.size-i, '..').concat(target[i..-1]) + i end - private_module_function :fu_relative_components_from + private_module_function :fu_common_components def fu_clean_components(*comp) #:nodoc: comp.shift while comp.first == "." @@ -2529,7 +2525,7 @@ def fu_clean_components(*comp) #:nodoc: while c = comp.shift if c == ".." and clean.last != ".." and !(fu_have_symlink? && File.symlink?(path)) clean.pop - path.chomp!(%r((?<=\A|/)[^/]+/\z), "") + path.sub!(%r((?<=\A|/)[^/]+/\z), "") else clean << c path << c << "/" diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index c9d8d0c..e4a64bb 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -1048,11 +1048,17 @@ def test_ln_sr end end + File.symlink 'data', 'link' + mkdir 'link/d1' + mkdir 'link/d2' + ln_sr 'link/d1/z', 'link/d2' + assert_equal '../d1/z', File.readlink('data/d2/z') + 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_file.symlink?('data/xxx') assert_equal 'ok', File.read('data/xxx') end From 277f7f2ff804c469e7b4205f4690d1a449fa7ae0 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 19 Jul 2025 22:23:42 +0900 Subject: [PATCH 2/2] Use shorter symlink by real paths --- lib/fileutils.rb | 24 ++++++++++++++++++------ test/fileutils/test_fileutils.rb | 4 +++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/fileutils.rb b/lib/fileutils.rb index 6af93b2..a0d666b 100644 --- a/lib/fileutils.rb +++ b/lib/fileutils.rb @@ -732,19 +732,31 @@ def ln_sf(src, dest, noop: nil, verbose: nil) # def ln_sr(src, dest, target_directory: true, force: nil, noop: nil, verbose: nil) cmd = "ln -s#{force ? 'f' : ''}#{target_directory ? '' : 'T'}" if verbose - unless target_directory - destdirs = fu_split_path(dest) - end fu_each_src_dest0(src, dest, target_directory) do |s,d| if target_directory - destdirs = fu_split_path(File.dirname(d)) - # else d == dest + parent = File.dirname(d) + destdirs = fu_split_path(parent) + real_ddirs = fu_split_path(File.realpath(parent)) + else + destdirs ||= fu_split_path(dest) + real_ddirs ||= fu_split_path(File.realpath(dest)) end srcdirs = fu_split_path(s) i = fu_common_components(srcdirs, destdirs) n = destdirs.size - i n -= 1 unless target_directory - s = File.join(fu_clean_components(*Array.new(n, '..'), *srcdirs[i..-1])) + link1 = fu_clean_components(*Array.new([n, 0].max, '..'), *srcdirs[i..-1]) + begin + real_sdirs = fu_split_path(File.realdirpath(s)) rescue nil + rescue + else + i = fu_common_components(real_sdirs, real_ddirs) + n = real_ddirs.size - i + n -= 1 unless target_directory + link2 = fu_clean_components(*Array.new([n, 0].max, '..'), *real_sdirs[i..-1]) + link1 = link2 if link1.size > link2.size + end + s = File.join(link1) fu_output_message [cmd, s, d].flatten.join(' ') if verbose next if noop remove_file d, true if force diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index e4a64bb..500c47b 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -1031,7 +1031,8 @@ def test_ln_sr assert_all_assertions_foreach(nil, *TARGETS) do |fname| lnfname = 'tmp/lnsdest' ln_sr fname, lnfname - assert FileTest.symlink?(lnfname), 'not symlink' + assert_file.symlink?(lnfname) + assert_file.identical?(lnfname, fname) assert_equal "../#{fname}", File.readlink(lnfname) ensure rm_f lnfname @@ -1060,6 +1061,7 @@ def test_ln_sr ln_sr 'tmp/src/xxx', 'data' assert_file.symlink?('data/xxx') assert_equal 'ok', File.read('data/xxx') + assert_equal 'src/xxx', File.readlink('data/xxx') end def test_ln_sr_not_target_directory