-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Conversation
src/file_utils.cr
Outdated
# FileUtils.ln_s("/var/log", "logs") | ||
# FileUtils.ln_s("src", "/tmp") | ||
# ``` | ||
def ln_s(old_path : String, new_path : String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're not renaming, so old and new aren't really precise. Something like source and destination would be clearer.
Hi, I was confused at first, but I saw that your code is more than plain aliases. Which is good. I know that Ruby does Second, instead of the def symlink(source, destination, *, force = false); ...; end
# Usage would look like this:
symlink("foo", "bar", force: user_config.force?) # No need for a clutter-if with an argument
symlink("foo", "bar", force: true) # Passing a constant value is also easy
symlink("foo", "bar", true) # Less clear: What's "true" for? What is true in this case? I'm no core maintainer, so someone else has more say in this. I think the shown functionality in this PR (With some cleaning) is useful and should be part of Crystal. Not part of this PR, but IMHO, we should rethink the |
src/file_utils.cr
Outdated
# ``` | ||
# FileUtils.ln(["vim", "emacs", "nano"], "/usr/bin") | ||
# ``` | ||
def ln(old_paths : Enumerable(String), dest_dir : String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like how you accept a Enumerable(String)
there 👍
src/file_utils.cr
Outdated
# ``` | ||
# FileUtils.ln_sf("foo.c", "bar.c") | ||
# ``` | ||
def ln_sf(old_path : String, new_path : String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As noted in the comment, use a force
argument instead of having a separate forcing version.
spec/std/file_utils_spec.cr
Outdated
|
||
describe "ln" do | ||
it "should create a hardlink" do | ||
path1 = "/tmp/crystal_ln_test_#{Process.pid}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Tempfile.dirname
to get the path to the temporary directory instead of hardcoding it. Consider using Tempfile
itself to generate temporary "test" files instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it would be a good idea to do this in another PR? /tmp
is hardcoded all over this file, and might be good to update the tests all at once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes in that case it would be a good follow-up PR.
spec/std/file_utils_spec.cr
Outdated
end | ||
|
||
it "should create multiple hardlinks inside a destination dir" do | ||
paths = 3.times.map { |i| "/tmp/crystal_ln_test_#{Process.pid + i}" }.to_a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of x.times.map{ y }.to_a
use Array.new(x){ y }
spec/std/file_utils_spec.cr
Outdated
path1 = "/tmp/crystal_ln_test_#{Process.pid}" | ||
path2 = "/tmp/crystal_ln_test_#{Process.pid + 1}" | ||
|
||
expect_raises Errno do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice if it could also check for the expected errno value itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this look reasonable?
expect_raises Errno do
FileUtils.ln(path1, path2)
end
Errno.value.should eq(Errno::ENOENT)
I wasn't sure if Errno.value
could be clobbered by other threads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't the errno exception contain the ENOENT
as part of its message? If so, you can use expect_raises(Errno, /ENOENT/)
to expect the method to raise an Errno, with a message containing the regex.
However, errno is a Thread Local variable, so your code should work too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like ENOENT
gets stringified to "No such file or directory":
Expected Errno with message matching /ENOENT/, got #<Errno: Error creating link from /tmp/crystal_ln_test_28472 to /tmp/crystal_ln_test_28473: No such file or directory> with backtrace:
I guess I could match on that, but it feels a little less reliable. I'll go with the Errno.value
check (I don't know how I forgot that errno
is thread local 🙂).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to use exception object for that:
ex = expect_raises Errno do
FileUtils.ln(path1, path2)
end
ex.errno.should eq(Errno::ENOENT)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @Sija!
FileUtils.ln_sf(path1, path2) | ||
File.symlink?(path1).should be_false | ||
File.symlink?(path2).should be_true | ||
ensure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty ensure
block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Filled it in 😄
src/file_utils.cr
Outdated
end | ||
end | ||
|
||
# Like `#ln_s(String, String)`, but overwrites `new_path` if it exists and is not a directory. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decent documentation overall 👍 Could you add a short description to each usage sample on what it'll do? That improves skim-ability of the docs:
# Create a symbolic link, pointing from bar to foo.
FileUtils.ln_sf("foo", "bar")
Thanks for the review and comments @Papierkorb!
I agree 100%, but I wasn't sure whether that would be appropriate given the scheme already in use in
I'm of two minds on that. Merging FileUtils into File and Dir would avoid a lot of confusion, but many of the FileUtils versions also add Unix semantics (like |
02bcb51
to
01ca324
Compare
@Papierkorb The purpose of |
Anything further I should do on this PR? |
# # 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated!
src/file_utils.cr
Outdated
# 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If dest_path
is a dir, this won't work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's consistent with ln -sf
's behavior -- if dest_path
is a dir, then dest_path/src_path
is created rather than dest_path
being deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes but dest_path/src_path
can exist, -f
imples that file is deleted, which doesn't happen here.
8f1c4ad
to
6974e92
Compare
src/file_utils.cr
Outdated
if File.file?(dest_path) | ||
File.delete(dest_path) | ||
elsif File.directory?(dest_path) | ||
dest_file = File.join(dest_path, File.basename(src_path)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point we just worked out the dest_path
, why not call File.symlink
directly from this function in both cases?
6974e92
to
96fde85
Compare
Anything else I can do here? 😄 |
src/file_utils.cr
Outdated
if File.file?(dest_path) | ||
File.delete(dest_path) | ||
File.symlink(src_path, dest_path) | ||
elsif File.directory?(dest_path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if dest_path
is neither a file or directory, for example if it's nonexistant? It should still link.
if File.directory?(dest_path)
dest_path = File.join(dest_path, File.basename(src_path))
end
File.delete(path) if File.file?(dest_path)
File.symlink(src_path, dest_path)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you added specs for that? Also please please push update commits instead of squashing. It makes it far easier tor view just the changes on a large PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add a spec in a bit (and sorry for the squashes).
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).
Seems like GTG, merge 🕦 ? |
Did we seriously just merge a method named |
I still think this is the right naming, both for consistency with Ruby's |
@oprypin we already have a method for I just want the module out of the stdlib entirely but unfortunately thats not really possible until we get |
* 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
These new methods in the
FileUtils
module are based largely ontheir Ruby equivalents.
The major difference between this and the Ruby implementation is that
ln
andln_s
raiseArgumentError
s in Crystal on errors,while Ruby raises
Errno
. Other than that, the only differencesare the lack of an
options
argument (consistent with otherCrystal
FileUtils
methods).I didn't get any comments on the RFC I made (#5369), so I don't know if these changes are welcome or not. Please let me know if they aren't!