diff --git a/NEWS.md b/NEWS.md index 3c10d3663c554..cbadec18bbe0b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -243,6 +243,12 @@ Library improvements * The `randexp` and `randexp!` functions are exported ([#9144]) + * File + + * Added function `readlink` which returns the value of a symbolic link "path" ([#10714]). + + * The `cp` function now accepts keyword arguments `remove_destination` and `follow_symlinks` ([#10888]). + * Other improvements * You can now tab-complete Emoji characters via their [short names](http://www.emoji-cheat-sheet.com/), using `\:name:` ([#10709]). @@ -1374,9 +1380,11 @@ Too numerous to mention. [#10659]: https://github.com/JuliaLang/julia/issues/10659 [#10679]: https://github.com/JuliaLang/julia/issues/10679 [#10709]: https://github.com/JuliaLang/julia/issues/10709 +[#10714]: https://github.com/JuliaLang/julia/pull/10714 [#10747]: https://github.com/JuliaLang/julia/issues/10747 [#10844]: https://github.com/JuliaLang/julia/issues/10844 [#10870]: https://github.com/JuliaLang/julia/issues/10870 [#10885]: https://github.com/JuliaLang/julia/issues/10885 +[#10888]: https://github.com/JuliaLang/julia/pull/10888 [#10893]: https://github.com/JuliaLang/julia/pull/10893 [#10914]: https://github.com/JuliaLang/julia/issues/10914 diff --git a/base/file.jl b/base/file.jl index 5d2ef92715118..f5fdb1fda57b1 100644 --- a/base/file.jl +++ b/base/file.jl @@ -70,18 +70,47 @@ end # The following use Unix command line facilites -function cp(src::AbstractString, dst::AbstractString; recursive::Bool=false) - if islink(src) || !isdir(src) - FS.sendfile(src, dst) - elseif recursive - mkdir(dst) - for p in readdir(src) - cp(joinpath(src, p), joinpath(dst, p), recursive=recursive) +function cptree(src::AbstractString, dst::AbstractString; remove_destination::Bool=false, + follow_symlinks::Bool=false) + isdir(src) || throw(ArgumentError("'$src' is not a directory. Use `cp(src, dst)`")) + if ispath(dst) + if remove_destination + rm(dst; recursive=true) + else + throw(ArgumentError(string("'$dst' exists. `remove_destination=true` ", + "is required to remove '$dst' before copying."))) + end + end + mkdir(dst) + for name in readdir(src) + srcname = joinpath(src, name) + if !follow_symlinks && islink(srcname) + symlink(readlink(srcname), joinpath(dst, name)) + elseif isdir(srcname) + cptree(srcname, joinpath(dst, name); remove_destination=remove_destination, + follow_symlinks=follow_symlinks) + else + FS.sendfile(srcname, joinpath(dst, name)) end + end +end + +function cp(src::AbstractString, dst::AbstractString; remove_destination::Bool=false, + follow_symlinks::Bool=false) + if ispath(dst) + if remove_destination + rm(dst; recursive=true) + else + throw(ArgumentError(string("'$dst' exists. `remove_destination=true` ", + "is required to remove '$dst' before copying."))) + end + end + if !follow_symlinks && islink(src) + symlink(readlink(src), dst) + elseif isdir(src) + cptree(src, dst; remove_destination=remove_destination, follow_symlinks=follow_symlinks) else - throw(ArgumentError(string("'$src' is a directory. ", - "Use `cp(src, dst, recursive=true)` ", - "to copy directories recursively."))) + FS.sendfile(src, dst) end end mv(src::AbstractString, dst::AbstractString) = FS.rename(src, dst) diff --git a/doc/helpdb.jl b/doc/helpdb.jl index fb69e689ee6b1..aac90438399e1 100644 --- a/doc/helpdb.jl +++ b/doc/helpdb.jl @@ -5172,10 +5172,14 @@ Millisecond(v) "), -("Base","cp","cp(src::AbstractString, dst::AbstractString; recursive=false) +("Base","cp","cp(src::AbstractString, dst::AbstractString; remove_destination::Bool=false, follow_symlinks::Bool=false) - Copy a file from *src* to *dest*. Passing \"recursive=true\" will - enable recursive copying of directories. + Copy the file, link, or directory from *src* to *dest*. + \"remove_destination=true\" will first remove an existing `dst`. + + If `follow_symlinks=false`, and src is a symbolic link, dst will be created as a symbolic link. + If `follow_symlinks=true` and src is a symbolic link, dst will be a copy of the file or directory + `src` refers to. "), diff --git a/doc/stdlib/file.rst b/doc/stdlib/file.rst index f3b5a86f32b84..c6846dd735900 100644 --- a/doc/stdlib/file.rst +++ b/doc/stdlib/file.rst @@ -109,10 +109,14 @@ Like uperm but gets the permissions for people who neither own the file nor are a member of the group owning the file -.. function:: cp(src::AbstractString,dst::AbstractString; recursive=false) +.. function:: cp(src::AbstractString, dst::AbstractString; remove_destination::Bool=false, follow_symlinks::Bool=false) - Copy a file from `src` to `dest`. Passing ``recursive=true`` will enable - recursive copying of directories. + Copy the file, link, or directory from *src* to *dest*. + \"remove_destination=true\" will first remove an existing `dst`. + + If `follow_symlinks=false`, and src is a symbolic link, dst will be created as a symbolic link. + If `follow_symlinks=true` and src is a symbolic link, dst will be a copy of the file or directory + `src` refers to. .. function:: download(url,[localfile]) diff --git a/test/file.jl b/test/file.jl index ea5591b8eb9e5..46961a494ad60 100644 --- a/test/file.jl +++ b/test/file.jl @@ -344,53 +344,86 @@ emptyf = open(emptyfile) close(emptyf) rm(emptyfile) -# Test copy file -afile = joinpath(dir, "a.txt") -touch(afile) -af = open(afile, "r+") -write(af, "This is indeed a test") -bfile = joinpath(dir, "b.txt") -cp(afile, bfile) - -mktempdir() do tmpdir - src = joinpath(tmpdir, "src") - dst = joinpath(tmpdir, "dst") - mkdir(src) - - @test_throws ArgumentError cp(src, dst) +########################################################################### +## This section tests cp files, directories, absolute and relative links. # +########################################################################### +function check_dir(orig_path::AbstractString, copied_path::AbstractString, follow_symlinks::Bool) + isdir(orig_path) || throw(ArgumentError("'$orig_path' is not a directory.")) + # copied_path must also be a dir. + @test isdir(copied_path) + readir_orig = readdir(orig_path) + readir_copied = readdir(copied_path) + @test readir_orig == readir_copied + # check recursive + for name in readir_orig + @test name in readir_copied + check_cp(joinpath(orig_path, name), joinpath(copied_path, name), follow_symlinks) + end end -# Recursive copy -mktempdir() do tmpdir - src = joinpath(tmpdir, "src") - dst = joinpath(tmpdir, "dst") - mkdir(src) - touch(joinpath(src, "foo")) - mkdir(joinpath(src, "bar")) - touch(joinpath(src, "bar", "qux")) - - cp(src, dst, recursive=true) - @test isfile(joinpath(dst, "foo")) - @test isfile(joinpath(dst, "bar", "qux")) +function check_cp(orig_path::AbstractString, copied_path::AbstractString, follow_symlinks::Bool) + if islink(orig_path) + if !follow_symlinks + # copied_path must be a link + @test islink(copied_path) + readlink_orig = readlink(orig_path) + # copied_path must have the same link value: + # this is true for absolute and relative links + @test readlink_orig == readlink(copied_path) + if isabspath(readlink_orig) + @test isabspath(readlink(copied_path)) + end + else + # copied_path may not be a link if follow_symlinks=true + @test islink(orig_path) == !islink(copied_path) + if isdir(orig_path) + check_dir(orig_path, copied_path, follow_symlinks) + else + # copied_path must also be a file. + @test isfile(copied_path) + # copied_path must have same content + @test readall(orig_path) == readall(copied_path) + end + end + elseif isdir(orig_path) + check_cp_main(orig_path, copied_path, follow_symlinks) + else + # copied_path must also be a file. + @test isfile(copied_path) + # copied_path must have same content + @test readall(orig_path) == readall(copied_path) + end end -# issue #10434 -mktempdir() do tmpdir - src = joinpath(tmpdir, "src") - dst = joinpath(tmpdir, "dst") - mkdir(src) +function check_cp_main(orig::AbstractString, copied::AbstractString, follow_symlinks::Bool) + if isdir(orig) + check_dir(orig, copied, follow_symlinks) + else + check_cp(orig, copied, follow_symlinks) + end +end - try cp(src, dst) end - @test !ispath(dst) +function cp_and_test(src::AbstractString, dst::AbstractString, follow_symlinks::Bool) + cp(src, dst; follow_symlinks=follow_symlinks) + check_cp_main(src, dst, follow_symlinks) end # issue #8698 +# Test copy file +afile = joinpath(dir, "a.txt") +touch(afile) +af = open(afile, "r+") +write(af, "This is indeed a test") + +bfile = joinpath(dir, "b.txt") +cp(afile, bfile) + cfile = joinpath(dir, "c.txt") open(cfile, "w") do cf write(cf, "This is longer than the contents of afile") end -cp(afile, cfile) +cp(afile, cfile; remove_destination=true) a_stat = stat(afile) b_stat = stat(bfile) @@ -404,6 +437,238 @@ rm(afile) rm(bfile) rm(cfile) +# issue #10506 #10434 +## Tests for directories and links to directories +@non_windowsxp_only begin + function setup_dirs(tmpdir) + srcdir = joinpath(tmpdir, "src") + srcdir_cp = joinpath(tmpdir, "srcdir_cp") + mkdir(srcdir) + abs_dirlink = joinpath(tmpdir, "abs_dirlink") + symlink(abspath(srcdir), abs_dirlink) + cd(tmpdir) + rel_dirlink = "rel_dirlink" + symlink("src", rel_dirlink) + cd(pwd_) + + cfile = joinpath(srcdir, "c.txt") + open(cfile, "w") do cf + write(cf, "This is c.txt with unicode - 这是一个文件") + end + + abs_dirlink_cp = joinpath(tmpdir, "abs_dirlink_cp") + path_rel_dirlink = joinpath(tmpdir, rel_dirlink) + path_rel_dirlink_cp = joinpath(tmpdir, "rel_dirlink_cp") + + test_src_paths = [srcdir, abs_dirlink, path_rel_dirlink] + test_cp_paths = [srcdir_cp, abs_dirlink_cp, path_rel_dirlink_cp] + return test_src_paths, test_cp_paths + end + + function cp_follow_symlinks_false_check(s, d; remove_destination=false) + cp(s, d; remove_destination=remove_destination, follow_symlinks=false) + @test isdir(s) == isdir(d) + @test islink(s) == islink(d) + islink(s) && @test readlink(s) == readlink(d) + islink(s) && @test isabspath(readlink(s)) == isabspath(readlink(d)) + # all should contain 1 file named "c.txt" + @test "c.txt" in readdir(d) + @test length(readdir(d)) == 1 + end + + ## Test require `remove_destination=true` (remove destination first) for existing + # directories and existing links to directories + mktempdir() do tmpdir + # Setup new copies for the test + test_src_paths, test_cp_paths = setup_dirs(tmpdir) + for (s, d) in zip(test_src_paths, test_cp_paths) + cp_follow_symlinks_false_check(s, d) + end + # Test require `remove_destination=true` + for s in test_src_paths + for d in test_cp_paths + @test_throws ArgumentError cp(s, d; remove_destination=false) + @test_throws ArgumentError cp(s, d; remove_destination=false, follow_symlinks=true) + end + end + # Test remove the existing path first and copy + for (s, d) in zip(test_src_paths, test_cp_paths) + cp_follow_symlinks_false_check(s, d; remove_destination=true) + end + # Test remove the existing path first and copy an empty dir + emptydir = joinpath(tmpdir, "emptydir") + mkdir(emptydir) + for d in test_cp_paths + cp(emptydir, d; remove_destination=true, follow_symlinks=false) + # Expect no link because a dir is copied (follow_symlinks=false does not effect this) + @test isdir(d) && !islink(d) + # none should contain any file + @test isempty(readdir(d)) + end + end + + # Test full: absolute and relative directory links + mktempdir() do tmpdir + maindir = joinpath(tmpdir, "mytestdir") + mkdir(maindir) + targetdir = abspath(joinpath(maindir, "targetdir")) + mkdir(targetdir) + subdir1 = joinpath(maindir, "subdir1") + mkdir(subdir1) + + cfile = abspath(joinpath(maindir, "c.txt")) + open(cfile, "w") do cf + write(cf, "This is c.txt - 这是一个文件") + end + open(abspath(joinpath(targetdir, "file1.txt")), "w") do cf + write(cf, "This is file1.txt - 这是一个文件") + end + + abs_dl = joinpath(maindir, "abs_linkto_targetdir") + symlink(targetdir, abs_dl) + # Setup relative links + cd(subdir1) + rel_dl = "rel_linkto_targetdir" + rel_dir = joinpath("..", "targetdir") + symlink(rel_dir, rel_dl) + cd(pwd_) + # TEST: Directory with links: Test each option + maindir_cp = joinpath(dirname(maindir),"maindir_cp") + maindir_cp_keepsym = joinpath(dirname(maindir),"maindir_cp_keepsym") + cp_and_test(maindir, maindir_cp, true) + cp_and_test(maindir, maindir_cp_keepsym, false) + end +end + +# issue #10506 #10434 +## Tests for files and links to files as well as directories and links to directories +@unix_only begin + function setup_files(tmpdir) + srcfile = joinpath(tmpdir, "srcfile.txt") + srcfile_cp = joinpath(tmpdir, "srcfile_cp.txt") + open(srcfile, "w") do f + write(f, "This is srcfile.txt with unicode - 这是一个文件") + end + srcfile_content = readall(srcfile) + abs_filelink = joinpath(tmpdir, "abs_filelink") + symlink(abspath(srcfile), abs_filelink) + cd(tmpdir) + rel_filelink = "rel_filelink" + symlink("srcfile.txt", rel_filelink) + cd(pwd_) + + abs_filelink_cp = joinpath(tmpdir, "abs_filelink_cp") + path_rel_filelink = joinpath(tmpdir, rel_filelink) + path_rel_filelink_cp = joinpath(tmpdir, "rel_filelink_cp") + + test_src_paths = [srcfile, abs_filelink, path_rel_filelink] + test_cp_paths = [srcfile_cp, abs_filelink_cp, path_rel_filelink_cp] + return test_src_paths, test_cp_paths, srcfile_content + end + + function cp_follow_symlinks_false_check(s, d, srcfile_content; remove_destination=false) + cp(s, d; remove_destination=remove_destination, follow_symlinks=false) + @test isfile(s) == isfile(d) + @test islink(s) == islink(d) + islink(s) && @test readlink(s) == readlink(d) + islink(s) && @test isabspath(readlink(s)) == isabspath(readlink(d)) + # all should contain the same + @test readall(s) == readall(d) == srcfile_content + end + + ## Test require `remove_destination=true` (remove destination first) for existing + # files and existing links to files + mktempdir() do tmpdir + # Setup new copies for the test + test_src_paths, test_cp_paths, srcfile_content = setup_files(tmpdir) + for (s, d) in zip(test_src_paths, test_cp_paths) + cp_follow_symlinks_false_check(s, d, srcfile_content) + end + # Test require `remove_destination=true` + for s in test_src_paths + for d in test_cp_paths + @test_throws ArgumentError cp(s, d; remove_destination=false) + @test_throws ArgumentError cp(s, d; remove_destination=false, follow_symlinks=true) + end + end + # Test remove the existing path first and copy: follow_symlinks=false + for (s, d) in zip(test_src_paths, test_cp_paths) + cp_follow_symlinks_false_check(s, d, srcfile_content; remove_destination=true) + end + # Test remove the existing path first and copy an other file + otherfile = joinpath(tmpdir, "otherfile.txt") + otherfile_content = "This is otherfile.txt with unicode - 这是一个文件" + open(otherfile, "w") do f + write(f, otherfile_content) + end + for d in test_cp_paths + cp(otherfile, d; remove_destination=true, follow_symlinks=false) + # Expect no link because a file is copied (follow_symlinks=false does not effect this) + @test isfile(d) && !islink(d) + # all should contain otherfile_content + @test readall(d) == otherfile_content + end + end + + # Test full: absolute and relative file links and absolute and relative directory links + mktempdir() do tmpdir + maindir = joinpath(tmpdir, "mytestdir") + mkdir(maindir) + targetdir = abspath(joinpath(maindir, "targetdir")) + mkdir(targetdir) + subdir1 = joinpath(maindir, "subdir1") + mkdir(subdir1) + + cfile = abspath(joinpath(maindir, "c.txt")) + open(cfile, "w") do cf + write(cf, "This is c.txt - 这是一个文件") + end + open(abspath(joinpath(targetdir, "file1.txt")), "w") do cf + write(cf, "This is file1.txt - 这是一个文件") + end + + abs_fl = joinpath(maindir, "abs_linkto_c.txt") + symlink(cfile, abs_fl) + abs_dl = joinpath(maindir, "abs_linkto_targetdir") + symlink(targetdir, abs_dl) + # Setup relative links + cd(subdir1) + rel_fl = "rel_linkto_c.txt" + rel_file = joinpath("..", "c.txt") + symlink(rel_file, rel_fl) + rel_dl = "rel_linkto_targetdir" + rel_dir = joinpath("..", "targetdir") + symlink(rel_dir, rel_dl) + rel_file_read_txt = readall(rel_file) + cd(pwd_) + # Setup copytodir + copytodir = joinpath(tmpdir, "copytodir") + mkdir(copytodir) + cp(cfile, joinpath(copytodir, basename(cfile))) + subdir_test = joinpath(copytodir, "subdir_test") + mkdir(subdir_test) + cp(targetdir, joinpath(copytodir, basename(targetdir)); follow_symlinks=false) + # TEST: Directory with links: Test each option + maindir_cp = joinpath(dirname(maindir),"maindir_cp") + maindir_cp_keepsym = joinpath(dirname(maindir),"maindir_cp_keepsym") + cp_and_test(maindir, maindir_cp, true) + cp_and_test(maindir, maindir_cp_keepsym, false) + + ## Tests single Files, File Links + rel_flpath = joinpath(subdir1, rel_fl) + # `cp file` + cp_and_test(cfile, joinpath(copytodir,"cfile_cp.txt"), true) + cp_and_test(cfile, joinpath(copytodir,"cfile_cp_keepsym.txt"), false) + # `cp absolute file link` + cp_and_test(abs_fl, joinpath(copytodir,"abs_fl_cp.txt"), true) + cp_and_test(abs_fl, joinpath(copytodir,"abs_fl_cp_keepsym.txt"), false) + # `cp relative file link` + cp_and_test(rel_flpath, joinpath(subdir_test,"rel_fl_cp.txt"), true) + cp_and_test(rel_flpath, joinpath(subdir_test,"rel_fl_cp_keepsym.txt"), false) + end +end + + ################### # FILE* interface # ###################