In a process running on Windows, each drive has a current directory, and a path like X:
or X:a
, beginning with a drive letter not followed immediately by a \
or /
, specifies a location relative to the current directory on that drive.
This raises the question of how to define the operation of joining such a path "under" an absolute path. For other paths, this is mostly straightforward:
- Taking an absolute path like
X:\a
and joining a fully relative path likeb\c
givesX:\a\b\c
. - Taking an absolute path like
X:\a
and joining an absolute path likeY:\b
gives the absolute pathY:\b
, because if you were located atX:\a
, this would not affect the meaning of another absolute path. - Taking an absolute path like
X:\a
and joining a path like\b\c
that is relative to the current drive givesX:\b\c
, because if you are located anywhere under theX:
drive, then\b\c
refers toX:\b\c
.
But there are other cases. One of them is taking an absolute path like X:\a
and joining another such path to it. This really divides into two sub-cases:
- The other path may be on the same drive, such as taking
X:\a
and joiningX:b
. If we follow the rule of trying to form an absolute path to the locationX:b
as it would be resolved when at the locationX:\a
, then we should formX:\a\b
. Some path joining implementations follow this rule, while others do not and instead simply returnX:b
. - The other path may be on a different drive, such as taking
X:\a
and joiningY:b
. If we were to follow the rule of trying to form an absolute path to the locationY:b
as it would be resolved when at the locationX:\a
, then the result would depend on the current directory onY:
, which would have to be obtained dynamically from the system (and which could fail, if there is currently noY:
drive). But this behavior would also be extremely weird, sinceY:
itself is such a path: takingX:
and joiningY:
should not give a path likeY:\aa\bb
, even if that is the current directory onY:
. Path joining implementations do not follow this rule.
This results in two potentially unintuitive effects:
- Different path joining implementations treat problems like taking
X:\a
and joiningX:b
differently. Especially to users coming from a Unix background. - Even implementations that produce an absolute path in that case will still produce a relative path in others such as taking
X:\a
and joiningY:b
.
In particular, it may be intuitive that taking an absolute path and joining a relative path "under" it sometimes produces a relative path.
This repository contains a Rust program whose code is in src/main.rs
, and a Python program whose code is at main.py
, that print some information about the results on Windows of taking the absolute path C:\foo
and joining C:bar
or D:bar
to it.
For the results below:
- All the examples below were run on Windows 10 (10.0.19045), though the Windows version probably does not matter, especially since the filesystem is not actually accessed to do the joining.
- The Rust code was built with Rust 1.80.1.
- The Python code was run in interpreters up from 3.8 up to 3.13 RC 1.
- Note how, in some versions of Python, the behavior differs between
Path
(whichoop
uses) andos.path.join
(whichclassic
uses).
C:\Users\ek\source\repos\pathjoin [main]> cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `target\debug\pathjoin.exe`
left="C:\\", right="C:foo"
joined = "C:foo"
joined.is_absolute() = false
joined.is_relative() = true
left="C:\\", right="D:foo"
joined = "D:foo"
joined.is_absolute() = false
joined.is_relative() = true
C:\Users\ek\source\repos\pathjoin [main]> python3.8 main.py
Python 3.8.10
left='C:\\', right='C:foo'
oop = WindowsPath('C:foo')
oop.is_absolute() = False
isabs(oop) = False
classic = 'C:\\foo'
isabs(classic) = True
left='C:\\', right='D:foo'
oop = WindowsPath('D:foo')
oop.is_absolute() = False
isabs(oop) = False
classic = 'D:foo'
isabs(classic) = False
C:\Users\ek\source\repos\pathjoin [main]> python3.9 main.py
Python 3.9.13
left='C:\\', right='C:foo'
oop = WindowsPath('C:foo')
oop.is_absolute() = False
isabs(oop) = False
classic = 'C:\\foo'
isabs(classic) = True
left='C:\\', right='D:foo'
oop = WindowsPath('D:foo')
oop.is_absolute() = False
isabs(oop) = False
classic = 'D:foo'
isabs(classic) = False
C:\Users\ek\source\repos\pathjoin [main]> python3.10 main.py
Python 3.10.11
left='C:\\', right='C:foo'
oop = WindowsPath('C:foo')
oop.is_absolute() = False
isabs(oop) = False
classic = 'C:\\foo'
isabs(classic) = True
left='C:\\', right='D:foo'
oop = WindowsPath('D:foo')
oop.is_absolute() = False
isabs(oop) = False
classic = 'D:foo'
isabs(classic) = False
C:\Users\ek\source\repos\pathjoin [main]> python3.11 main.py
Python 3.11.9
left='C:\\', right='C:foo'
oop = WindowsPath('C:foo')
oop.is_absolute() = False
isabs(oop) = False
classic = 'C:\\foo'
isabs(classic) = True
left='C:\\', right='D:foo'
oop = WindowsPath('D:foo')
oop.is_absolute() = False
isabs(oop) = False
classic = 'D:foo'
isabs(classic) = False
C:\Users\ek\source\repos\pathjoin [main]> python3.12 main.py
Python 3.12.5
left='C:\\', right='C:foo'
oop = WindowsPath('C:/foo')
oop.is_absolute() = True
isabs(oop) = True
classic = 'C:\\foo'
isabs(classic) = True
left='C:\\', right='D:foo'
oop = WindowsPath('D:foo')
oop.is_absolute() = False
isabs(oop) = False
classic = 'D:foo'
isabs(classic) = False
C:\Users\ek\source\repos\pathjoin [main]> python3.13 main.py
Python 3.13.0rc1
left='C:\\', right='C:foo'
oop = WindowsPath('C:/foo')
oop.is_absolute() = True
isabs(oop) = True
classic = 'C:\\foo'
isabs(classic) = True
left='C:\\', right='D:foo'
oop = WindowsPath('D:foo')
oop.is_absolute() = False
isabs(oop) = False
classic = 'D:foo'
isabs(classic) = False
Everything in this repository is licensed under 0BSD.