Skip to content

Commit 5116088

Browse files
authored
[Feature #18925] Add ln_sr method and relative: option to ln_s
1 parent 42983c2 commit 5116088

File tree

2 files changed

+138
-4
lines changed

2 files changed

+138
-4
lines changed

lib/fileutils.rb

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
# - ::ln, ::link: Creates hard links.
3737
# - ::ln_s, ::symlink: Creates symbolic links.
3838
# - ::ln_sf: Creates symbolic links, overwriting if necessary.
39+
# - ::ln_sr: Creates symbolic links relative to targets
3940
#
4041
# === Deleting
4142
#
@@ -690,6 +691,7 @@ def cp_lr(src, dest, noop: nil, verbose: nil,
690691
# Keyword arguments:
691692
#
692693
# - <tt>force: true</tt> - overwrites +dest+ if it exists.
694+
# - <tt>relative: false</tt> - create links relative to +dest+.
693695
# - <tt>noop: true</tt> - does not create links.
694696
# - <tt>verbose: true</tt> - prints an equivalent command:
695697
#
@@ -709,7 +711,10 @@ def cp_lr(src, dest, noop: nil, verbose: nil,
709711
#
710712
# Related: FileUtils.ln_sf.
711713
#
712-
def ln_s(src, dest, force: nil, noop: nil, verbose: nil)
714+
def ln_s(src, dest, force: nil, relative: false, target_directory: true, noop: nil, verbose: nil)
715+
if relative
716+
return ln_sr(src, dest, force: force, noop: noop, verbose: verbose)
717+
end
713718
fu_output_message "ln -s#{force ? 'f' : ''} #{[src,dest].flatten.join ' '}" if verbose
714719
return if noop
715720
fu_each_src_dest0(src, dest) do |s,d|
@@ -729,6 +734,48 @@ def ln_sf(src, dest, noop: nil, verbose: nil)
729734
end
730735
module_function :ln_sf
731736

737+
# Like FileUtils.ln_s, but create links relative to +dest+.
738+
#
739+
def ln_sr(src, dest, target_directory: true, force: nil, noop: nil, verbose: nil)
740+
options = "#{force ? 'f' : ''}#{target_directory ? '' : 'T'}"
741+
dest = File.path(dest)
742+
srcs = Array(src)
743+
link = proc do |s, target_dir_p = true|
744+
s = File.path(s)
745+
if target_dir_p
746+
d = File.join(destdirs = dest, File.basename(s))
747+
else
748+
destdirs = File.dirname(d = dest)
749+
end
750+
destdirs = fu_split_path(File.realpath(destdirs))
751+
if fu_starting_path?(s)
752+
srcdirs = fu_split_path((File.realdirpath(s) rescue File.expand_path(s)))
753+
base = fu_relative_components_from(srcdirs, destdirs)
754+
s = File.join(*base)
755+
else
756+
srcdirs = fu_clean_components(*fu_split_path(s))
757+
base = fu_relative_components_from(fu_split_path(Dir.pwd), destdirs)
758+
while srcdirs.first&. == ".." and base.last&.!=("..") and !fu_starting_path?(base.last)
759+
srcdirs.shift
760+
base.pop
761+
end
762+
s = File.join(*base, *srcdirs)
763+
end
764+
fu_output_message "ln -s#{options} #{s} #{d}" if verbose
765+
next if noop
766+
remove_file d, true if force
767+
File.symlink s, d
768+
end
769+
case srcs.size
770+
when 0
771+
when 1
772+
link[srcs[0], target_directory && File.directory?(dest)]
773+
else
774+
srcs.each(&link)
775+
end
776+
end
777+
module_function :ln_sr
778+
732779
# Creates {hard links}[https://en.wikipedia.org/wiki/Hard_link]; returns +nil+.
733780
#
734781
# Arguments +src+ and +dest+
@@ -2436,15 +2483,15 @@ def fu_each_src_dest(src, dest) #:nodoc:
24362483
end
24372484
private_module_function :fu_each_src_dest
24382485

2439-
def fu_each_src_dest0(src, dest) #:nodoc:
2486+
def fu_each_src_dest0(src, dest, target_directory = true) #:nodoc:
24402487
if tmp = Array.try_convert(src)
24412488
tmp.each do |s|
24422489
s = File.path(s)
2443-
yield s, File.join(dest, File.basename(s))
2490+
yield s, (target_directory ? File.join(dest, File.basename(s)) : dest)
24442491
end
24452492
else
24462493
src = File.path(src)
2447-
if File.directory?(dest)
2494+
if target_directory and File.directory?(dest)
24482495
yield src, File.join(dest, File.basename(src))
24492496
else
24502497
yield src, File.path(dest)
@@ -2468,6 +2515,56 @@ def fu_output_message(msg) #:nodoc:
24682515
end
24692516
private_module_function :fu_output_message
24702517

2518+
def fu_split_path(path)
2519+
path = File.path(path)
2520+
list = []
2521+
until (parent, base = File.split(path); parent == path or parent == ".")
2522+
list << base
2523+
path = parent
2524+
end
2525+
list << path
2526+
list.reverse!
2527+
end
2528+
private_module_function :fu_split_path
2529+
2530+
def fu_relative_components_from(target, base) #:nodoc:
2531+
i = 0
2532+
while target[i]&.== base[i]
2533+
i += 1
2534+
end
2535+
Array.new(base.size-i, '..').concat(target[i..-1])
2536+
end
2537+
private_module_function :fu_relative_components_from
2538+
2539+
def fu_clean_components(*comp)
2540+
comp.shift while comp.first == "."
2541+
return comp if comp.empty?
2542+
clean = [comp.shift]
2543+
path = File.join(*clean, "") # ending with File::SEPARATOR
2544+
while c = comp.shift
2545+
if c == ".." and clean.last != ".." and !(fu_have_symlink? && File.symlink?(path))
2546+
clean.pop
2547+
path.chomp!(%r((?<=\A|/)[^/]+/\z), "")
2548+
else
2549+
clean << c
2550+
path << c << "/"
2551+
end
2552+
end
2553+
clean
2554+
end
2555+
private_module_function :fu_clean_components
2556+
2557+
if fu_windows?
2558+
def fu_starting_path?(path)
2559+
path&.start_with?(%r(\w:|/))
2560+
end
2561+
else
2562+
def fu_starting_path?(path)
2563+
path&.start_with?("/")
2564+
end
2565+
end
2566+
private_module_function :fu_starting_path?
2567+
24712568
# This hash table holds command options.
24722569
OPT_TABLE = {} #:nodoc: internal use only
24732570
(private_instance_methods & methods(false)).inject(OPT_TABLE) {|tbl, name|

test/fileutils/test_fileutils.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,43 @@ def test_ln_sf_pathname
998998
}
999999
end if have_symlink?
10001000

1001+
def test_ln_sr
1002+
check_singleton :ln_sr
1003+
1004+
TARGETS.each do |fname|
1005+
begin
1006+
lnfname = 'tmp/lnsdest'
1007+
ln_sr fname, lnfname
1008+
assert FileTest.symlink?(lnfname), 'not symlink'
1009+
assert_equal "../#{fname}", File.readlink(lnfname), fname
1010+
ensure
1011+
rm_f lnfname
1012+
end
1013+
end
1014+
mkdir 'data/src'
1015+
File.write('data/src/xxx', 'ok')
1016+
File.symlink '../data/src', 'tmp/src'
1017+
ln_sr 'tmp/src/xxx', 'data'
1018+
assert File.symlink?('data/xxx')
1019+
assert_equal 'ok', File.read('data/xxx')
1020+
end if have_symlink?
1021+
1022+
def test_ln_sr_broken_symlink
1023+
assert_nothing_raised {
1024+
ln_sr 'tmp/symlink', 'tmp/symlink'
1025+
}
1026+
end if have_symlink? and !no_broken_symlink?
1027+
1028+
def test_ln_sr_pathname
1029+
# pathname
1030+
touch 'tmp/lns_dest'
1031+
assert_nothing_raised {
1032+
ln_sr Pathname.new('tmp/lns_dest'), 'tmp/symlink_tmp1'
1033+
ln_sr 'tmp/lns_dest', Pathname.new('tmp/symlink_tmp2')
1034+
ln_sr Pathname.new('tmp/lns_dest'), Pathname.new('tmp/symlink_tmp3')
1035+
}
1036+
end if have_symlink?
1037+
10011038
def test_mkdir
10021039
check_singleton :mkdir
10031040

0 commit comments

Comments
 (0)