Skip to content

Commit 4f1c4a6

Browse files
committed
Add ln_sr method and relative: option to ln_s
1 parent 346a71b commit 4f1c4a6

File tree

2 files changed

+137
-4
lines changed

2 files changed

+137
-4
lines changed

lib/fileutils.rb

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ def cp_lr(src, dest, noop: nil, verbose: nil,
690690
# Keyword arguments:
691691
#
692692
# - <tt>force: true</tt> - overwrites +dest+ if it exists.
693+
# - <tt>relative: false</tt> - create links relative to +dest+.
693694
# - <tt>noop: true</tt> - does not create links.
694695
# - <tt>verbose: true</tt> - prints an equivalent command:
695696
#
@@ -709,7 +710,10 @@ def cp_lr(src, dest, noop: nil, verbose: nil,
709710
#
710711
# Related: FileUtils.ln_sf.
711712
#
712-
def ln_s(src, dest, force: nil, noop: nil, verbose: nil)
713+
def ln_s(src, dest, force: nil, relative: false, target_directory: true, noop: nil, verbose: nil)
714+
if relative
715+
return ln_sr(src, dest, force: force, noop: noop, verbose: verbose)
716+
end
713717
fu_output_message "ln -s#{force ? 'f' : ''} #{[src,dest].flatten.join ' '}" if verbose
714718
return if noop
715719
fu_each_src_dest0(src, dest) do |s,d|
@@ -729,6 +733,48 @@ def ln_sf(src, dest, noop: nil, verbose: nil)
729733
end
730734
module_function :ln_sf
731735

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

2431-
def fu_each_src_dest0(src, dest) #:nodoc:
2477+
def fu_each_src_dest0(src, dest, target_directory = true) #:nodoc:
24322478
if tmp = Array.try_convert(src)
24332479
tmp.each do |s|
24342480
s = File.path(s)
2435-
yield s, File.join(dest, File.basename(s))
2481+
yield s, (target_directory ? File.join(dest, File.basename(s)) : dest)
24362482
end
24372483
else
24382484
src = File.path(src)
2439-
if File.directory?(dest)
2485+
if target_directory and File.directory?(dest)
24402486
yield src, File.join(dest, File.basename(src))
24412487
else
24422488
yield src, File.path(dest)
@@ -2460,6 +2506,56 @@ def fu_output_message(msg) #:nodoc:
24602506
end
24612507
private_module_function :fu_output_message
24622508

2509+
def fu_split_path(path)
2510+
path = File.path(path)
2511+
list = []
2512+
until (parent, base = File.split(path); parent == path or parent == ".")
2513+
list << base
2514+
path = parent
2515+
end
2516+
list << path
2517+
list.reverse!
2518+
end
2519+
private_module_function :fu_split_path
2520+
2521+
def fu_relative_components_from(target, base) #:nodoc:
2522+
i = 0
2523+
while target[i]&.== base[i]
2524+
i += 1
2525+
end
2526+
Array.new(base.size-i, '..').concat(target[i..-1])
2527+
end
2528+
private_module_function :fu_relative_components_from
2529+
2530+
def fu_clean_components(*comp)
2531+
comp.shift while comp.first == "."
2532+
return comp if comp.empty?
2533+
clean = [comp.shift]
2534+
path = File.join(*clean, "") # ending with File::SEPARATOR
2535+
while c = comp.shift
2536+
if c == ".." and clean.last != ".." and !(fu_have_symlink? && File.symlink?(path))
2537+
clean.pop
2538+
path.chomp!(%r((?<=\A|/)[^/]+/\z), "")
2539+
else
2540+
clean << c
2541+
path << c << "/"
2542+
end
2543+
end
2544+
clean
2545+
end
2546+
private_module_function :fu_clean_components
2547+
2548+
if fu_windows?
2549+
def fu_starting_path?(path)
2550+
path&.start_with?(%r(\w:|/))
2551+
end
2552+
else
2553+
def fu_starting_path?(path)
2554+
path&.start_with?("/")
2555+
end
2556+
end
2557+
private_module_function :fu_starting_path?
2558+
24632559
# This hash table holds command options.
24642560
OPT_TABLE = {} #:nodoc: internal use only
24652561
(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
@@ -980,6 +980,43 @@ def test_ln_sf_pathname
980980
}
981981
end if have_symlink?
982982

983+
def test_ln_sr
984+
check_singleton :ln_sr
985+
986+
TARGETS.each do |fname|
987+
begin
988+
lnfname = 'tmp/lnsdest'
989+
ln_sr fname, lnfname
990+
assert FileTest.symlink?(lnfname), 'not symlink'
991+
assert_equal "../#{fname}", File.readlink(lnfname), fname
992+
ensure
993+
rm_f lnfname
994+
end
995+
end
996+
mkdir 'data/src'
997+
File.write('data/src/xxx', 'ok')
998+
File.symlink '../data/src', 'tmp/src'
999+
ln_sr 'tmp/src/xxx', 'data'
1000+
assert File.symlink?('data/xxx')
1001+
assert_equal 'ok', File.read('data/xxx')
1002+
end if have_symlink?
1003+
1004+
def test_ln_sr_broken_symlink
1005+
assert_nothing_raised {
1006+
ln_sr 'tmp/symlink', 'tmp/symlink'
1007+
}
1008+
end if have_symlink? and !no_broken_symlink?
1009+
1010+
def test_ln_sr_pathname
1011+
# pathname
1012+
touch 'tmp/lns_dest'
1013+
assert_nothing_raised {
1014+
ln_sr Pathname.new('tmp/lns_dest'), 'tmp/symlink_tmp1'
1015+
ln_sr 'tmp/lns_dest', Pathname.new('tmp/symlink_tmp2')
1016+
ln_sr Pathname.new('tmp/lns_dest'), Pathname.new('tmp/symlink_tmp3')
1017+
}
1018+
end if have_symlink?
1019+
9831020
def test_mkdir
9841021
check_singleton :mkdir
9851022

0 commit comments

Comments
 (0)