Giter Site home page Giter Site logo

path.swift's Introduction

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
// ^^ 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:
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.

Sponsor @mxcl

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

Sponsor @mxcl.

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",
]

Though we recommend encoding relative paths‡:

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

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

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

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

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 (potentially) dangerous (at runtime).

Pathish

Path, and DynamicPath (the result of eg. Path.root) both conform to Pathish which is a protocol that contains all pathing functions. Thus if you create objects from a mixture of both you need to create generic functions or convert any DynamicPaths to Path first:

let path1 = Path("/usr/lib")!
let path2 = Path.root.usr.bin
var paths = [Path]()
paths.append(path1)        // fine
paths.append(path2)        // error
paths.append(Path(path2))  // ok

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

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.

Initializing from known strings

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

let absolutePath = "/known/path"
let path1 = Path.root/absolutePath

let pathWithoutInitialSlash = "known/path"
let path2 = Path.root/pathWithoutInitialSlash

assert(path1 == path2)

let path3 = Path(absolutePath)!  // at your options

assert(path2 == path3)

// be cautious:
let path4 = Path(pathWithoutInitialSlash)!  // CRASH!

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" }

let includingHiddenFiles = Path.home.ls(.a)

Note ls() does not throw, instead outputing a warning to the console if it fails to list the directory. The rationale for this is weak, please open a ticket for discussion.

We provide find() for recursive listing:

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

It is configurable:

