From a68ae0098fc74d321e7c96a385e133fce4427473 Mon Sep 17 00:00:00 2001 From: Mus Date: Sat, 5 Oct 2019 14:58:29 -0400 Subject: [PATCH 1/4] Fix joinpath on Windows and improve joinpath on linux --- LICENSE.md | 2 +- base/path.jl | 86 ++++++++++++++++++++++++++++++++++++++-------------- test/path.jl | 18 +++++++++++ 3 files changed, 82 insertions(+), 24 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 3d315105d038e..0d5d98311766c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -35,7 +35,7 @@ Julia includes code from the following projects, which have their own licenses: - [MUSL](https://git.musl-libc.org/cgit/musl/tree/COPYRIGHT) (for getopt implementation on Windows) [MIT] - [MINGW](https://sourceforge.net/p/mingw/mingw-org-wsl/ci/legacy/tree/mingwrt/mingwex/dirname.c) (for dirname implementation on Windows) [MIT] - [NetBSD](https://www.netbsd.org/about/redistribution.html) (for setjmp, longjmp, and strptime implementations on Windows) [BSD-3] -- [Python](https://docs.python.org/2/license.html) (for strtod implementation on Windows) [BSD-3, effectively] +- [Python](https://docs.python.org/2/license.html) (for strtod and joinpath implementation on Windows) [BSD-3, effectively] The following components included in Julia `Base` have their own separate licenses: diff --git a/base/path.jl b/base/path.jl index 234519818075a..8fcc25cb1e5b7 100644 --- a/base/path.jl +++ b/base/path.jl @@ -198,14 +198,6 @@ function splitext(path::String) a*m.captures[1], String(m.captures[2]) end -function pathsep(paths::AbstractString...) - for path in paths - m = match(path_separator_re, String(path)) - m !== nothing && return m.match[1:1] - end - return path_separator -end - """ splitpath(path::AbstractString) -> Vector{String} @@ -246,8 +238,69 @@ function splitpath(p::String) return out end +joinpath(path::AbstractString) = String(path) + +if Sys.iswindows() + +function joinpath(path::AbstractString, paths::AbstractString...) + result_drive, result_path = splitdrive(path) + + local p_drive, p_path + for p in paths + p_drive, p_path = splitdrive(p) + + if startswith(p_path, ('\\', '/')) + # second path is absolute + if !isempty(p_drive) || !isempty(result_drive) + result_drive = p_drive + end + result_path = p_path + continue + elseif !isempty(p_drive) && p_drive != result_drive + if lowercase(p_drive) != lowercase(result_drive) + # different drives, ignore the first path entirely + result_drive = p_drive + result_path = p_path + continue + end + result_drive = p_drive # same drive in different case + end + + # second path is relative to the first + if !isempty(result_path) && result_path[end] ∉ ('\\', '/') + result_path *= "\\" + end + + result_path = result_path * p_path + end + + # add separator between UNC and non-absolute path + if !isempty(p_path) && result_path[1] ∉ ('\\', '/') && !isempty(result_drive) && result_drive[end] != ':' + return result_drive * "\\" * result_path + end + + return result_drive * result_path +end + +else + +function joinpath(path::AbstractString, paths::AbstractString...) + for p in paths + if isabspath(p) + path = p + elseif isempty(path) || path[end] == '/' + path *= p + else + path *= "/" * p + end + end + return path +end + +end # os-test + """ - joinpath(parts...) -> AbstractString + joinpath(parts::AbstractString...) -> String Join path components into a full path. If some argument is an absolute path or (on Windows) has a drive specification that doesn't match the drive computed for @@ -259,20 +312,7 @@ julia> joinpath("/home/myuser", "example.jl") "/home/myuser/example.jl" ``` """ -joinpath(a::AbstractString, b::AbstractString, c::AbstractString...) = joinpath(joinpath(a,b), c...) -joinpath(a::AbstractString) = a - -function joinpath(a::String, b::String) - isabspath(b) && return b - A, a = splitdrive(a) - B, b = splitdrive(b) - !isempty(B) && A != B && return string(B,b) - C = isempty(B) ? A : B - isempty(a) ? string(C,b) : - occursin(path_separator_re, a[end:end]) ? string(C,a,b) : - string(C,a,pathsep(a,b),b) -end -joinpath(a::AbstractString, b::AbstractString) = joinpath(String(a), String(b)) +joinpath """ normpath(path::AbstractString) -> AbstractString diff --git a/test/path.jl b/test/path.jl index b61535a4031a9..0ac50e926e918 100644 --- a/test/path.jl +++ b/test/path.jl @@ -31,8 +31,14 @@ @test isdirpath(S("..")) end @testset "joinpath" begin + @test joinpath(S("")) == "" @test joinpath(S("foo")) == "foo" @test joinpath(S("foo"), S("bar")) == "foo$(sep)bar" + @test joinpath(S("foo"), S("bar"), S("baz")) == "foo$(sep)bar$(sep)baz" + @test joinpath(S("foo"), S(""), S("baz")) == "foo$(sep)baz" + @test joinpath(S("foo"), S(""), S("")) == "foo$(sep)" + @test joinpath(S("foo"), S(""), S(""), S("bar")) == "foo$(sep)bar" + @test joinpath(S("foo"), S(homedir())) == homedir() @test joinpath(S(abspath("foo")), S(homedir())) == homedir() @@ -40,6 +46,18 @@ @test joinpath(S("foo"),S("bar:baz")) == "bar:baz" @test joinpath(S("C:"),S("foo"),S("D:"),S("bar")) == "D:bar" @test joinpath(S("C:"),S("foo"),S("D:bar"),S("baz")) == "D:bar$(sep)baz" + + # relative folders and case-insensitive drive letters + @test joinpath(S("C:\\a\\b"), S("c:c\\e")) == "c:\\a\\b\\c\\e" + + # UNC paths + @test joinpath(S("\\\\server"), S("share")) == "\\\\server\\share" + @test joinpath(S("\\\\server"), S("share"), S("a")) == "\\\\server\\share\\a" + @test joinpath(S("\\\\server\\"), S("share"), S("a")) == "\\\\server\\share\\a" + @test joinpath(S("\\\\server"), S("share"), S("a"), S("b")) == "\\\\server\\share\\a\\b" + @test joinpath(S("\\\\server\\share"),S("a")) == "\\\\server\\share\\a" + @test joinpath(S("\\\\server\\share\\"), S("a")) == "\\\\server\\share\\a" + elseif Sys.isunix() @test joinpath(S("foo"),S("bar:baz")) == "foo$(sep)bar:baz" @test joinpath(S("C:"),S("foo"),S("D:"),S("bar")) == "C:$(sep)foo$(sep)D:$(sep)bar" From 899bc753463200af61174066b6ab3c780e1c6883 Mon Sep 17 00:00:00 2001 From: Mus Date: Thu, 10 Oct 2019 15:49:07 -0400 Subject: [PATCH 2/4] Force early conversion to String for performance --- base/path.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/path.jl b/base/path.jl index 8fcc25cb1e5b7..07df7c114a68d 100644 --- a/base/path.jl +++ b/base/path.jl @@ -238,11 +238,11 @@ function splitpath(p::String) return out end -joinpath(path::AbstractString) = String(path) +joinpath(path::String) = String(path) if Sys.iswindows() -function joinpath(path::AbstractString, paths::AbstractString...) +function joinpath(path::String, paths::String...) result_drive, result_path = splitdrive(path) local p_drive, p_path @@ -284,7 +284,7 @@ end else -function joinpath(path::AbstractString, paths::AbstractString...) +function joinpath(path::String, paths::String...) for p in paths if isabspath(p) path = p @@ -312,7 +312,7 @@ julia> joinpath("/home/myuser", "example.jl") "/home/myuser/example.jl" ``` """ -joinpath +joinpath(parts::AbstractString...) = joinpath(String.(parts)...) """ normpath(path::AbstractString) -> AbstractString From 91aa8b85fdcb4e3d577190c18219a80ca9b2f6df Mon Sep 17 00:00:00 2001 From: Mus Date: Thu, 17 Oct 2019 13:41:21 -0400 Subject: [PATCH 3/4] Don't eagrly convert --- base/path.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/path.jl b/base/path.jl index 07df7c114a68d..e2a61da1bb6e6 100644 --- a/base/path.jl +++ b/base/path.jl @@ -238,11 +238,11 @@ function splitpath(p::String) return out end -joinpath(path::String) = String(path) +joinpath(path::AbstractString)::String = path if Sys.iswindows() -function joinpath(path::String, paths::String...) +function joinpath(path::AbstractString, paths::AbstractString...)::String result_drive, result_path = splitdrive(path) local p_drive, p_path @@ -284,7 +284,7 @@ end else -function joinpath(path::String, paths::String...) +function joinpath(path::AbstractString, paths::AbstractString...)::String for p in paths if isabspath(p) path = p @@ -312,7 +312,7 @@ julia> joinpath("/home/myuser", "example.jl") "/home/myuser/example.jl" ``` """ -joinpath(parts::AbstractString...) = joinpath(String.(parts)...) +joinpath """ normpath(path::AbstractString) -> AbstractString From 0bc6b77ab0fd3b629508aa0a523f9e0830a3ccb7 Mon Sep 17 00:00:00 2001 From: Mus Date: Fri, 18 Oct 2019 16:44:14 -0400 Subject: [PATCH 4/4] update behavior and and docstring --- base/path.jl | 6 +++++- test/path.jl | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/base/path.jl b/base/path.jl index e2a61da1bb6e6..c34172abddb4b 100644 --- a/base/path.jl +++ b/base/path.jl @@ -263,7 +263,6 @@ function joinpath(path::AbstractString, paths::AbstractString...)::String result_path = p_path continue end - result_drive = p_drive # same drive in different case end # second path is relative to the first @@ -306,6 +305,11 @@ Join path components into a full path. If some argument is an absolute path or (on Windows) has a drive specification that doesn't match the drive computed for the join of the preceding paths, then prior components are dropped. +Note on Windows since there is a current directory for each drive, `joinpath("c:", "foo")` +represents a path relative to the current directory on drive "c:" so this is equal to "c:foo", +not "c:\\foo". Furthermore, `joinpath` treats this as a non-absolute path and ignores the drive +letter casing, hence `joinpath("C:\\A","c:b") = "C:\\A\\b"`. + # Examples ```jldoctest julia> joinpath("/home/myuser", "example.jl") diff --git a/test/path.jl b/test/path.jl index 0ac50e926e918..e09a46ef9370e 100644 --- a/test/path.jl +++ b/test/path.jl @@ -48,7 +48,7 @@ @test joinpath(S("C:"),S("foo"),S("D:bar"),S("baz")) == "D:bar$(sep)baz" # relative folders and case-insensitive drive letters - @test joinpath(S("C:\\a\\b"), S("c:c\\e")) == "c:\\a\\b\\c\\e" + @test joinpath(S("C:\\a\\b"), S("c:c\\e")) == "C:\\a\\b\\c\\e" # UNC paths @test joinpath(S("\\\\server"), S("share")) == "\\\\server\\share"