Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FileUtils: Add ln, ln_s, and ln_sf #5421

Merged
merged 2 commits into from
May 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions spec/std/file_utils_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -480,4 +480,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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty ensure block

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Filled it in 😄

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a version of this which takes multiple paths.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add this, but it'll functionally be a duplicate of ln_s (since taking multiple paths only makes sense if dest is a directory, and the force behavior is only applied if dest is a file). Should I still do it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is force only applied if dest is a file? There can still be name clashes if the destination is a directory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

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