Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

License

NotificationsYou must be signed in to change notification settings

mxcl/Path.swift

Repository files navigation

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

import Path// convenient static memberslethome=Path.home// pleasant joining syntaxletdocs=Path.home/"Documents"// paths are *always* absolute thus avoiding common bugsletpath=Path(userInput)??Path.cwd/userInput// elegant, chainable syntaxtryPath.home.join("foo").mkdir().join("bar").touch().chmod(0o555)// sensible considerationstryPath.home.join("bar").mkdir()tryPath.home.join("bar").mkdir()  // doesn’t throw ∵ we already have the desired result// easy file-managementletbar=tryPath.root.join("foo").copy(to:Path.root/"bar")print(bar)         // => /barprint(bar.isFile)  // => true// careful API considerations so as to avoid common bugsletfoo=tryPath.root.join("foo").copy(into:Path.root.join("bar").mkdir())print(foo)         // => /bar/fooprint(foo.isFile)  // => true// ^^ the `into:` version will only copy *into* a directory, the `to:` version copies// to a file at that path, thus you will not accidentally copy into directories you// may not have realized existed.// we support dynamic-member-syntax when joining named static members, eg:letprefs=Path.home.Library.Preferences  // => /Users/mxcl/Library/Preferences// a practical example: installing a helper executabletryBundle.resources.helper.copy(into:Path.root.usr.local.bin).chmod(0o500)

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

Sponsor @mxcl

Hi, I’m Max Howell and I have written a lot of open source software—generallya good deal of my free time 👨🏻‍💻. Sponsorship helps me justify creating new opensource and maintaining it. Thank you.

Sponsor @mxcl.

Handbook

Ouronline API documentation covers 100% of our public API and isautomatically updated for new releases.

Codable

We supportCodable as you would expect:

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

Though we recommend encodingrelative paths‡:

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

Note if you encode with this key set youmust decode with the keyset also:

letdecoder=JSONDecoder()decoder.userInfo[.relativePath]=Path.hometry decoder.decode(from: data)  // would throw if `.relativePath` not set

‡ If you are saving files to a system provided location, eg. Documents thenthe directory could change at Apple’s choice, or if say the user changes theirusername. Using relative paths also provides you with the flexibility infuture to change where you are storing your files without hassle.

Dynamic members

We support@dynamicMemberLookup:

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

We only provide this for “starting” function, eg.Path.home orBundle.path.This is because we found in practice it was easy to write incorrect code, sinceeverything would compile if we allowed arbituary variables to takeany namedproperty as valid syntax. What we have is what you want most of the time butmuch less (potentially) dangerous (at runtime).

Pathish

Path, andDynamicPath (the result of eg.Path.root) both conform toPathish which is a protocol that contains all pathing functions. Thus ifyou create objects from a mixture of both you need to create genericfunctions or convert anyDynamicPaths toPath first:

letpath1=Path("/usr/lib")!letpath2=Path.root.usr.binvarpaths=[Path]()paths.append(path1)        // finepaths.append(path2)        // errorpaths.append(Path(path2))  // ok

This is inconvenient but as Swift stands there’s nothing we can think ofthat would help.

Initializing from user-input

ThePath initializer returnsnil unless fed an absolute path; thus toinitialize from user-input that may contain a relative path use this form:

letpath=Path(userInput)??Path.cwd/userInput

This is explicit, not hiding anything that code-review may miss and preventingcommon bugs like accidentally creatingPath objects from strings you did notexpect to be relative.

Our initializer is nameless to be consistent with the equivalent operation forconverting strings toInt,Float etc. in the standard library.

Initializing from known strings

There’s no need to use the optional initializer in general if you have knownstrings that you need to be paths:

letabsolutePath="/known/path"letpath1=Path.root/absolutePathletpathWithoutInitialSlash="known/path"letpath2=Path.root/pathWithoutInitialSlashassert(path1== path2)letpath3=Path(absolutePath)!  // at your optionsassert(path2== path3)// be cautious:letpath4=Path(pathWithoutInitialSlash)!  // CRASH!

Extensions

We have some extensions to Apple APIs:

letbashProfile=tryString(contentsOf:Path.home/".bash_profile")lethistory=tryData(contentsOf:Path.home/".history")bashProfile+="\n\nfoo"try bashProfile.write(to:Path.home/".bash_profile")tryBundle.main.resources.join("foo").copy(to:.home)

Directory listings

We providels(), called because it behaves like the Terminalls function,the name thus implies its behavior, ie. that it is not recursive and doesn’tlist hidden files.

