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

Conversation

woodruffw
Copy link
Contributor

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 ArgumentErrors 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).

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!

# FileUtils.ln_s("/var/log", "logs")
# FileUtils.ln_s("src", "/tmp")
# ```
def ln_s(old_path : String, new_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.

You're not renaming, so old and new aren't really precise. Something like source and destination would be clearer.

@Papierkorb
Copy link
Contributor

Papierkorb commented Dec 20, 2017

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 #ln_s and friends, but I never liked that scheme. #ln should be #link or #hardlink, and #ln_s something like #symlink or #symbolic_link. Much clearer in naming to the uninitated, and easier to search for.

Second, instead of the f variants, have a force: argument instead. This makes it much more powerful to use, easier to read, and less to remember for the developer. You can force the developer to use a named argument using an empty * argument:

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 FileUtils module however. Maybe we can get rid of it by adding this functionality into their sibling methods in File and Dir?

# ```
# FileUtils.ln(["vim", "emacs", "nano"], "/usr/bin")
# ```
def ln(old_paths : Enumerable(String), dest_dir : String)
Copy link
Contributor

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 👍

# ```
# FileUtils.ln_sf("foo.c", "bar.c")
# ```
def ln_sf(old_path : String, new_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.

As noted in the comment, use a force argument instead of having a separate forcing version.


describe "ln" do
it "should create a hardlink" do
path1 = "/tmp/crystal_ln_test_#{Process.pid}"
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

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

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 }

path1 = "/tmp/crystal_ln_test_#{Process.pid}"
path2 = "/tmp/crystal_ln_test_#{Process.pid + 1}"

expect_raises Errno do
Copy link
Contributor

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.

Copy link
Contributor Author

@woodruffw woodruffw Dec 20, 2017

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.

Copy link
Contributor

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

Copy link
Contributor Author

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 🙂).

Copy link
Contributor

@Sija Sija Dec 20, 2017

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)

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 @Sija!

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 😄

end
end

# Like `#ln_s(String, String)`, but overwrites `new_path` if it exists and is not a directory.
Copy link
Contributor

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")

@woodruffw
Copy link
Contributor Author

Thanks for the review and comments @Papierkorb!

I know that Ruby does #ln_s and friends, but I never liked that scheme. #ln should be #link or #hardlink, and #ln_s something like #symlink or #symbolic_link. Much clearer in naming to the uninitated, and easier to search for.

I agree 100%, but I wasn't sure whether that would be appropriate given the scheme already in use in FileUtils -- cp_r, mkdir_p, and rm_rf all use the "unix-command-plus-flags" naming scheme already, so I didn't know if it would be appropriate to deviate from it. I'll let one of the core maintainers decide what the approach should be here 😄

Not part of this PR, but IMHO, we should rethink the FileUtils module however. Maybe we can get rid of it by adding this functionality into their sibling methods in File and Dir?

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 cp and ln copying into a destination directory implicitly). That additional behavior really comes in handy when writing quick scripts, but would probably be confusing if added to the equivalent methods in File and Dir.

@woodruffw woodruffw force-pushed the fileutils-ln branch 3 times, most recently from 02bcb51 to 01ca324 Compare December 20, 2017 23:17
@RX14
Copy link
Contributor

RX14 commented Dec 21, 2017

@Papierkorb The purpose of FileUtils is to keep the same naming as the commandline. I dislike the module, and I think that it should be a shard personally. In addition, more work needs to be done on base File/Dir and possibly add Path. That being said, all of that is far outside the scope of this PR and we should keep the conventions of FileUtils in this PR.

@woodruffw
Copy link
Contributor Author

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)
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!

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

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@woodruffw woodruffw force-pushed the fileutils-ln branch 2 times, most recently from 8f1c4ad to 6974e92 Compare December 31, 2017 04:53
if File.file?(dest_path)
File.delete(dest_path)
elsif File.directory?(dest_path)
dest_file = File.join(dest_path, File.basename(src_path))
Copy link
Contributor

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?

@woodruffw
Copy link
Contributor Author

Anything else I can do here? 😄

if File.file?(dest_path)
File.delete(dest_path)
File.symlink(src_path, dest_path)
elsif File.directory?(dest_path)
Copy link
Contributor

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)

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!

Copy link
Contributor

@RX14 RX14 Apr 12, 2018

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.

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'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).
@Sija
Copy link
Contributor

Sija commented May 11, 2018

Seems like GTG, merge 🕦 ?

@RX14 RX14 merged commit 07c7066 into crystal-lang:master May 14, 2018
@RX14 RX14 added this to the Next milestone May 14, 2018
@woodruffw woodruffw deleted the fileutils-ln branch May 14, 2018 16:24
@oprypin
Copy link
Member

oprypin commented May 14, 2018

Did we seriously just merge a method named ln_sf?

@woodruffw
Copy link
Contributor Author

Did we seriously just merge a method named ln_sf?

I still think this is the right naming, both for consistency with Ruby's FileUtils and in line with the general distinction between File, Dir, and FileUtils -- File and Dir represent the general notions of files and directories, and FileUtils represents the UNIX-y and userspace-y convenience layer that's frequently laid over them.

@RX14
Copy link
Contributor

RX14 commented May 14, 2018

@oprypin we already have a method for rm_rf. It's not nice but it's how the module works.

I just want the module out of the stdlib entirely but unfortunately thats not really possible until we get Path done.

chris-huxtable pushed a commit to chris-huxtable/crystal that referenced this pull request Jun 6, 2018
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants