FilePath APIs are too safe

sorry for the rant, but i was recently writing some python scripts, and had to fill in the swift-side parts, and i’m becoming aware of just how much less productive i am using SystemPackage’s APIs, compared to os.path.

put simply, FilePath is just too safe. the amount of boilerplate needed for the simplest operations just feels excessive. it’s like String indexing, but taken to an extreme.

to walk through an example of a simple task that’s way over-complicated by FilePath, i had a text file with a list of directories, one on each line:

directories.txt
foo 
bar 
baz

i read the file contents, and split on newlines, to get an array of [Substring]:

func loadDirectoryList(from path:FilePath) throws -> [Substring]
{
    try self.read(from: path).split(whereSeparator: \.isWhitespace)
}

next, i try to iterate over the list of directories, constructing a relative path with each directory as the last path component:

for directory:Substring in loadDirectoryList(
    from: .init(root: nil, components: "workspace", "directories.txt"))
{
    let directory:FilePath = 
        .init(root: nil, components: "workspace", .init(String.init(directory)))
}

first, why do i have to write root: nil to get a relative path? why can’t there just be a static constructor like .relative("workspace", "directories.txt")?

second, why do i have to copy directory:Substring to a String in order to construct a FilePath.Component? why can’t it just take some StringProtocol like everything else?

finally, it doesn’t even compile, because FilePath.Component.init(_:) is failable:

error: value of optional type 'FilePath.Component?' must be unwrapped 
to a value of type 'FilePath.Component'

at the same time, FilePath.Component is ExpressibleByStringLiteral, which strikes me as inconsistent. if i want to refactor say the "workspace" directory into something configurable, i can’t just drop in a .init(workspaceName), since that would become optional.

pretty much all of this feels completely pointless to me. the text file will never contain empty lines or '/'characters, and nobody besides SystemPackage accepts FilePath anyway, they would all inevitably be converted back into String. for example, all of SwiftNIO’s file-based APIs take String, not FilePath.

2 Likes

Our intention is to begin to accept FilePath in the near future.

2 Likes

I would refactor that example code into something dry:

let base = FilePath("workspace")
let source = base.appending("directories.txt")

then when reading dir names from file, you'd be appending each line from file to the base path instead of reconstructing the whole path from scratch:

let strings = try loadDirectoryList(from: source)
for str in strings {
  let directory = base.appending(String(str))
}

This does not eliminate the need of turning a borrowed Substring into owned String though.

1 Like

why is it we can append a raw String to a FilePath non-optionally, but we can only append a FilePath.Component optionally?

Is that true? I can do this:

import System
var p = FilePath("/var")
let f = FilePath.Component("run")
p.append(f)
print(p) // /var/run

This is swift 5.6.1