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 31, 2017
1 parent 86a4bb0 commit 6974e92
Show file tree
Hide file tree
Showing 2 changed files with 326 additions and 0 deletions.
226 changes: 226 additions & 0 deletions spec/std/file_utils_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
100 changes: 100 additions & 0 deletions src/file_utils.cr
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,106 @@ 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.file?(dest_path)
File.delete(dest_path)
elsif File.directory?(dest_path)
dest_file = File.join(dest_path, File.basename(src_path))
File.delete(dest_file) if File.file?(dest_file)
end

ln_s(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 6974e92

Please sign in to comment.