Skip to content

Delightful, robust, cross-platform and chainable file-pathing functions.

License

Notifications You must be signed in to change notification settings

jhheider/Path.swift

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Path.swift badge-platforms badge-languages badge-ci badge-jazzy badge-codecov badge-version

A file-system pathing library focused on developer experience and robust end results.

import Path

// convenient static members
let home = Path.home

// pleasant joining syntax
let docs = Path.home/"Documents"

// paths are *always* absolute thus avoiding common bugs
let path = Path(userInput) ?? Path.cwd/userInput

// elegant, chainable syntax
try Path.home.join("foo").mkdir().join("bar").touch().chmod(0o555)

// sensible considerations
try Path.home.join("bar").mkdir()
try Path.home.join("bar").mkdir()  // doesn’t throw ∵ we already have the desired result

// easy file-management
let bar = try Path.root.join("foo").copy(to: Path.root/"bar")
print(bar)         // => /bar
print(bar.isFile)  // => true

// careful API considerations so as to avoid common bugs
let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir())
print(foo)         // => /bar/foo
print(foo.isFile)  // => true

// we support dynamic-member-syntax when joining named static members, eg:
let prefs = Path.home.Library.Preferences  // => /Users/mxcl/Library/Preferences

// a practical example: installing a helper executable
try Bundle.resources.helper.copy(into: Path.root.usr.local.bin).chmod(0o500)

We emphasize safety and correctness, just like Swift, and also (again like Swift), we provide a thoughtful and comprehensive (yet concise) API.

Support mxcl

Hi, I’m Max Howell and I have written a lot of open source software, and probably you already use some of it (Homebrew anyone?). I work full-time on open source and it’s hard; currently I earn less than minimum wage. Please help me continue my work, I appreciate it x

Other donation/tipping options

Handbook

Our online API documentation covers 100% of our public API and is automatically updated for new releases.

Codable

We support Codable as you would expect:

try JSONEncoder().encode([Path.home, Path.home/"foo"])
[
    "/Users/mxcl",
    "/Users/mxcl/foo",
]

However, often you want to encode relative paths:

let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = Path.home
encoder.encode([Path.home, Path.home/"foo"])
[
    "",
    "foo",
]

Note make sure you decode with this key set also, otherwise we fatal (unless the paths are absolute obv.)

let decoder = JSONDecoder()
decoder.userInfo[.relativePath] = Path.home
decoder.decode(from: data)

Dynamic members

We support @dynamicMemberLookup:

let ls = Path.root.usr.bin.ls  // => /usr/bin/ls

We only provide this for “starting” function, eg. Path.home or Bundle.path. This is because we found in practice it was easy to write incorrect code, since everything would compile if we allowed arbituary variables to take any named property as valid syntax. What we have is what you want most of the time but much less dangerous.

Initializing from user-input

The Path initializer returns nil unless fed an absolute path; thus to initialize from user-input that may contain a relative path use this form:

let path = Path(userInput) ?? Path.cwd/userInput

This is explicit, not hiding anything that code-review may miss and preventing common bugs like accidentally creating Path objects from strings you did not expect to be relative.

Our initializer is nameless to be consistent with the equivalent operation for converting strings to Int, Float etc. in the standard library.

Extensions

We have some extensions to Apple APIs:

let bashProfile = try String(contentsOf: Path.home/".bash_profile")
let history = try Data(contentsOf: Path.home/".history")

bashProfile += "\n\nfoo"

try bashProfile.write(to: Path.home/".bash_profile")

try Bundle.main.resources.join("foo").copy(to: .home)

Directory listings

We provide ls(), called because it behaves like the Terminal ls function, the name thus implies its behavior, ie. that it is not recursive and doesn’t list hidden files.

for path in Path.home.ls() {
    //…
}

for path in Path.home.ls() where path.isFile {
    //…
}

for path in Path.home.ls() where path.mtime > yesterday {
    //…
}

let dirs = Path.home.ls().directories
// ^^ directories that *exist*

let files = Path.home.ls().files
// ^^ files that both *exist* and are *not* directories

let swiftFiles = Path.home.ls().files.filter{ $0.extension == "swift" }

We provide find() for recursive listing:

Path.home.find().execute { path in
    //…
}

Which is configurable:

Path.home.find().maxDepth(1).extension("swift").kind(.file) { path in
    //…
}

And can be controlled:

Path.home.find().execute { path in
    guard foo else { return .skip }
    guard bar else { return .abort }
    return .continue
}