for path in Path.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 { path in
    guard 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:

let paths = Path.home.find().map(\.self)

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 just get on with it and not have to worry.

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 (normalized) 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 simultaneously 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

// though note that we provide `.parent`:
Path.home.foo.bar.parent      // => /Users/mxcl/foo

// 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.root.foo       // => /foo

// unless you do it explicitly
try Path.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 call readlink on a non-symlink, we return self

However notably if you try to copy or move a file without 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 type 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 to change its working directory when it executes.

If you must change directory then use FileManager.changeCurrentDirectory as early in your process as possible. Altering the global state of your app’s environment is fundamentally dangerous creating hard to debug issues that you won‘t find for potentially years.

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.

In defense of our naming scheme

Chainable syntax demands short method names, thus we adopted the naming scheme of the terminal, which is absolutely not very “Apple” when it comes to how they design their APIs, however for users of the terminal (which surely is most developers) 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 with SwiftUI.Path, etc.

We have a typealias of PathStruct you can use instead.

Alternatives

path.swift's People

Contributors

confusedvorlon avatar dkolas avatar jaapwijnen avatar lakshya-sky avatar lucianopalmeida avatar mxcl avatar repo-ranger[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

path.swift's Issues

Refactor

I think some var names are bad ,so should think about refactoring it ,for instance when I saw the var name ,if lack of comments I cannot understand the meaning about it ..

tiny typo

In your installation instructions, you have

pod 'Path.swift' ~> '0.5.0'

where it should be

pod 'Path.swift', '~> 0.5.0'
or
pod 'Path.swift', '~> 0.9.0'

Incorrect framework name when using CocoaPods

If Path.swift is added as a dependency using CocoaPods, Path_swift.framework ends up getting generated, not Path.framework. This leads to the user needing to do import Path_swift instead of import Path.

module_name needs to be specified in the Podspec, as it is done in SQLite.swift here.

Drop `@dynamicMember`

I keep writing code like foo.path where I'm thinking foo is some other object, but it’s already a Path, this compiles our to dynamicMember and then I'm confused about it.

In general there are only a few times it is useful:

Path.root.usr.local
Path.home.Library
Bundle.main.resources.foo
// etc.

So perhaps make it so these return some other type that can be dynamic-membered while standard Paths cannot.

Also:

Path.usr.bin

Would be nice.

Issue with CLion.

adding Path as dependency gives error in CLion.

/Path.swift/Package.swift:15:67: error: use of unresolved identifier 'package'; did you mean 'Package'?
; import Foundation; print(String(data: try! JSONEncoder().encode(package), encoding: .utf8)!)
^~~~~~~
Package
PackageDescription.Package:1:20: note: 'Package' declared here
final public class Package {
^

error: 'Path' is not a member type of 'Path'

I'm not sure if this is my issue, but I get this error when I try to do anything with Path on Google Colab:

expression produced error: error: /tmp/expr56-6421f7..swift:1:70: error: 'Path' is not a member type of 'Path'
Swift._DebuggerSupport.stringForPrintObject(Swift.UnsafePointer<Path.Path>(bitPattern: 0x7f6d9bca1340)!.pointee)
                                                                ~~~~ ^

Path.Path:1:36: note: 'Path' declared here
@dynamicMemberLookup public struct Path : Equatable, Hashable, Comparable {

My code:

%install-location $cwd/swift-install
%install '.package(url: "https://github.com/mxcl/Path.swift", from: "0.16.1")' Path
import Path
Path("/")
// or:
Path.root

suggestion: Entry.files(withExtension) could make the extension optional

I'm making a list of files in a directory.
I want files that are not directories.

I see that there is a convenience filter

path.ls().directories()

but not

path.ls().files()

my suggestion would be to make the withExtension param optional in the existing files convenience function

func files(withExtension ext: String? = nil)

so that you can use

path.ls().files() -> Returns all non-directories

or

path.ls().files(withExtension:"png") -> Returns all non-directories with file extension .png

while you're in there, it would also be handy to have

func files(withExtensions ext: [String])

e.g. for when you're looking for image files and have a list of supported extensions

thanks for the library. I'm just starting to play with it, but am impressed so far :)

Consider making TemporaryDirectory public?

Hi,

I was checking out swiftsh code and found Path.mktemp which is really useful, but when trying to use it I was surprised is not available in Path. I see that in this library is just used as an helper for the tests but swiftsh uses it for production code in eject.

Would you consider making TemporaryDirectory part of the Path API? Or is there any reason why that's a bad idea?

Cheers

How I can properly add Path to swift manifest file

Hi,
I tried to add Path to dependencies of Package.swift file and I'm getting this error if I list "Path" as target.dependecies["Path"].

Updating https://github.com/mxcl/Path.swift
Resolving https://github.com/mxcl/Path.swift at 1.2.0
'MyCode' /Users/vk/Work/Languages/Swift/MyCode: error: dependency 'Path' in target 'SwiftMLExample' requires explicit declaration; reference the package in the target dependency with '.product(name: "Path", package: "Path.swift")'

If I list it as

.target(dependencies[.product(name: "Path", package: "Path")])

I get this error: unknown package 'Path' in dependencies of target.

Can someone show example of Package.swift file where Path is included in dependencies? I'm new to swift.
Thanks,
Valentin.

Add conformances to support Swift ArgumentParser?

The Swift ArgumentParser library makes it easy to parse command-line arguments. I feel like it should be possible to directly provide a Path argument:

struct
Calculate : ParsableCommand
{
	@Option(name: .shortAndLong, help: "Path to input word list, one per line")
	var inputWordList: Path = Path.cwd/"wordlist.txt"
}

extension
Path : ExpressibleByArgument
{
	public init?(argument: String) {
		if let p = Path(argument)
		{
			self = p
		}
		else
		{
			return nil
		}
	}
}

but the ExpressibleByArgument conformance seems clunky to me.

Is there a way to create a file and its directory if missing?

Currently, path.touch() fails if path.parent, path.parent.parent, and so on doesn't exist.
Currently, my workaround is as such:

/// Get the missing parent directories of a path, shallow to deep.
/// - Parameter path: The path to get the missing parent directories of.
/// - Returns: An array of the missing parent directories.
func getMissingParentDirectories(of path: Path) -> [Path] {
    var missingParentDirectories: [Path] = []
    var currentPath = path.parent
    while !currentPath.exists {
        missingParentDirectories.append(currentPath)
        currentPath = currentPath.parent
    }
    return missingParentDirectories.reversed()
}

/// Create the missing parent directories of a path, shallow to deep.
/// - Parameter path: The path to create the missing parent directories of.
func createMissingParentDirectories(of path: Path) {
    let missingParentDirectories = getMissingParentDirectories(of: path)
    for missingParentDirectory in missingParentDirectories {
        do {
            try missingParentDirectory.mkdir()
        } catch {
            fatalError("Could not create the directory \(missingParentDirectory)")
        }
    }
}

Is there a native way to do this that I am missing?

[Swift 5] `swift build` fails because it has a minimum platform requirement

$ swift build --configuration release -Xswiftc -static-stdlib --disable-sandbox
error: the product 'Path' requires minimum platform version 10.0 for tvos platform
error: the product 'Path' requires minimum platform version 3.0 for watchos platform

Steps

  1. Clone something which depends on Path.swift (e.g. swift-sh)
  2. Execute swift build

Environment

  • Xcode 10.2 Beta
  • Swift 5

Am I missing something really obvious here or should this work?

[Feature request] filehandles

As the isFile property is implemented I thought maybe it would be an idea to implement to get an optional FileHandle from a path. Let me know what you think, if you like it I can make a PR.

import Path // mxcl/Path.swift ~> 1.0.0 not working with Xcode 11.4

I have a script

#!/usr/bin/swift sh

import Commander // @kylef ~> 0.9.1
import Environment // @wlisac ~> 0.11.1
import Foundation
import Path // mxcl/Path.swift ~> 1.0.0
import ShellOut // @JohnSundell ~> 2.0.0

When I run it with Xcode 11.3.1 command-line tools, it compiles fine.
When I runt it with Xcode 11.4 command-line tools, I get the following error:

Resolving https://github.com/mxcl/Path.swift.git at 1.0.1
'lint' /Users/srebaud/Library/Developer/swift-sh.cache/6d4dfe72d3b82da83c126a62c089349f: error: dependency 'Path' in target 'lint' requires explicit declaration; reference the package in the target dependency with '.product(name: "Path", package: "Path.swift")'
error: 1 <(/usr/bin/swift build -Xswiftc -suppress-warnings)

In both cases, the Package.swift is identical:

// swift-tools-version:5.2
import PackageDescription

let pkg = Package(name: "lint")

pkg.products = [
    .executable(name: "lint", targets: ["lint"])
]
pkg.dependencies = [
    .package(url: "https://github.com/kylef/Commander.git", .upToNextMajor(from: "0.9.1")),
    .package(url: "https://github.com/wlisac/Environment.git", .upToNextMajor(from: "0.11.1")),
    .package(url: "https://github.com/mxcl/Path.swift.git", .upToNextMajor(from: "1.0.0")),
    .package(url: "https://github.com/JohnSundell/ShellOut.git", .upToNextMajor(from: "2.0.0"))
]
pkg.targets = [
    .target(name: "lint", dependencies: ["Commander", "Environment", "Path", "ShellOut"], path: ".", sources: ["main.swift"])
]

#if swift(>=5) && os(macOS)
pkg.platforms = [
   .macOS(.v10_15)
]
#endif

`delete()` shouldn’t throw if no file

Thus consistent with mkdir()

But we should have a consistent flag to relevant functions to suppress this.

Should consider removing mkpath too and instead doing mkdir with a flag.

Could do eg:

try mkdir(.p)
try mkdir()
try ls(.a)
try ls()

Path.home.find(): Value of type 'Any' has no member 'extension'

Hello,
first of all, thanks for providing this library. :-)
Secondly, I'm completely new to Swift, so bear with me if I'm just holding this wrong.

I was playing around with the API and cannot get one of the examples from the README to work.
This code...

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

... always gives me this error:

Value of type 'Any' has no member 'extension'

I tried to add execute to the call, to align it with the other examples, but that didn't help.
I'm using SPM to install version 1.0.0-alpha.3. Any help is appreciated. Thanks!

Add `mimeType` property?

I need to guess mime types for files and have extended Path as follows:

extension Path {
    public var mimeType: String? {
        return MimeTypes[self.extension]
    }
}


let MimeTypes = [
    "323": "text/h323",
    ... <snip long list I grabbed from https://github.com/Microsoft/referencesource/blob/master/System.Web/MimeMapping.cs > ...
]

I know there's more to guessing mime types than this but maybe it's a useful start - at least it works for what I need it to do right now.

Is it worth opening a PR with this or is it out of scope?

Path conflicts with SwiftUI.Path, no way to disambiguate

Alas, if you import both Path and SwiftUI, you'll get a compiler error when using Path in your code, and there’s no way to disambiguate Path.Path. In a SwiftPM project, you can alias the module name when you import it, but there appears to be no way to do that in Xcode.

Output for ls() is unsorted, but ls in the command line is sorted alphabetically by default

I was surprise to find out that ls() output is not sorted.

In the source code (here https://github.com/mxcl/Path.swift/blob/master/Sources/Path%2Bls.swift#L152) is mentioned that
Same as the ls command ∴ output is ”shallow” and unsorted.

But from the man page of the ls command (http://man7.org/linux/man-pages/man1/ls.1.html) I can read that the output is shorted alphabetically by default.

I can open a PR for this if you want it, but seems to me like a major change, not sure how you want to handle it :)

Suggestion: Add option for .find() to ignore hidden files

I love the easy recursive search. It would be great if this could be augmented with an option to ignore hidden files too.

perhaps:

folderPath.find().depth(max: 2).extension("heic").type(.file).hidden(false)

thanks again for a fantastic package

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.