Process.currentDirectoryURL strange behaviour

Hi everyone,

Today have been playing with Process from Foundation and I noticed a strange behaviour when changing the currentDirectoryURL value.

I created a small playground that reproduces this issue

import Foundation

FileManager.default.changeCurrentDirectoryPath("/Users")

let directoryURL = URL(fileURLWithPath: "../usr")
let process = Process()
process.currentDirectoryURL = directoryURL

print(directoryURL.path)
print(process.currentDirectoryURL?.path)

The output:

/usr
Optional("/Users/usr")

I don't really understand why process.currentDirectoryURL?.path is /Users/usr, it's like the ../ from ../usr was ignored.

Am I missing something ?

Thx

Are you running this code from macOS? Just tried the example on Linux with Swift 5.2.4 and the output is:

/usr
Optional("/usr")

edit: Ok, just tried on my mac, the output is yours. That's unusual...

Weird. It seems to always turn ".." into "/Users"

 29> process.currentDirectoryURL = URL(fileURLWithPath: "/usr/local/") 
 30> print(process.currentDirectoryURL?.path) 
Optional("/usr/local")
 31> process.currentDirectoryURL = URL(fileURLWithPath: "../bin") 
 32> print(process.currentDirectoryURL?.path) 
Optional("/Users/bin")

This is with Xcode 11.5 on Catalina.
Apple Swift version 5.2.4 (swiftlang-1103.0.32.9 clang-1103.0.32.530

1 Like

@ValentinDebon I am running the same configuration as @tim1724

So could it be a problem in macOS ?

Here’s what’s going on…

The currentDirectoryURL property is a relatively recent addition. Historically, NSTask (and hence Process) only supported currentDirectoryPath.

The new currentDirectoryURL property is a wrapper around the old currentDirectoryPath property. Internally the setter converts the URL to a path and the getter does the reverse.

The conversion process in the setter uses url.standardizedURL.path.

The standardizedURL property is documented to return A copy of the URL with any instances of ".." or "." removed from its path. The tricky part here is that it looks just at the URL’s path. If the URL is relative, it ignores the path in the base URL.

In your case the URL is relative:

print(directoryURL)
// prints: ../usr/ -- file:///Users/

and thus standardized returns unhelpful results:

print(directoryURL.standardized)
usr/ -- file:///Users/

You can avoid the problem with a judicious application of absoluteURL.

`process.currentDirectoryURL = directoryURL.absoluteURL

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

Thx Eskimo for the answer but there is still something I don't understand.

In my example, I print directoryURL.path which gives /usr and given the source code of currentDirectoryURL:

private var _currentDirectoryPath = FileManager.default.currentDirectoryPath
open var currentDirectoryURL: URL? {
    get { _currentDirectoryPath == "" ? nil : URL(fileURLWithPath: _currentDirectoryPath, isDirectory: true) }
    set {
        // Setting currentDirectoryURL to nil resets to the current directory
        if let url = newValue {
            guard url.isFileURL else { fatalError("non-file URL argument") }
            _currentDirectoryPath = url.path
        } else {
            _currentDirectoryPath = FileManager.default.currentDirectoryPath
        }
    }
}

@available(*, deprecated, renamed: "currentDirectoryURL")
open var currentDirectoryPath: String {
    get { _currentDirectoryPath }
    set { _currentDirectoryPath = newValue }
}

The setter uses url.path too, which is /usr in my example. So why it's not like that when we get the URL ?

Also it does not explain why the result is different between macOS and Linux.

1 Like

I admit the behavior mismatch between macOS and Linux is what bothers me the most. Especially knowing the underlying code is pure Swift. There shouldn't be any problem/

Hey @eskimo,

thx for the good explanation. But as @IceIceIce explains there is a strange behavior. When I set a URL that is relative it should return the same url when I get it back.

I maybe found a solution to be fixed in Process.swift. Right there the path property of URL is used which doesn't work when the path was a relative one, as you described very well.

What if we use the relativePath property here since it preserves the format of the path (../usr/):

var url = URL(fileURLWithPath: "/Users")
url = URL(fileURLWithPath: "../usr", relativeTo: url)
print(url.relativePath)
// prints: ../usr

... and for non-relative paths it prints the correct path too:

let url = URL(fileURLWithPath: "/Users/usr")
print(url.relativePath)
// prints: /Users/usr

So it seems to work in both cases. Wdyt?

@blackjacx I think there must be a reason why the Process class uses absolute paths. But still, this doesn't explain why the path is ill-formed in both cases.
I just did a test on both macOS and Linux, using Swift 5.2.4:
In macOS, executing the REPL in /Users:

Welcome to Apple Swift version 5.2.4 (swiftlang-1103.0.32.9 clang-1103.0.32.53).
Type :help for assistance.
  1> import Foundation
  2> let url = URL(fileURLWithPath: "../bin")
url: URL = {}
  3> print(url)
../bin/ -- file:///Users/
  4> print(url.standardized.path)
/Users/bin

And Linux, executing in /home:

 1> import Foundation
 2> let url = URL(fileURLWithPath: "../bin")
[...]
 3> print(url)
../usr/bin/ -- file:///home/
 4> print(url.standardized.path)
/usr/bin

Which is confusing, why the hell is there a /usr added in my Linux code, why is the behavior inconsistent?

The difference you're seeing between macOS and Linux is due to the fact that Foundation on Darwin platforms is not the same as swift-corelibs-foundation, which is the pure Swift reimplementation of Foundation for non-Darwin platforms. (i.e., swift-corelibs-foundation's Process.currentDirectoryURL is not the code you're calling on macOS)

The goal of swift-corelibs-foundation is to match the behavior of Darwin's Foundation as closely as possible, but inconsistencies like this sometimes slip through; I think this is worthy of a bug report so either the native currentDirectoryURL behavior is reconsidered, or swift-corelibs-foundation behavior is changed to match.

1 Like

@itaiferber I agree about the bug report. Diving a little further into the code base shows the Process class is unrelated to the problem. We can trace it back to NSURL, giving an inconsistent behavior for the following:

import Foundation
let nsurl = NSURL(fileURLWithPath: "../bin")
print(nsurl)
print(nsurl.standardized?.path)

Giving different outputs on both macOS and Linux anywhere "../bin" references a valid location

That's true. NSURL parsing is extremely complex, and unlikely to change for myriad backwards-compatibility reasons. It may be easier to account for this within Process itself (in some cases) to provide better defaults.

1 Like