Or just get all paths at once:

let paths = Path.home.find().execute()

Path.swift is robust

Some parts of FileManager are not exactly idiomatic. For example isExecutableFile returns true even if there is no file there, it is instead telling you that if you made a file there it could be executable. Thus we check the POSIX permissions of the file first, before returning the result of isExecutableFile. Path.swift has done the leg-work for you so you can get on with your work without worries.

There is also some magic going on in Foundation’s filesystem APIs, which we look for and ensure our API is deterministic, eg. this test.

Path.swift is properly cross-platform

FileManager on Linux is full of holes. We have found the holes and worked round them where necessary.

Rules & Caveats

Paths are just string representations, there might not be a real file there.

Path.home/"b"      // => /Users/mxcl/b

// joining multiple strings works as you’d expect
Path.home/"b"/"c"  // => /Users/mxcl/b/c

// joining multiple parts at a time is fine
Path.home/"b/c"    // => /Users/mxcl/b/c

// joining with absolute paths omits prefixed slash
Path.home/"/b"     // => /Users/mxcl/b

// joining with .. or . works as expected
Path.home.foo.bar.join("..")  // => /Users/mxcl/foo
Path.home.foo.bar.join(".")   // => /Users/mxcl/foo/bar

// of course, feel free to join variables:
let b = "b"
let c = "c"
Path.home/b/c      // => /Users/mxcl/b/c

// tilde is not special here
Path.root/"~b"     // => /~b
Path.root/"~/b"    // => /~/b

// but is here
Path("~/foo")!     // => /Users/mxcl/foo

// this works provided the user `Guest` exists
Path("~Guest")     // => /Users/Guest

// but if the user does not exist
Path("~foo")       // => nil

// paths with .. or . are resolved
Path("/foo/bar/../baz")  // => /foo/baz

// symlinks are not resolved
Path.root.bar.symlink(as: "foo")
Path("foo")        // => /foo
Path.foo           // => /foo

// unless you do it explicitly
try Path.foo.readlink()  // => /bar
                         // `readlink` only resolves the *final* path component,
                         // thus use `realpath` if there are multiple symlinks

Path.swift has the general policy that if the desired end result preexists, then it’s a noop:

  • If you try to delete a file, but the file doesn't exist, we do nothing.
  • If you try to make a directory and it already exists, we do nothing.
  • If you call readlink on a non-symlink, we return self

However notably if you try to copy or move a file with specifying overwrite and the file already exists at the destination and is identical, we don’t check for that as the check was deemed too expensive to be worthwhile.

Symbolic links

  • Two paths may represent the same resolved path yet not be equal due to symlinks in such cases you should use realpath on both first if an equality check is required.
  • There are several symlink paths on Mac that are typically automatically resolved by Foundation, eg. /private, we attempt to do the same for functions that you would expect it (notably realpath), we do the same for Path.init, but do not if you are joining a path that ends up being one of these paths, (eg. Path.root.join("var/private')).

If a Path is a symlink but the destination of the link does not exist exists returns false. This seems to be the correct thing to do since symlinks are meant to be an abstraction for filesystems. To instead verify that there is no filesystem entry there at all check if kind is nil.

We do not provide change directory functionality

Changing directory is dangerous, you should always try to avoid it and thus we don’t even provide the method. If you are executing a sub-process then use Process.currentDirectoryURL.

If you must then use FileManager.changeCurrentDirectory.

I thought I should only use URLs?

Apple recommend this because they provide a magic translation for file-references embodied by URLs, which gives you URLs like so:

file:///.file/id=6571367.15106761

Therefore, if you are not using this feature you are fine. If you have URLs the correct way to get a Path is:

if let path = Path(url: url) {
    /*…*/
}

Our initializer calls path on the URL which resolves any reference to an actual filesystem path, however we also check the URL has a file scheme first.

Installation

SwiftPM:

package.append(.package(url: "https://github.com/mxcl/Path.swift.git", from: "0.13.0"))

CocoaPods:

pod 'Path.swift', '~> 0.13'

Carthage:

Waiting on: @Carthage#1945.

Pre‐1.0 status

We are pre 1.0, thus we can change the API as we like, and we will (to the pursuit of getting it right)! We will tag 1.0 as soon as possible.

Get push notifications for new releases

https://mxcl.dev/canopy/

Alternatives

About

Delightful, robust, cross-platform and chainable file-pathing functions.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Swift 100.0%