forpathinPath.home.ls(){    //…}forpathinPath.home.ls()where path.isFile{    //…}forpathinPath.home.ls()where path.mtime> yesterday{    //…}letdirs=Path.home.ls().directories// ^^ directories that *exist*letfiles=Path.home.ls().files// ^^ files that both *exist* and are *not* directoriesletswiftFiles=Path.home.ls().files.filter{ $0.extension=="swift"}letincludingHiddenFiles=Path.home.ls(.a)

Notels() does not throw, instead outputing a warning to the console if itfails to list the directory. The rationale for this is weak, please open aticket for discussion.

We providefind() for recursive listing:

forpathinPath.home.find(){    // descends all directories, and includes hidden files by default    // so it behaves the same as the terminal command `find`}

It is configurable:

forpathinPath.home.find().depth(max:1).extension("swift").type(.file).hidden(false){    //…}

It can be controlled with a closure syntax:

Path.home.find().depth(2...3).execute{ pathinguard path.basename()!="foo.lock"else{return.abort}if path.basename()==".build", path.isDirectory{return.skip}    //…return.continue}

Or get everything at once as an array:

letpaths=Path.home.find().map(\.self)

Path.swift is robust

Some parts ofFileManager are not exactly idiomatic. For exampleisExecutableFile returnstrue even if there is no file there, it is insteadtelling you thatif you made a file there itcould be executable. Thus wecheck the POSIX permissions of the file first, before returning the result ofisExecutableFile.Path.swift has done the leg-work for you so you can justget on with it and not have to worry.

There is also some magic going on in Foundation’s filesystem APIs, which we lookfor 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 workedround them where necessary.

Rules & Caveats

Paths are just (normalized) string representations, theremight not be a realfile there.

Path.home/"b"      // => /Users/mxcl/b// joining multiple strings works as you’d expectPath.home/"b"/"c"  // => /Users/mxcl/b/c// joining multiple parts simultaneously is finePath.home/"b/c"    // => /Users/mxcl/b/c// joining with absolute paths omits prefixed slashPath.home/"/b"     // => /Users/mxcl/b// joining with .. or . works as expectedPath.home.foo.bar.join("..")  // => /Users/mxcl/fooPath.home.foo.bar.join(".")   // => /Users/mxcl/foo/bar// though note that we provide `.parent`:Path.home.foo.bar.parent      // => /Users/mxcl/foo// of course, feel free to join variables:letb="b"letc="c"Path.home/b/c      // => /Users/mxcl/b/c// tilde is not special herePath.root/"~b"     // => /~bPath.root/"~/b"    // => /~/b// but is herePath("~/foo")!     // => /Users/mxcl/foo// this works provided the user `Guest` existsPath("~Guest")     // => /Users/Guest// but if the user does not existPath("~foo")       // => nil// paths with .. or . are resolvedPath("/foo/bar/../baz")  // => /foo/baz// symlinks are not resolvedPath.root.bar.symlink(as:"foo")Path("/foo")        // => /fooPath.root.foo       // => /foo// unless you do it explicitlytryPath.root.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 callreadlink on a non-symlink, we returnself

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

Symbolic links

  • Two paths may represent the sameresolved path yet not be equal due tosymlinks in such cases you should userealpath on both first if anequality check is required.
  • There are several symlink paths on Mac that are typically automaticallyresolved by Foundation, eg./private, we attempt to do the same forfunctions that you would expect it (notablyrealpath), wedo the sameforPath.init, butdo not if you are joining a path that ends up beingone of these paths, (eg.Path.root.join("var/private')).

If aPath is a symlink but the destination of the link does not existexistsreturnsfalse. This seems to be the correct thing to do since symlinks aremeant to be an abstraction for filesystems. To instead verify that there isno filesystem entry there at all check iftype isnil.

We do not provide change directory functionality

Changing directory is dangerous, you shouldalways try to avoid it and thuswe don’t even provide the method. If you are executing a sub-process thenuseProcess.currentDirectoryURL to changeits working directory when itexecutes.

If you must change directory then useFileManager.changeCurrentDirectory asearly in your process aspossible. Altering the global state of your app’senvironment is fundamentally dangerous creating hard to debug issues thatyou won‘t find for potentiallyyears.

I thought I should only useURLs?

Apple recommend this because they provide a magic translation forfile-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 thecorrect way to get aPath is:

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

Our initializer callspath on the URL which resolves any reference to anactual filesystem path, however we also check the URL has afile scheme first.

In defense of our naming scheme

Chainable syntax demands short method names, thus we adopted the naming schemeof the terminal, which is absolutely not very “Apple” when it comes to how theydesign their APIs, however for users of the terminal (whichsurely is mostdevelopers) it is snappy and familiar.

Installation

SwiftPM:

package.append(.package(url:"https://github.com/mxcl/Path.swift.git", from:"1.0.0"))package.targets.append(.target(name:"Foo", dependencies:[.product(name:"Path",package:"Path.swift")]))

CocoaPods:

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

Carthage:

Waiting on:@Carthage#1945.

Naming Conflicts withSwiftUI.Path, etc.

We have a typealias ofPathStruct you can use instead.

Alternatives

About

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

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

No packages published

Contributors9


[8]ページ先頭

©2009-2025 Movatter.jp