Skip to content

Commit

Permalink
FileUtils: Add ln, ln_s, and ln_sf (#5421)
Browse files Browse the repository at this point in the history
* 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: Add FileUtils.ln_sf test for nonexistent dest
  • Loading branch information
woodruffw authored and RX14 committed May 14, 2018
1 parent 1ecc65b commit 07c7066
Show file tree
Hide file tree
Showing 2 changed files with 339 additions and 0 deletions.
241 changes: 241 additions & 0 deletions spec/std/file_utils_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -497,4 +497,245 @@ 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

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
98 changes: 98 additions & 0 deletions src/file_utils.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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).
#
Expand Down

0 comments on commit 07c7066

Please sign in to comment.