From 84c02fef374c0abc972b6c68444b096dca15a0b5 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Sat, 9 Dec 2017 02:21:20 -0500 Subject: [PATCH 1/2] FileUtils: Add `ln`, `ln_s`, and `ln_sf` These new methods in the FileUtils module are based largely on their Ruby equivalents. The major difference between this and the Ruby implementation is that `ln` and `ln_s` raise `ArgumentError`s in Crystal on errors, while Ruby raises `Errno`. Other than that, the only differences are the lack of an `options` argument (consistent with other Crystal `FileUtils` methods). --- spec/std/file_utils_spec.cr | 226 ++++++++++++++++++++++++++++++++++++ src/file_utils.cr | 98 ++++++++++++++++ 2 files changed, 324 insertions(+) diff --git a/spec/std/file_utils_spec.cr b/spec/std/file_utils_spec.cr index dc1cbfb0c4df..550219f70360 100644 --- a/spec/std/file_utils_spec.cr +++ b/spec/std/file_utils_spec.cr @@ -480,4 +480,230 @@ describe "FileUtils" do FileUtils.rm([path1, path2, path2]) end end + + describe "ln" do + it "creates a hardlink" do + path1 = "/tmp/crystal_ln_test_#{Process.pid}" + path2 = "/tmp/crystal_ln_test_#{Process.pid + 1}" + + begin + FileUtils.touch(path1) + FileUtils.ln(path1, path2) + File.exists?(path2).should be_true + File.symlink?(path2).should be_false + ensure + FileUtils.rm_rf([path1, path2]) + end + end + + it "creates a hardlink inside a destination dir" do + path1 = "/tmp/crystal_ln_test_#{Process.pid}" + path2 = "/tmp/crystal_ln_test_#{Process.pid + 1}/" + path3 = File.join(path2, File.basename(path1)) + + begin + FileUtils.touch(path1) + FileUtils.mkdir(path2) + FileUtils.ln(path1, path2) + File.exists?(path3).should be_true + File.symlink?(path3).should be_false + ensure + FileUtils.rm_rf([path1, path2]) + end + end + + it "creates multiple hardlinks inside a destination dir" do + paths = Array.new(3) { |i| "/tmp/crystal_ln_test_#{Process.pid + i}" } + dir_path = "/tmp/crystal_ln_test_#{Process.pid + 3}/" + + begin + paths.each { |path| FileUtils.touch(path) } + FileUtils.mkdir(dir_path) + FileUtils.ln(paths, dir_path) + + paths.each do |path| + link_path = File.join(dir_path, File.basename(path)) + File.exists?(link_path).should be_true + File.symlink?(link_path).should be_false + end + ensure + FileUtils.rm_rf(paths) + FileUtils.rm_rf(dir_path) + end + end + + it "fails with a nonexistent source" do + path1 = "/tmp/crystal_ln_test_#{Process.pid}" + path2 = "/tmp/crystal_ln_test_#{Process.pid + 1}" + + ex = expect_raises Errno do + FileUtils.ln(path1, path2) + end + + ex.errno.should eq(Errno::ENOENT) + end + + it "fails with an extant destination" do + path1 = "/tmp/crystal_ln_test_#{Process.pid}" + path2 = "/tmp/crystal_ln_test_#{Process.pid + 1}" + + begin + FileUtils.touch([path1, path2]) + + ex = expect_raises Errno do + FileUtils.ln(path1, path2) + end + + ex.errno.should eq(Errno::EEXIST) + ensure + FileUtils.rm_rf([path1, path2]) + end + end + end + + describe "ln_s" do + it "creates a symlink" do + path1 = "/tmp/crystal_ln_s_test_#{Process.pid}" + path2 = "/tmp/crystal_ln_s_test_#{Process.pid + 1}" + + begin + FileUtils.touch(path1) + FileUtils.ln_s(path1, path2) + File.exists?(path2).should be_true + File.symlink?(path2).should be_true + ensure + FileUtils.rm_rf([path1, path2]) + end + end + + it "creates a symlink inside a destination dir" do + path1 = "/tmp/crystal_ln_s_test_#{Process.pid}" + path2 = "/tmp/crystal_ln_s_test_#{Process.pid + 1}/" + path3 = File.join(path2, File.basename(path1)) + + begin + FileUtils.touch(path1) + FileUtils.mkdir(path2) + FileUtils.ln_s(path1, path2) + File.exists?(path3).should be_true + File.symlink?(path3).should be_true + ensure + FileUtils.rm_rf([path1, path2]) + end + end + + it "creates multiple symlinks inside a destination dir" do + paths = Array.new(3) { |i| "/tmp/crystal_ln_s_test_#{Process.pid + i}" } + dir_path = "/tmp/crystal_ln_s_test_#{Process.pid + 3}/" + + begin + paths.each { |path| FileUtils.touch(path) } + FileUtils.mkdir(dir_path) + FileUtils.ln_s(paths, dir_path) + + paths.each do |path| + link_path = File.join(dir_path, File.basename(path)) + File.exists?(link_path).should be_true + File.symlink?(link_path).should be_true + end + ensure + FileUtils.rm_rf(paths) + FileUtils.rm_rf(dir_path) + end + end + + it "works with a nonexistent source" do + path1 = "/tmp/crystal_ln_s_test_#{Process.pid}" + path2 = "/tmp/crystal_ln_s_test_#{Process.pid + 1}" + + begin + FileUtils.ln_s(path1, path2) + File.exists?(path2).should be_false + File.symlink?(path2).should be_true + + ex = expect_raises Errno do + File.real_path(path2) + end + + ex.errno.should eq(Errno::ENOENT) + ensure + FileUtils.rm_rf(path2) + end + end + + it "fails with an extant destination" do + path1 = "/tmp/crystal_ln_s_test_#{Process.pid}" + path2 = "/tmp/crystal_ln_s_test_#{Process.pid + 1}" + + begin + FileUtils.touch([path1, path2]) + + ex = expect_raises Errno do + FileUtils.ln_s(path1, path2) + end + + ex.errno.should eq(Errno::EEXIST) + ensure + FileUtils.rm_rf([path1, path2]) + end + end + end + + describe "ln_sf" do + it "overwrites a destination file" do + path1 = "/tmp/crystal_ln_sf_test_#{Process.pid}" + path2 = "/tmp/crystal_ln_sf_test_#{Process.pid + 1}" + + begin + FileUtils.touch([path1, path2]) + File.symlink?(path1).should be_false + File.symlink?(path2).should be_false + + FileUtils.ln_sf(path1, path2) + File.symlink?(path1).should be_false + File.symlink?(path2).should be_true + ensure + FileUtils.rm_rf([path1, path2]) + end + end + + it "overwrites a destination file inside a dir" do + dir = "/tmp/crystal_ln_sf_test_#{Process.pid}/" + path1 = File.join(dir, "crystal_ln_sf_test_#{Process.pid + 1}") + path2 = "/tmp/crystal_ln_sf_test_#{Process.pid + 1}" + + begin + FileUtils.mkdir(dir) + FileUtils.touch([path1, path2]) + File.symlink?(path1).should be_false + File.symlink?(path2).should be_false + + FileUtils.ln_sf(path2, dir) + File.symlink?(path1).should be_true + File.symlink?(path2).should be_false + ensure + FileUtils.rm_rf([dir, path2]) + end + end + + it "creates multiple symlinks in a destination dir, with overwrites" do + dir = "/tmp/crystal_ln_sf_test_#{Process.pid + 3}" + paths1 = Array.new(3) { |i| "crystal_ln_sf_test_#{Process.pid + i}" } + paths2 = paths1.map { |p| File.join("/tmp/", p) } + paths3 = paths1.map { |p| File.join(dir, p) } + + begin + FileUtils.mkdir(dir) + FileUtils.touch(paths2 + paths3) + (paths2 + paths3).each { |p| File.symlink?(p).should be_false } + + FileUtils.ln_sf(paths2, dir) + paths2.each { |p| File.symlink?(p).should be_false } + paths3.each { |p| File.symlink?(p).should be_true } + ensure + FileUtils.rm_rf(paths2) + FileUtils.rm_rf(dir) + end + end + end end diff --git a/src/file_utils.cr b/src/file_utils.cr index f9e34450384c..9fd4905eedc9 100644 --- a/src/file_utils.cr +++ b/src/file_utils.cr @@ -146,6 +146,104 @@ module FileUtils end end + # Creates a hard link *dest_path* which points to *src_path*. + # If *dest_path* already exists and is a directory, creates a link *dest_path/src_path*. + # + # ``` + # # Create a hard link, pointing from /usr/bin/emacs to /usr/bin/vim + # FileUtils.ln("/usr/bin/vim", "/usr/bin/emacs") + # # Create a hard link, pointing from /tmp/foo.c to foo.c + # FileUtils.ln("foo.c", "/tmp") + # ``` + def ln(src_path : String, dest_path : String) + if Dir.exists?(dest_path) + File.link(src_path, File.join(dest_path, File.basename(src_path))) + else + File.link(src_path, dest_path) + end + end + + # Creates a hard link to each path in *src_paths* inside the *dest_dir* directory. + # If *dest_dir* is not a directory, raises an `ArgumentError`. + # + # ``` + # # Create /usr/bin/vim, /usr/bin/emacs, and /usr/bin/nano as hard links + # FileUtils.ln(["vim", "emacs", "nano"], "/usr/bin") + # ``` + def ln(src_paths : Enumerable(String), dest_dir : String) + raise ArgumentError.new("No such directory : #{dest_dir}") unless Dir.exists?(dest_dir) + + src_paths.each do |path| + ln(path, dest_dir) + end + end + + # Creates a symbolic link *dest_path* which points to *src_path*. + # If *dest_path* already exists and is a directory, creates a link *dest_path/src_path*. + # + # ``` + # # Create a symbolic link pointing from logs to /var/log + # FileUtils.ln_s("/var/log", "logs") + # # Create a symbolic link pointing from /tmp/src to src + # FileUtils.ln_s("src", "/tmp") + # ``` + def ln_s(src_path : String, dest_path : String) + if Dir.exists?(dest_path) + File.symlink(src_path, File.join(dest_path, File.basename(src_path))) + else + File.symlink(src_path, dest_path) + end + end + + # Creates a symbolic link to each path in *src_paths* inside the *dest_dir* directory. + # If *dest_dir* is not a directory, raises an `ArgumentError`. + # + # ``` + # # Create symbolic links in src/ pointing to every .c file in the current directory + # FileUtils.ln_s(Dir["*.c"], "src") + # ``` + def ln_s(src_paths : Enumerable(String), dest_dir : String) + raise ArgumentError.new("No such directory : #{dest_dir}") unless Dir.exists?(dest_dir) + + src_paths.each do |path| + ln_s(path, dest_dir) + end + end + + # Like `#ln_s(String, String)`, but overwrites `dest_path` if it exists and is not a directory + # or if `dest_path/src_path` exists. + # + # ``` + # # Create a symbolic link pointing from bar.c to foo.c, even if bar.c already exists + # FileUtils.ln_sf("foo.c", "bar.c") + # ``` + def ln_sf(src_path : String, dest_path : String) + if File.directory?(dest_path) + dest_path = File.join(dest_path, File.basename(src_path)) + end + + File.delete(dest_path) if File.file?(dest_path) + File.symlink(src_path, dest_path) + end + + # Creates a symbolic link to each path in *src_paths* inside the *dest_dir* directory, + # ignoring any overwritten paths. + # + # If *dest_dir* is not a directory, raises an `ArgumentError`. + # + # ``` + # # Create symbolic links in src/ pointing to every .c file in the current directory, + # # even if it means overwriting files in src/ + # FileUtils.ln_sf(Dir["*.c"], "src") + # ``` + def ln_sf(src_paths : Enumerable(String), dest_dir : String) + raise ArgumentError.new("No such directory : #{dest_dir}") unless Dir.exists?(dest_dir) + + src_paths.each do |path| + ln_sf(path, dest_dir) + end + end + # Creates a new directory at the given *path*. The linux-style permission *mode* # can be specified, with a default of 777 (0o777). # From f6b59c7e8e6d512770dcfa42a1ce1cea7799891d Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 11 Apr 2018 22:12:35 -0400 Subject: [PATCH 2/2] spec: Add FileUtils.ln_sf test for nonexistent dest --- spec/std/file_utils_spec.cr | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/std/file_utils_spec.cr b/spec/std/file_utils_spec.cr index 550219f70360..fc566113062d 100644 --- a/spec/std/file_utils_spec.cr +++ b/spec/std/file_utils_spec.cr @@ -705,5 +705,20 @@ describe "FileUtils" do FileUtils.rm_rf(dir) end end + + it "creates a symlink even if there's nothing to overwrite" do + path1 = "/tmp/crystal_ln_sf_test_#{Process.pid}" + path2 = "/tmp/crystal_ln_sf_test_#{Process.pid + 1}" + + begin + FileUtils.touch(path1) + File.exists?(path2).should be_false + + FileUtils.ln_sf(path1, path2) + File.symlink?(path2).should be_true + ensure + FileUtils.rm_rf([path1, path2]) + end + end end end