Skip to content

Commit

Permalink
FileUtils: Add ln, ln_s, and ln_sf
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
woodruffw committed Dec 20, 2017
1 parent 78fa174 commit 02bcb51
Show file tree
Hide file tree
Showing 2 changed files with 259 additions and 0 deletions.
184 changes: 184 additions & 0 deletions spec/std/file_utils_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -467,4 +467,188 @@ describe "FileUtils" do
FileUtils.rm([path1, path2, path2])
end
end

describe "ln" do
it "should create 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 "should create 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 "should create 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 "should fail with a nonexistent source" do
path1 = "/tmp/crystal_ln_test_#{Process.pid}"
path2 = "/tmp/crystal_ln_test_#{Process.pid + 1}"

expect_raises Errno do
FileUtils.ln(path1, path2)
end

Errno.value.should eq(Errno::ENOENT)
end

it "should fail 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])
expect_raises Errno do
FileUtils.ln(path1, path2)
end

Errno.value.should eq(Errno::EEXIST)
ensure
FileUtils.rm_rf([path1, path2])
end
end
end

describe "ln_s" do
it "should create 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 "should create 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 "should create multiple symlinks inside a destination dir" do
paths = 3.times.map { |i| "/tmp/crystal_ln_s_test_#{Process.pid + i}" }.to_a
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 "should work 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
expect_raises Errno do
File.real_path(path2)
end

Errno.value.should eq(Errno::ENOENT)
ensure
FileUtils.rm_rf(path2)
end
end

it "should fail 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])
expect_raises Errno do
FileUtils.ln_s(path1, path2)
end

Errno.value.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
end
end
75 changes: 75 additions & 0 deletions src/file_utils.cr
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,81 @@ 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.
#
# ```
# # 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)
File.delete(dest_path) if File.file?(dest_path)
ln_s(src_path, dest_path)
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 02bcb51

Please sign in to comment.