profile
viewpoint

milseman/yaml-light 4

A light-weight Haskell wrapper with utility functions around HsSyck

milseman/swift 2

The Swift Programming Language

milseman/swift-evolution 2

This maintains proposals for changes and user-visible enhancements to the Swift Programming Language.

milseman/Haskell-Dnd-Utilities 1

Haskell libraries and utilities for playing/DMing Dnd sessions

milseman/MikesDots 0

My configuration files, maude, and misc setups

milseman/swift-corelibs-foundation 0

The Foundation Project, providing core utilities, internationalization, and OS independence

milseman/swift-corelibs-xctest 0

The XCTest Project, A Swift core library for providing unit test support

milseman/swift-integration-tests 0

Automated tests for validating the generated Swift snapshots behave correctly

milseman/swift-lldb 0

This is the version of LLDB that supports the Swift programming language & REPL.

issue commentapple/swift-system

Increased API Coverage

Thanks @miles. For reference, here's where I used the package. It's a great example of where the API's coverage is quite low.

Really glad to see this package coming along though!

milseman

comment created time in 9 days

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+/*+ This source file is part of the Swift System open source project++ Copyright (c) 2020 Apple Inc. and the Swift System project authors+ Licensed under Apache License v2.0 with Runtime Library Exception++ See https://swift.org/LICENSE.txt for license information+*/++// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)+extension FilePath {+  /// Creates a file path by copying bytes from a null-terminated platform string.+  ///+  /// - Parameter platformString: A pointer to a null-terminated platform string.+  public init(platformString: UnsafePointer<PlatformChar>) {+    self.init(SystemString(platformString: platformString))+  }++  /// Calls the given closure with a pointer to the contents of the file path,+  /// represented as a null-terminated platform string.+  ///+  /// - Parameter body: A closure with a pointer parameter+  ///   that points to a null-terminated platform string.+  ///   If `body` has a return value,+  ///   that value is also used as the return value for this method.+  /// - Returns: The return value, if any, of the `body` closure parameter.+  ///+  /// The pointer passed as an argument to `body` is valid+  /// only during the execution of this method.+  /// Don't try to store the pointer for later use.+  public func withPlatformString<Result>(+    _ body: (UnsafePointer<PlatformChar>) throws -> Result+  ) rethrows -> Result {+    try storage.withPlatformString(body)+  }+}++extension FilePath.Component {+  /// Creates a file path component by copying bytes from a null-terminated platform string.+  ///+  /// - Parameter string: A pointer to a null-terminated platform string.+  public init(platformString: UnsafePointer<PlatformChar>) {+    self.init(SystemString(platformString: platformString))+  }++  /// Calls the given closure with a pointer to the contents of the file path component,+  /// represented as a null-terminated platform string.+  ///+  /// If this is not the last component of a path, an allocation will occur in order to+  /// add the null terminator+  ///+  /// - Parameter body: A closure with a pointer parameter+  ///   that points to a null-terminated platform string.+  ///   If `body` has a return value,+  ///   that value is also used as the return value for this method.+  /// - Returns: The return value, if any, of the `body` closure parameter.+  ///+  /// The pointer passed as an argument to `body` is valid+  /// only during the execution of this method.+  /// Don't try to store the pointer for later use.+  public func withPlatformString<Result>(+    _ body: (UnsafePointer<PlatformChar>) throws -> Result+  ) rethrows -> Result {+    try slice.withPlatformString(body)+  }+}++++// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)+extension FilePath: ExpressibleByStringLiteral {+  /// Creates a file path from a string literal.+  ///+  /// - Parameter stringLiteral: A string literal+  ///   whose Unicode encoded contents to use as the contents of the path.+  public init(stringLiteral: String) {+    self.init(stringLiteral)+  }++  /// Creates a file path from a string.+  ///+  /// - Parameter string: A string+  ///   whose Unicode encoded contents to use as the contents of the path.+  public init(_ string: String) {+    self.init(SystemString(string))+  }+}++// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)+extension String {+  /// Creates a string by interpreting the file path's content as UTF-8 on Unix+  /// and UTF-16 on Windows.+  ///+  /// - Parameter path: The file path to be interpreted as `PlatformUnicodeEncoding`.+  ///+  /// If the content of the file path isn't a well-formed Unicode string,+  /// this initializer replaces invalid bytes them with U+FFFD.+  /// This means that, depending on the semantics of the specific file system,+  /// conversion to a string and back to a path+  /// might result in a value that's different from the original path.+  public init(decoding path: FilePath) {+    self = path.withPlatformString { String(platformString: $0) }+  }++  /// Creates a string from a file path, validating its contents as UTF-8 on Unix+  /// and UTF-16 on Windows.+  ///+  /// - Parameter path: The file path to be interpreted as `PlatformUnicodeEncoding`.+  ///+  /// If the contents of the file path isn't a well-formed Unicode string,+  /// this initializer returns `nil`.+  public init?(validating path: FilePath) {+    guard let str = path.withPlatformString(+      String.init(validatingPlatformString:)+    ) else {+      return nil+    }+    self = str+  }+}+++// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)+extension FilePath: CustomStringConvertible, CustomDebugStringConvertible {+  /// A textual representation of the file path.+  ///+  /// If the content of the path isn't a well-formed Unicode string,+  /// this replaces invalid bytes them with U+FFFD. See `String.init(decoding:)`+  @inline(never)+  public var description: String { String(decoding: self) }++  /// A textual representation of the file path, suitable for debugging.+  ///+  /// If the content of the path isn't a well-formed Unicode string,+  /// this replaces invalid bytes them with U+FFFD. See `String.init(decoding:)`+  public var debugDescription: String { description.debugDescription }+}++// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)+extension String {+  /// Creates a string by interpreting the path component's content as UTF-8 on Unix+  /// and UTF-16 on Windows.+  ///+  /// - Parameter path: The path component to be interpreted as `PlatformUnicodeEncoding`.

The initializer methods in this extension appear to be documented incorrectly. Please correct me if I am wrong, but I believe IDEs would expect - Parameter path: to describe a parameter that is not named in any of these signatures. Should these documentation comments instead read - Parameter component?

Also, below is another instance of "invalid bytes them".

milseman

comment created time in 10 days

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+/*+ This source file is part of the Swift System open source project++ Copyright (c) 2020 Apple Inc. and the Swift System project authors+ Licensed under Apache License v2.0 with Runtime Library Exception++ See https://swift.org/LICENSE.txt for license information+*/++// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)+extension FilePath {+  /// Creates a file path by copying bytes from a null-terminated platform string.+  ///+  /// - Parameter platformString: A pointer to a null-terminated platform string.+  public init(platformString: UnsafePointer<PlatformChar>) {+    self.init(SystemString(platformString: platformString))+  }++  /// Calls the given closure with a pointer to the contents of the file path,+  /// represented as a null-terminated platform string.+  ///+  /// - Parameter body: A closure with a pointer parameter+  ///   that points to a null-terminated platform string.+  ///   If `body` has a return value,+  ///   that value is also used as the return value for this method.+  /// - Returns: The return value, if any, of the `body` closure parameter.+  ///+  /// The pointer passed as an argument to `body` is valid+  /// only during the execution of this method.+  /// Don't try to store the pointer for later use.+  public func withPlatformString<Result>(+    _ body: (UnsafePointer<PlatformChar>) throws -> Result+  ) rethrows -> Result {+    try storage.withPlatformString(body)+  }+}++extension FilePath.Component {+  /// Creates a file path component by copying bytes from a null-terminated platform string.+  ///+  /// - Parameter string: A pointer to a null-terminated platform string.+  public init(platformString: UnsafePointer<PlatformChar>) {+    self.init(SystemString(platformString: platformString))+  }++  /// Calls the given closure with a pointer to the contents of the file path component,+  /// represented as a null-terminated platform string.+  ///+  /// If this is not the last component of a path, an allocation will occur in order to+  /// add the null terminator+  ///+  /// - Parameter body: A closure with a pointer parameter+  ///   that points to a null-terminated platform string.+  ///   If `body` has a return value,+  ///   that value is also used as the return value for this method.+  /// - Returns: The return value, if any, of the `body` closure parameter.+  ///+  /// The pointer passed as an argument to `body` is valid+  /// only during the execution of this method.+  /// Don't try to store the pointer for later use.+  public func withPlatformString<Result>(+    _ body: (UnsafePointer<PlatformChar>) throws -> Result+  ) rethrows -> Result {+    try slice.withPlatformString(body)+  }+}++++// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)+extension FilePath: ExpressibleByStringLiteral {+  /// Creates a file path from a string literal.+  ///+  /// - Parameter stringLiteral: A string literal+  ///   whose Unicode encoded contents to use as the contents of the path.+  public init(stringLiteral: String) {+    self.init(stringLiteral)+  }++  /// Creates a file path from a string.+  ///+  /// - Parameter string: A string+  ///   whose Unicode encoded contents to use as the contents of the path.+  public init(_ string: String) {+    self.init(SystemString(string))+  }+}++// @available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)+extension String {+  /// Creates a string by interpreting the file path's content as UTF-8 on Unix+  /// and UTF-16 on Windows.+  ///+  /// - Parameter path: The file path to be interpreted as `PlatformUnicodeEncoding`.+  ///+  /// If the content of the file path isn't a well-formed Unicode string,+  /// this initializer replaces invalid bytes them with U+FFFD.

We probably shouldn't leave "invalid bytes them" in the documentation comments. Should this sentence refer to "invalid bytes" or to "them"?

This phrase occurs in a few other places here as well.

milseman

comment created time in 12 days

startedSiguza/ios-resources

started time in 13 days

startedSiguza/ios-resources

started time in 13 days

startedgoogle/swift-benchmark

started time in 14 days

fork plotfi/Signal-iOS

A private messenger for iOS.

https://signal.org

fork in 17 days

created repositoryplotfi/devenv

Docker/Ubuntu-LTS based Development Environment for LLVM

created time in 23 days

fork plotfi/swift-syntax

SwiftPM package for SwiftSyntax library.

fork in a month

fork plotfi/swift-format

Formatting technology for Swift source code

fork in a month

starteddebauchee/barrier

started time in a month

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {+    get {+      let rootEnd = _parseRoot().0+      guard rootEnd != storage.startIndex else { return nil }+      return Component(self, ..<rootEnd)+    }+    set {+      fatalError()+    }+  }++  /// REVIEW NOTE: I'm dropping rootName and rootDirectory. I think+  /// C++ was a little misguided here after looking more at C#. I think+  /// a better alternative would be to actually identify and work with+  /// the root when on Windows, more akin to Rust's `Prefix` type.++  /// Gets or sets the relative portion of the path (everything after root)+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => foo/bar`+  ///   * `foo/bar => foo/bar`+  ///   * `/ => ""`+  /// * Windows:+  ///   * `C:\foo\bar => foo\bar`+  ///   * `foo\bar => foo\bar`+  ///   * `\\?\UNC\server\share\file => file`+  ///   * `\ => ""`+  ///+  /// REVIEW NOTE: Not optional, can return empty paths. Empty paths+  /// are relative (but are not roots), hence non-optional.+  public var relativePath: FilePath {+    get {+      let (_, relIdx) = _parseRoot()+      return FilePath(slice: storage[relIdx...])+    }+    set { fatalError() }+  }++  /// Returns the final component of the path. If the path is a normal file, this+  /// will be the file name. If the path is a directory, this is the directory name.+  /// Returns `nil` if the path is empty or only contains a root+  ///+  /// REVIEW NOTE: We follow  `basename` semantics, unlike `file_name` in+  /// C++ which treats trailing `/` specially, or in Rust which returns nil for `..`+  ///+  /// REVIEW NOTE: shell and C basename will return `/` if the path is `/`. I don't+  /// think we want to, it seems clearer that this only is in the relative portion of the path,+  /// and doing so would make the setter weird.+  public var basename: FilePath.Component? {+    get {+      let (rootEnd, _) = _parseRoot()+      guard let base = self.components.last, base.slice.startIndex >= rootEnd else {+        return nil+      }+      return base+    }+    set {+      // TODO: this is assuming that pop correctly preserves root+      _ = self.pop()+      if let c = newValue { self.append(FilePath(c)) }+    }+  }++  /// Returns the path up to but not including the `basename`.+  ///+  /// If the path only contains a root, returns `self`.+  /// If the path has no root and only includes a single component,+  /// returns an empty FilePath.+  ///+  /// REVIEW NOTE: Shell returns `.` if there is no dirname, while we return the empty path.+  /// REVIEW NOTE: I removed the setter. It seems sketchy and its use indicates programmer error.+  public var dirname: FilePath {+    get {+      // No basename means path is root or empty (which is the dirname)+      if self.basename == nil { return self }+      return FilePath(self.components.dropLast())+    }+  }++  /// The extension of the file or directoy last component.+  ///+  /// If `basename` is `nil` or one of the special path components+  /// `.` or `..`, `get` returns `nil` and `set` does nothing.+  ///+  /// If `basename` does not contain a `.` anywhere, or only+  /// at the start, `get` returns `nil` and `set` will append a+  /// `.` and `newValue` to `basename`+  ///+  /// Otherwise `get` returns everyting after it, and `set` will+  /// replace the extension+  ///+  /// Examples:+  ///   * `/tmp/foo.txt => "txt"`+  ///   * `/Appliations/Foo.app/ => app`+  ///   * `/Appliations/Foo.app/bar.txt => txt`+  ///   * `/tmp/.hidden => nil``+  ///   * `/tmp/.. => nil``+  ///+  /// REVIEW NOTE: Rust has a set_extension instead, which returns whether it+  /// did anything, but having a setter is more convenient.+  public var `extension`: String? {+    get {+      fatalError()+    }+    set { fatalError() }+  }++  /// The non-extension portion of the file or directoy last component.+  ///+  /// Returns `nil` if `basename` is `nil`+  ///+  ///   * `/tmp/foo.txt => "foo"`+  ///   * `/Appliations/Foo.app/ => Foo`+  ///   * `/Appliations/Foo.app/bar.txt => bar`+  ///   * `/tmp/.hidden => .hidden``+  ///   * `/tmp/.. => ..``+  ///   * `/ => nil`+  ///+  /// TODO: setter? That's a little harder to imagine than for `extension`.+  /// Setting to `nil` is more likely a bug.+  public var stem: String? {+    fatalError()+  }++}++// Modification and concatenation API+extension FilePath {+  /// Remove the contents, keeping the null termiantor+  ///+  /// TODO: better name? I like `clear` but that's not "Swifty"+  /// TODO: Since we'll still have the null terminator, should we intern the empty path?+  public mutating func removeContents(keepingCapacity: Bool = false) {+    fatalError()+  }++  /// Reserve enough storage space to store `minimumCapacity` path characters+  public mutating func reserveCapacity(_ minimumCapacity: Int) {+    self.storage.reserveCapacity(1 + minimumCapacity)+  }++  /// Append the contents of `other`.+  ///+  /// TODO: What if `other` is absolute? C++ does replacement for append+  /// but not for concat... Rust's `push` does replacement. Should we just do push/pop?+  ///+  public mutating func append(_ other: FilePath) {+    // TODO: Faster to do byte copy, checking for leading separator+    self.components.append(contentsOf: other.components)+  }++  /// Remove and return the last component of this file path. If the path is+  /// root or empty, does nothing and returns `nil`.+  ///+  /// Examples:+  /// * `"/".pop() == nil          // path is /`+  /// * `"/foo/bar".pop() == "bar" // path is /foo`+  public mutating func pop() -> FilePath.Component? {+    self.components.popLast() // FIXME: popLastButNotRoot()+  }++  /// See `append`+  ///+  /// TODO: Should this be `/`? Should the RHS have to be Component?+  public static func +(_ lhs: FilePath, _ rhs: FilePath) -> FilePath {+    var result = lhs+    result.append(rhs)+    return result+  }++  /// Collapse "." and ".." components syntactically, that is, without following symlinks+  ///+  /// TODO: Should we have a var as well?+  mutating func lexicallyNormalize() {+    fatalError()+  }++  /// Returns `self` in a form that's relative to `base`.+  /// This does not cosult the file system or resolve symlinks.+  ///+  /// Returns `nil` if they do not share the same root+  ///+  /// TODO: examples+  ///+  /// TODO: C++ adds in the "if any `file_name` in either `relative_path` can be interpreted as a root name, return empty".+  /// What should we say/do?+  public func lexicallyRelative(toBase base: FilePath) -> FilePath? {+    fatalError()+  }+++  /// If `prefix` is a prefix of `self`, removes it and returns `true`. Otherwise+  /// returns `false`.+  ///+  /// TODO: var too?+  public mutating func stripPrefix(_ prefix: FilePath) -> Bool {+    fatalError()+  }+}++// Convenience helpers+extension FilePath {+  /// TODO: Should we have this? It is massively annoying+  /// to have to call String(decoding: path), especially since so+  /// many of our operations return `FilePath`.+  public var string: String {+    String(decoding: self)+  }++  /// TODO: This would otherwise be pretty annoying for users to do+  /// themselves+  public var componentStrings: [String] {+    self.components.map { String(decoding: $0) }+  }+}++// REVIEW NOTE: This is a mock-up of the kind of thing we could provide on Windows+// as future work. Remove this before merging.+#if os(Windows)+extension FilePath {+  /*public*/ struct WindowsRootName {+    /*public*/ enum Kind {+      case drive(Character)+      case unc(host: String, share: String)+      case volume(String) // Would this be for \\?\Foo+    }+    /*public*/ var kind: Kind { fatalError() }+    /*public*/ var isVerbatim: Bool { fatalError() }+  }+  /*+   DOS: letter ':'+   UNC: '\\' host name '\' (share name | letter '$')+   DOS device:+   ('\\.\' | '\\?\') (letter ':' | 'Volume{' GUID '}')+   */+  /*public*/ var windowsRootName: WindowsRootName? { nil }

What I meant by my question was whether on Windows, I could create a path that had the root corresponding to a particular drive letter (such as one entered by a user) without already having a FilePath instance from which to get a root.

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {+    get {+      let rootEnd = _parseRoot().0+      guard rootEnd != storage.startIndex else { return nil }+      return Component(self, ..<rootEnd)+    }+    set {+      fatalError()+    }+  }++  /// REVIEW NOTE: I'm dropping rootName and rootDirectory. I think+  /// C++ was a little misguided here after looking more at C#. I think+  /// a better alternative would be to actually identify and work with+  /// the root when on Windows, more akin to Rust's `Prefix` type.++  /// Gets or sets the relative portion of the path (everything after root)+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => foo/bar`+  ///   * `foo/bar => foo/bar`+  ///   * `/ => ""`+  /// * Windows:+  ///   * `C:\foo\bar => foo\bar`+  ///   * `foo\bar => foo\bar`+  ///   * `\\?\UNC\server\share\file => file`+  ///   * `\ => ""`+  ///+  /// REVIEW NOTE: Not optional, can return empty paths. Empty paths+  /// are relative (but are not roots), hence non-optional.+  public var relativePath: FilePath {+    get {+      let (_, relIdx) = _parseRoot()+      return FilePath(slice: storage[relIdx...])+    }+    set { fatalError() }+  }++  /// Returns the final component of the path. If the path is a normal file, this+  /// will be the file name. If the path is a directory, this is the directory name.+  /// Returns `nil` if the path is empty or only contains a root+  ///+  /// REVIEW NOTE: We follow  `basename` semantics, unlike `file_name` in+  /// C++ which treats trailing `/` specially, or in Rust which returns nil for `..`+  ///+  /// REVIEW NOTE: shell and C basename will return `/` if the path is `/`. I don't+  /// think we want to, it seems clearer that this only is in the relative portion of the path,+  /// and doing so would make the setter weird.+  public var basename: FilePath.Component? {+    get {+      let (rootEnd, _) = _parseRoot()+      guard let base = self.components.last, base.slice.startIndex >= rootEnd else {+        return nil+      }+      return base+    }+    set {+      // TODO: this is assuming that pop correctly preserves root+      _ = self.pop()+      if let c = newValue { self.append(FilePath(c)) }+    }+  }++  /// Returns the path up to but not including the `basename`.+  ///+  /// If the path only contains a root, returns `self`.+  /// If the path has no root and only includes a single component,+  /// returns an empty FilePath.+  ///+  /// REVIEW NOTE: Shell returns `.` if there is no dirname, while we return the empty path.+  /// REVIEW NOTE: I removed the setter. It seems sketchy and its use indicates programmer error.+  public var dirname: FilePath {+    get {+      // No basename means path is root or empty (which is the dirname)+      if self.basename == nil { return self }+      return FilePath(self.components.dropLast())+    }+  }++  /// The extension of the file or directoy last component.+  ///+  /// If `basename` is `nil` or one of the special path components+  /// `.` or `..`, `get` returns `nil` and `set` does nothing.+  ///+  /// If `basename` does not contain a `.` anywhere, or only+  /// at the start, `get` returns `nil` and `set` will append a+  /// `.` and `newValue` to `basename`

It would be a platform-specific consideration. I looked to see if I could find anything more specific about macOS, which is the platform I was thinking of with the no-digits consideration, but I couldn't find anything relevant so I may be misremembering. This was something that was discussed a lot around the time of Mac OS X 10.0, when the Classic MacOS semantics were being reconciled with the Unix-based conventions of OPENSTEP. But I think I'm either misremembering or things have changed, since it's just that the . can't be the first or last character of the last path component.

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {+    get {+      let rootEnd = _parseRoot().0+      guard rootEnd != storage.startIndex else { return nil }+      return Component(self, ..<rootEnd)+    }+    set {+      fatalError()+    }+  }++  /// REVIEW NOTE: I'm dropping rootName and rootDirectory. I think+  /// C++ was a little misguided here after looking more at C#. I think+  /// a better alternative would be to actually identify and work with+  /// the root when on Windows, more akin to Rust's `Prefix` type.++  /// Gets or sets the relative portion of the path (everything after root)+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => foo/bar`+  ///   * `foo/bar => foo/bar`+  ///   * `/ => ""`+  /// * Windows:+  ///   * `C:\foo\bar => foo\bar`+  ///   * `foo\bar => foo\bar`+  ///   * `\\?\UNC\server\share\file => file`+  ///   * `\ => ""`+  ///+  /// REVIEW NOTE: Not optional, can return empty paths. Empty paths+  /// are relative (but are not roots), hence non-optional.+  public var relativePath: FilePath {+    get {+      let (_, relIdx) = _parseRoot()+      return FilePath(slice: storage[relIdx...])+    }+    set { fatalError() }+  }++  /// Returns the final component of the path. If the path is a normal file, this+  /// will be the file name. If the path is a directory, this is the directory name.

Thanks, that's much clearer.

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {

Ah, right, that's true. I forgot about that, and that certainly complicates things.

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }

This makes sense. Thanks for explaining.

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {

Right, I do think that we'd want to avoid #if at the callsites where possible. Maybe a better way for me to phrase the question is "what API would a client use to get a portable description that can be stored in a file and reconstituted on any platform".

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }

That makes sense.

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {

Understood. I do think that ~ expansion and realpath would probably be outside of a FilePath value type (especially realpath, since it depends on which file system is involved), but that would be a later discussion as well.

milseman

comment created time in 2 months

issue commentapple/swift-system

Increased API Coverage

Not sure if it's on the road map, but additional FileDescriptor operations such as dup and dup2 also support for the FILE APIs? I'm not 100% sure if this aligns with this packages goals, just commenting because I was recently doing some things with these.

milseman

comment created time in 2 months

pull request commentapple/swift-system

[Draft]: FilePath syntactic operations

Overall this looks great! Really excited to see this API!

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {+    get {+      let rootEnd = _parseRoot().0+      guard rootEnd != storage.startIndex else { return nil }+      return Component(self, ..<rootEnd)+    }+    set {+      fatalError()+    }+  }++  /// REVIEW NOTE: I'm dropping rootName and rootDirectory. I think+  /// C++ was a little misguided here after looking more at C#. I think+  /// a better alternative would be to actually identify and work with+  /// the root when on Windows, more akin to Rust's `Prefix` type.++  /// Gets or sets the relative portion of the path (everything after root)+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => foo/bar`+  ///   * `foo/bar => foo/bar`+  ///   * `/ => ""`+  /// * Windows:+  ///   * `C:\foo\bar => foo\bar`+  ///   * `foo\bar => foo\bar`+  ///   * `\\?\UNC\server\share\file => file`+  ///   * `\ => ""`+  ///+  /// REVIEW NOTE: Not optional, can return empty paths. Empty paths+  /// are relative (but are not roots), hence non-optional.+  public var relativePath: FilePath {+    get {+      let (_, relIdx) = _parseRoot()+      return FilePath(slice: storage[relIdx...])+    }+    set { fatalError() }+  }++  /// Returns the final component of the path. If the path is a normal file, this+  /// will be the file name. If the path is a directory, this is the directory name.+  /// Returns `nil` if the path is empty or only contains a root+  ///+  /// REVIEW NOTE: We follow  `basename` semantics, unlike `file_name` in+  /// C++ which treats trailing `/` specially, or in Rust which returns nil for `..`+  ///+  /// REVIEW NOTE: shell and C basename will return `/` if the path is `/`. I don't+  /// think we want to, it seems clearer that this only is in the relative portion of the path,+  /// and doing so would make the setter weird.+  public var basename: FilePath.Component? {+    get {+      let (rootEnd, _) = _parseRoot()+      guard let base = self.components.last, base.slice.startIndex >= rootEnd else {+        return nil+      }+      return base+    }+    set {+      // TODO: this is assuming that pop correctly preserves root+      _ = self.pop()+      if let c = newValue { self.append(FilePath(c)) }+    }+  }++  /// Returns the path up to but not including the `basename`.+  ///+  /// If the path only contains a root, returns `self`.+  /// If the path has no root and only includes a single component,+  /// returns an empty FilePath.+  ///+  /// REVIEW NOTE: Shell returns `.` if there is no dirname, while we return the empty path.+  /// REVIEW NOTE: I removed the setter. It seems sketchy and its use indicates programmer error.+  public var dirname: FilePath {+    get {+      // No basename means path is root or empty (which is the dirname)+      if self.basename == nil { return self }+      return FilePath(self.components.dropLast())+    }+  }++  /// The extension of the file or directoy last component.+  ///+  /// If `basename` is `nil` or one of the special path components+  /// `.` or `..`, `get` returns `nil` and `set` does nothing.+  ///+  /// If `basename` does not contain a `.` anywhere, or only+  /// at the start, `get` returns `nil` and `set` will append a+  /// `.` and `newValue` to `basename`+  ///+  /// Otherwise `get` returns everyting after it, and `set` will+  /// replace the extension+  ///+  /// Examples:+  ///   * `/tmp/foo.txt => "txt"`+  ///   * `/Appliations/Foo.app/ => app`+  ///   * `/Appliations/Foo.app/bar.txt => txt`+  ///   * `/tmp/.hidden => nil``+  ///   * `/tmp/.. => nil``+  ///+  /// REVIEW NOTE: Rust has a set_extension instead, which returns whether it+  /// did anything, but having a setter is more convenient.+  public var `extension`: String? {+    get {+      fatalError()+    }+    set { fatalError() }+  }++  /// The non-extension portion of the file or directoy last component.+  ///+  /// Returns `nil` if `basename` is `nil`+  ///+  ///   * `/tmp/foo.txt => "foo"`+  ///   * `/Appliations/Foo.app/ => Foo`+  ///   * `/Appliations/Foo.app/bar.txt => bar`+  ///   * `/tmp/.hidden => .hidden``+  ///   * `/tmp/.. => ..``+  ///   * `/ => nil`+  ///+  /// TODO: setter? That's a little harder to imagine than for `extension`.+  /// Setting to `nil` is more likely a bug.+  public var stem: String? {+    fatalError()+  }++}++// Modification and concatenation API+extension FilePath {+  /// Remove the contents, keeping the null termiantor+  ///+  /// TODO: better name? I like `clear` but that's not "Swifty"+  /// TODO: Since we'll still have the null terminator, should we intern the empty path?+  public mutating func removeContents(keepingCapacity: Bool = false) {+    fatalError()+  }++  /// Reserve enough storage space to store `minimumCapacity` path characters+  public mutating func reserveCapacity(_ minimumCapacity: Int) {+    self.storage.reserveCapacity(1 + minimumCapacity)+  }++  /// Append the contents of `other`.+  ///+  /// TODO: What if `other` is absolute? C++ does replacement for append+  /// but not for concat... Rust's `push` does replacement. Should we just do push/pop?+  ///+  public mutating func append(_ other: FilePath) {+    // TODO: Faster to do byte copy, checking for leading separator+    self.components.append(contentsOf: other.components)+  }++  /// Remove and return the last component of this file path. If the path is+  /// root or empty, does nothing and returns `nil`.+  ///+  /// Examples:+  /// * `"/".pop() == nil          // path is /`+  /// * `"/foo/bar".pop() == "bar" // path is /foo`+  public mutating func pop() -> FilePath.Component? {+    self.components.popLast() // FIXME: popLastButNotRoot()+  }++  /// See `append`+  ///+  /// TODO: Should this be `/`? Should the RHS have to be Component?+  public static func +(_ lhs: FilePath, _ rhs: FilePath) -> FilePath {+    var result = lhs+    result.append(rhs)+    return result+  }++  /// Collapse "." and ".." components syntactically, that is, without following symlinks+  ///+  /// TODO: Should we have a var as well?+  mutating func lexicallyNormalize() {+    fatalError()+  }++  /// Returns `self` in a form that's relative to `base`.+  /// This does not cosult the file system or resolve symlinks.+  ///+  /// Returns `nil` if they do not share the same root+  ///+  /// TODO: examples+  ///+  /// TODO: C++ adds in the "if any `file_name` in either `relative_path` can be interpreted as a root name, return empty".+  /// What should we say/do?+  public func lexicallyRelative(toBase base: FilePath) -> FilePath? {+    fatalError()+  }+++  /// If `prefix` is a prefix of `self`, removes it and returns `true`. Otherwise+  /// returns `false`.+  ///+  /// TODO: var too?+  public mutating func stripPrefix(_ prefix: FilePath) -> Bool {+    fatalError()+  }+}++// Convenience helpers+extension FilePath {+  /// TODO: Should we have this? It is massively annoying+  /// to have to call String(decoding: path), especially since so+  /// many of our operations return `FilePath`.+  public var string: String {+    String(decoding: self)+  }++  /// TODO: This would otherwise be pretty annoying for users to do+  /// themselves+  public var componentStrings: [String] {+    self.components.map { String(decoding: $0) }+  }+}++// REVIEW NOTE: This is a mock-up of the kind of thing we could provide on Windows+// as future work. Remove this before merging.+#if os(Windows)+extension FilePath {+  /*public*/ struct WindowsRootName {+    /*public*/ enum Kind {+      case drive(Character)+      case unc(host: String, share: String)+      case volume(String) // Would this be for \\?\Foo+    }+    /*public*/ var kind: Kind { fatalError() }+    /*public*/ var isVerbatim: Bool { fatalError() }+  }+  /*+   DOS: letter ':'+   UNC: '\\' host name '\' (share name | letter '$')+   DOS device:+   ('\\.\' | '\\?\') (letter ':' | 'Volume{' GUID '}')+   */+  /*public*/ var windowsRootName: WindowsRootName? { nil }

Will there also be a way to get at the various roots on Windows, e.g. a static function to return the root for a particular drive letter on Windows? Or would that always be done by just creating a particular path with a C:\ string passed in?

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {+    get {+      let rootEnd = _parseRoot().0+      guard rootEnd != storage.startIndex else { return nil }+      return Component(self, ..<rootEnd)+    }+    set {+      fatalError()+    }+  }++  /// REVIEW NOTE: I'm dropping rootName and rootDirectory. I think+  /// C++ was a little misguided here after looking more at C#. I think+  /// a better alternative would be to actually identify and work with+  /// the root when on Windows, more akin to Rust's `Prefix` type.++  /// Gets or sets the relative portion of the path (everything after root)+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => foo/bar`+  ///   * `foo/bar => foo/bar`+  ///   * `/ => ""`+  /// * Windows:+  ///   * `C:\foo\bar => foo\bar`+  ///   * `foo\bar => foo\bar`+  ///   * `\\?\UNC\server\share\file => file`+  ///   * `\ => ""`+  ///+  /// REVIEW NOTE: Not optional, can return empty paths. Empty paths+  /// are relative (but are not roots), hence non-optional.+  public var relativePath: FilePath {+    get {+      let (_, relIdx) = _parseRoot()+      return FilePath(slice: storage[relIdx...])+    }+    set { fatalError() }+  }++  /// Returns the final component of the path. If the path is a normal file, this+  /// will be the file name. If the path is a directory, this is the directory name.+  /// Returns `nil` if the path is empty or only contains a root+  ///+  /// REVIEW NOTE: We follow  `basename` semantics, unlike `file_name` in+  /// C++ which treats trailing `/` specially, or in Rust which returns nil for `..`+  ///+  /// REVIEW NOTE: shell and C basename will return `/` if the path is `/`. I don't+  /// think we want to, it seems clearer that this only is in the relative portion of the path,+  /// and doing so would make the setter weird.+  public var basename: FilePath.Component? {+    get {+      let (rootEnd, _) = _parseRoot()+      guard let base = self.components.last, base.slice.startIndex >= rootEnd else {+        return nil+      }+      return base+    }+    set {+      // TODO: this is assuming that pop correctly preserves root+      _ = self.pop()+      if let c = newValue { self.append(FilePath(c)) }+    }+  }++  /// Returns the path up to but not including the `basename`.+  ///+  /// If the path only contains a root, returns `self`.+  /// If the path has no root and only includes a single component,+  /// returns an empty FilePath.+  ///+  /// REVIEW NOTE: Shell returns `.` if there is no dirname, while we return the empty path.+  /// REVIEW NOTE: I removed the setter. It seems sketchy and its use indicates programmer error.+  public var dirname: FilePath {+    get {+      // No basename means path is root or empty (which is the dirname)+      if self.basename == nil { return self }+      return FilePath(self.components.dropLast())+    }+  }++  /// The extension of the file or directoy last component.+  ///+  /// If `basename` is `nil` or one of the special path components+  /// `.` or `..`, `get` returns `nil` and `set` does nothing.+  ///+  /// If `basename` does not contain a `.` anywhere, or only+  /// at the start, `get` returns `nil` and `set` will append a+  /// `.` and `newValue` to `basename`+  ///+  /// Otherwise `get` returns everyting after it, and `set` will+  /// replace the extension+  ///+  /// Examples:+  ///   * `/tmp/foo.txt => "txt"`+  ///   * `/Appliations/Foo.app/ => app`+  ///   * `/Appliations/Foo.app/bar.txt => txt`+  ///   * `/tmp/.hidden => nil``+  ///   * `/tmp/.. => nil``+  ///+  /// REVIEW NOTE: Rust has a set_extension instead, which returns whether it+  /// did anything, but having a setter is more convenient.+  public var `extension`: String? {+    get {+      fatalError()+    }+    set { fatalError() }+  }++  /// The non-extension portion of the file or directoy last component.+  ///+  /// Returns `nil` if `basename` is `nil`+  ///+  ///   * `/tmp/foo.txt => "foo"`+  ///   * `/Appliations/Foo.app/ => Foo`+  ///   * `/Appliations/Foo.app/bar.txt => bar`+  ///   * `/tmp/.hidden => .hidden``+  ///   * `/tmp/.. => ..``+  ///   * `/ => nil`+  ///+  /// TODO: setter? That's a little harder to imagine than for `extension`.+  /// Setting to `nil` is more likely a bug.+  public var stem: String? {+    fatalError()+  }++}++// Modification and concatenation API+extension FilePath {+  /// Remove the contents, keeping the null termiantor+  ///+  /// TODO: better name? I like `clear` but that's not "Swifty"+  /// TODO: Since we'll still have the null terminator, should we intern the empty path?+  public mutating func removeContents(keepingCapacity: Bool = false) {+    fatalError()+  }++  /// Reserve enough storage space to store `minimumCapacity` path characters+  public mutating func reserveCapacity(_ minimumCapacity: Int) {+    self.storage.reserveCapacity(1 + minimumCapacity)+  }++  /// Append the contents of `other`.+  ///+  /// TODO: What if `other` is absolute? C++ does replacement for append+  /// but not for concat... Rust's `push` does replacement. Should we just do push/pop?+  ///+  public mutating func append(_ other: FilePath) {+    // TODO: Faster to do byte copy, checking for leading separator+    self.components.append(contentsOf: other.components)+  }++  /// Remove and return the last component of this file path. If the path is+  /// root or empty, does nothing and returns `nil`.+  ///+  /// Examples:+  /// * `"/".pop() == nil          // path is /`+  /// * `"/foo/bar".pop() == "bar" // path is /foo`+  public mutating func pop() -> FilePath.Component? {+    self.components.popLast() // FIXME: popLastButNotRoot()+  }++  /// See `append`+  ///+  /// TODO: Should this be `/`? Should the RHS have to be Component?+  public static func +(_ lhs: FilePath, _ rhs: FilePath) -> FilePath {

I think + is the right choice here. One concern with the operator combined with the string literal conversion is that it isn't obvious whether each foo + "usr" + "local" + "include" does, based on whether foo is a path or string; in that case it looks a lot like string concatenation. But / doesn't feel right since it's specific to the Unix form of the path.

If the RHS has to be a component, then there would need to be some other way to append a path.

In TSC's AbsolutePath and RelativePath we avoided the + operator for this reason. Instead there is AbsolutePath.append(_ subpath: RelativePath) and AbsolutePath.append(component: String) (as well as AbsolutePath.append(components: String...)), but that does get a bit cumbersome to type.

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {+    get {+      let rootEnd = _parseRoot().0+      guard rootEnd != storage.startIndex else { return nil }+      return Component(self, ..<rootEnd)+    }+    set {+      fatalError()+    }+  }++  /// REVIEW NOTE: I'm dropping rootName and rootDirectory. I think+  /// C++ was a little misguided here after looking more at C#. I think+  /// a better alternative would be to actually identify and work with+  /// the root when on Windows, more akin to Rust's `Prefix` type.++  /// Gets or sets the relative portion of the path (everything after root)+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => foo/bar`+  ///   * `foo/bar => foo/bar`+  ///   * `/ => ""`+  /// * Windows:+  ///   * `C:\foo\bar => foo\bar`+  ///   * `foo\bar => foo\bar`+  ///   * `\\?\UNC\server\share\file => file`+  ///   * `\ => ""`+  ///+  /// REVIEW NOTE: Not optional, can return empty paths. Empty paths+  /// are relative (but are not roots), hence non-optional.+  public var relativePath: FilePath {+    get {+      let (_, relIdx) = _parseRoot()+      return FilePath(slice: storage[relIdx...])+    }+    set { fatalError() }+  }++  /// Returns the final component of the path. If the path is a normal file, this+  /// will be the file name. If the path is a directory, this is the directory name.+  /// Returns `nil` if the path is empty or only contains a root+  ///+  /// REVIEW NOTE: We follow  `basename` semantics, unlike `file_name` in+  /// C++ which treats trailing `/` specially, or in Rust which returns nil for `..`+  ///+  /// REVIEW NOTE: shell and C basename will return `/` if the path is `/`. I don't+  /// think we want to, it seems clearer that this only is in the relative portion of the path,+  /// and doing so would make the setter weird.+  public var basename: FilePath.Component? {+    get {+      let (rootEnd, _) = _parseRoot()+      guard let base = self.components.last, base.slice.startIndex >= rootEnd else {+        return nil+      }+      return base+    }+    set {+      // TODO: this is assuming that pop correctly preserves root+      _ = self.pop()+      if let c = newValue { self.append(FilePath(c)) }+    }+  }++  /// Returns the path up to but not including the `basename`.+  ///+  /// If the path only contains a root, returns `self`.+  /// If the path has no root and only includes a single component,+  /// returns an empty FilePath.+  ///+  /// REVIEW NOTE: Shell returns `.` if there is no dirname, while we return the empty path.+  /// REVIEW NOTE: I removed the setter. It seems sketchy and its use indicates programmer error.+  public var dirname: FilePath {+    get {+      // No basename means path is root or empty (which is the dirname)+      if self.basename == nil { return self }+      return FilePath(self.components.dropLast())+    }+  }++  /// The extension of the file or directoy last component.+  ///+  /// If `basename` is `nil` or one of the special path components+  /// `.` or `..`, `get` returns `nil` and `set` does nothing.+  ///+  /// If `basename` does not contain a `.` anywhere, or only+  /// at the start, `get` returns `nil` and `set` will append a+  /// `.` and `newValue` to `basename`+  ///+  /// Otherwise `get` returns everyting after it, and `set` will+  /// replace the extension+  ///+  /// Examples:+  ///   * `/tmp/foo.txt => "txt"`+  ///   * `/Appliations/Foo.app/ => app`+  ///   * `/Appliations/Foo.app/bar.txt => txt`+  ///   * `/tmp/.hidden => nil``+  ///   * `/tmp/.. => nil``+  ///+  /// REVIEW NOTE: Rust has a set_extension instead, which returns whether it+  /// did anything, but having a setter is more convenient.+  public var `extension`: String? {+    get {+      fatalError()+    }+    set { fatalError() }+  }++  /// The non-extension portion of the file or directoy last component.+  ///+  /// Returns `nil` if `basename` is `nil`+  ///+  ///   * `/tmp/foo.txt => "foo"`+  ///   * `/Appliations/Foo.app/ => Foo`+  ///   * `/Appliations/Foo.app/bar.txt => bar`+  ///   * `/tmp/.hidden => .hidden``+  ///   * `/tmp/.. => ..``+  ///   * `/ => nil`+  ///+  /// TODO: setter? That's a little harder to imagine than for `extension`.+  /// Setting to `nil` is more likely a bug.+  public var stem: String? {+    fatalError()+  }++}++// Modification and concatenation API+extension FilePath {+  /// Remove the contents, keeping the null termiantor+  ///+  /// TODO: better name? I like `clear` but that's not "Swifty"+  /// TODO: Since we'll still have the null terminator, should we intern the empty path?+  public mutating func removeContents(keepingCapacity: Bool = false) {+    fatalError()+  }++  /// Reserve enough storage space to store `minimumCapacity` path characters+  public mutating func reserveCapacity(_ minimumCapacity: Int) {+    self.storage.reserveCapacity(1 + minimumCapacity)+  }++  /// Append the contents of `other`.+  ///+  /// TODO: What if `other` is absolute? C++ does replacement for append+  /// but not for concat... Rust's `push` does replacement. Should we just do push/pop?+  ///+  public mutating func append(_ other: FilePath) {+    // TODO: Faster to do byte copy, checking for leading separator+    self.components.append(contentsOf: other.components)+  }++  /// Remove and return the last component of this file path. If the path is+  /// root or empty, does nothing and returns `nil`.+  ///+  /// Examples:+  /// * `"/".pop() == nil          // path is /`+  /// * `"/foo/bar".pop() == "bar" // path is /foo`+  public mutating func pop() -> FilePath.Component? {

I would suggest popLast here to be a bit more clear (not a big fan of the word "pop" for data structures that aren't specifically stacks, but there's certainly precedent for that in Swift).

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {+    get {+      let rootEnd = _parseRoot().0+      guard rootEnd != storage.startIndex else { return nil }+      return Component(self, ..<rootEnd)+    }+    set {+      fatalError()+    }+  }++  /// REVIEW NOTE: I'm dropping rootName and rootDirectory. I think+  /// C++ was a little misguided here after looking more at C#. I think+  /// a better alternative would be to actually identify and work with+  /// the root when on Windows, more akin to Rust's `Prefix` type.++  /// Gets or sets the relative portion of the path (everything after root)+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => foo/bar`+  ///   * `foo/bar => foo/bar`+  ///   * `/ => ""`+  /// * Windows:+  ///   * `C:\foo\bar => foo\bar`+  ///   * `foo\bar => foo\bar`+  ///   * `\\?\UNC\server\share\file => file`+  ///   * `\ => ""`+  ///+  /// REVIEW NOTE: Not optional, can return empty paths. Empty paths+  /// are relative (but are not roots), hence non-optional.+  public var relativePath: FilePath {+    get {+      let (_, relIdx) = _parseRoot()+      return FilePath(slice: storage[relIdx...])+    }+    set { fatalError() }+  }++  /// Returns the final component of the path. If the path is a normal file, this+  /// will be the file name. If the path is a directory, this is the directory name.+  /// Returns `nil` if the path is empty or only contains a root+  ///+  /// REVIEW NOTE: We follow  `basename` semantics, unlike `file_name` in+  /// C++ which treats trailing `/` specially, or in Rust which returns nil for `..`+  ///+  /// REVIEW NOTE: shell and C basename will return `/` if the path is `/`. I don't+  /// think we want to, it seems clearer that this only is in the relative portion of the path,+  /// and doing so would make the setter weird.+  public var basename: FilePath.Component? {+    get {+      let (rootEnd, _) = _parseRoot()+      guard let base = self.components.last, base.slice.startIndex >= rootEnd else {+        return nil+      }+      return base+    }+    set {+      // TODO: this is assuming that pop correctly preserves root+      _ = self.pop()+      if let c = newValue { self.append(FilePath(c)) }+    }+  }++  /// Returns the path up to but not including the `basename`.+  ///+  /// If the path only contains a root, returns `self`.+  /// If the path has no root and only includes a single component,+  /// returns an empty FilePath.+  ///+  /// REVIEW NOTE: Shell returns `.` if there is no dirname, while we return the empty path.+  /// REVIEW NOTE: I removed the setter. It seems sketchy and its use indicates programmer error.+  public var dirname: FilePath {+    get {+      // No basename means path is root or empty (which is the dirname)+      if self.basename == nil { return self }+      return FilePath(self.components.dropLast())+    }+  }++  /// The extension of the file or directoy last component.+  ///+  /// If `basename` is `nil` or one of the special path components+  /// `.` or `..`, `get` returns `nil` and `set` does nothing.+  ///+  /// If `basename` does not contain a `.` anywhere, or only+  /// at the start, `get` returns `nil` and `set` will append a+  /// `.` and `newValue` to `basename`+  ///+  /// Otherwise `get` returns everyting after it, and `set` will+  /// replace the extension+  ///+  /// Examples:+  ///   * `/tmp/foo.txt => "txt"`+  ///   * `/Appliations/Foo.app/ => app`+  ///   * `/Appliations/Foo.app/bar.txt => txt`+  ///   * `/tmp/.hidden => nil``+  ///   * `/tmp/.. => nil``+  ///+  /// REVIEW NOTE: Rust has a set_extension instead, which returns whether it+  /// did anything, but having a setter is more convenient.+  public var `extension`: String? {+    get {+      fatalError()+    }+    set { fatalError() }+  }++  /// The non-extension portion of the file or directoy last component.+  ///+  /// Returns `nil` if `basename` is `nil`+  ///+  ///   * `/tmp/foo.txt => "foo"`+  ///   * `/Appliations/Foo.app/ => Foo`+  ///   * `/Appliations/Foo.app/bar.txt => bar`+  ///   * `/tmp/.hidden => .hidden``+  ///   * `/tmp/.. => ..``+  ///   * `/ => nil`+  ///+  /// TODO: setter? That's a little harder to imagine than for `extension`.+  /// Setting to `nil` is more likely a bug.+  public var stem: String? {+    fatalError()+  }++}++// Modification and concatenation API+extension FilePath {+  /// Remove the contents, keeping the null termiantor+  ///+  /// TODO: better name? I like `clear` but that's not "Swifty"+  /// TODO: Since we'll still have the null terminator, should we intern the empty path?+  public mutating func removeContents(keepingCapacity: Bool = false) {+    fatalError()+  }++  /// Reserve enough storage space to store `minimumCapacity` path characters+  public mutating func reserveCapacity(_ minimumCapacity: Int) {+    self.storage.reserveCapacity(1 + minimumCapacity)+  }++  /// Append the contents of `other`.+  ///+  /// TODO: What if `other` is absolute? C++ does replacement for append+  /// but not for concat... Rust's `push` does replacement. Should we just do push/pop?

This is where I would again argue for separate AbsolutePath and RelativePath types. :) It makes the intent clear, not just here, but also for any client APIs that take or return paths. There are a few edge cases where something could be absolute or relative, but in many of those cases it's appropriate to use an enum anyway, at least in the cases where this has come up in SwiftPM.

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {+    get {+      let rootEnd = _parseRoot().0+      guard rootEnd != storage.startIndex else { return nil }+      return Component(self, ..<rootEnd)+    }+    set {+      fatalError()+    }+  }++  /// REVIEW NOTE: I'm dropping rootName and rootDirectory. I think+  /// C++ was a little misguided here after looking more at C#. I think+  /// a better alternative would be to actually identify and work with+  /// the root when on Windows, more akin to Rust's `Prefix` type.++  /// Gets or sets the relative portion of the path (everything after root)+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => foo/bar`+  ///   * `foo/bar => foo/bar`+  ///   * `/ => ""`+  /// * Windows:+  ///   * `C:\foo\bar => foo\bar`+  ///   * `foo\bar => foo\bar`+  ///   * `\\?\UNC\server\share\file => file`+  ///   * `\ => ""`+  ///+  /// REVIEW NOTE: Not optional, can return empty paths. Empty paths+  /// are relative (but are not roots), hence non-optional.+  public var relativePath: FilePath {+    get {+      let (_, relIdx) = _parseRoot()+      return FilePath(slice: storage[relIdx...])+    }+    set { fatalError() }+  }++  /// Returns the final component of the path. If the path is a normal file, this+  /// will be the file name. If the path is a directory, this is the directory name.+  /// Returns `nil` if the path is empty or only contains a root+  ///+  /// REVIEW NOTE: We follow  `basename` semantics, unlike `file_name` in+  /// C++ which treats trailing `/` specially, or in Rust which returns nil for `..`+  ///+  /// REVIEW NOTE: shell and C basename will return `/` if the path is `/`. I don't+  /// think we want to, it seems clearer that this only is in the relative portion of the path,+  /// and doing so would make the setter weird.+  public var basename: FilePath.Component? {+    get {+      let (rootEnd, _) = _parseRoot()+      guard let base = self.components.last, base.slice.startIndex >= rootEnd else {+        return nil+      }+      return base+    }+    set {+      // TODO: this is assuming that pop correctly preserves root+      _ = self.pop()+      if let c = newValue { self.append(FilePath(c)) }+    }+  }++  /// Returns the path up to but not including the `basename`.+  ///+  /// If the path only contains a root, returns `self`.+  /// If the path has no root and only includes a single component,+  /// returns an empty FilePath.+  ///+  /// REVIEW NOTE: Shell returns `.` if there is no dirname, while we return the empty path.+  /// REVIEW NOTE: I removed the setter. It seems sketchy and its use indicates programmer error.+  public var dirname: FilePath {+    get {+      // No basename means path is root or empty (which is the dirname)+      if self.basename == nil { return self }+      return FilePath(self.components.dropLast())+    }+  }++  /// The extension of the file or directoy last component.+  ///+  /// If `basename` is `nil` or one of the special path components+  /// `.` or `..`, `get` returns `nil` and `set` does nothing.+  ///+  /// If `basename` does not contain a `.` anywhere, or only+  /// at the start, `get` returns `nil` and `set` will append a+  /// `.` and `newValue` to `basename`+  ///+  /// Otherwise `get` returns everyting after it, and `set` will+  /// replace the extension+  ///+  /// Examples:+  ///   * `/tmp/foo.txt => "txt"`+  ///   * `/Appliations/Foo.app/ => app`+  ///   * `/Appliations/Foo.app/bar.txt => txt`+  ///   * `/tmp/.hidden => nil``+  ///   * `/tmp/.. => nil``+  ///+  /// REVIEW NOTE: Rust has a set_extension instead, which returns whether it+  /// did anything, but having a setter is more convenient.+  public var `extension`: String? {+    get {+      fatalError()+    }+    set { fatalError() }+  }++  /// The non-extension portion of the file or directoy last component.+  ///+  /// Returns `nil` if `basename` is `nil`+  ///+  ///   * `/tmp/foo.txt => "foo"`+  ///   * `/Appliations/Foo.app/ => Foo`+  ///   * `/Appliations/Foo.app/bar.txt => bar`+  ///   * `/tmp/.hidden => .hidden``+  ///   * `/tmp/.. => ..``+  ///   * `/ => nil`+  ///+  /// TODO: setter? That's a little harder to imagine than for `extension`.+  /// Setting to `nil` is more likely a bug.+  public var stem: String? {

Since it's only the root that doesn't have a stem, it feels a little odd to have this be optional, because of the effect on the callsites.

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {+    get {+      let rootEnd = _parseRoot().0+      guard rootEnd != storage.startIndex else { return nil }+      return Component(self, ..<rootEnd)+    }+    set {+      fatalError()+    }+  }++  /// REVIEW NOTE: I'm dropping rootName and rootDirectory. I think+  /// C++ was a little misguided here after looking more at C#. I think+  /// a better alternative would be to actually identify and work with+  /// the root when on Windows, more akin to Rust's `Prefix` type.++  /// Gets or sets the relative portion of the path (everything after root)+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => foo/bar`+  ///   * `foo/bar => foo/bar`+  ///   * `/ => ""`+  /// * Windows:+  ///   * `C:\foo\bar => foo\bar`+  ///   * `foo\bar => foo\bar`+  ///   * `\\?\UNC\server\share\file => file`+  ///   * `\ => ""`+  ///+  /// REVIEW NOTE: Not optional, can return empty paths. Empty paths+  /// are relative (but are not roots), hence non-optional.+  public var relativePath: FilePath {+    get {+      let (_, relIdx) = _parseRoot()+      return FilePath(slice: storage[relIdx...])+    }+    set { fatalError() }+  }++  /// Returns the final component of the path. If the path is a normal file, this+  /// will be the file name. If the path is a directory, this is the directory name.+  /// Returns `nil` if the path is empty or only contains a root+  ///+  /// REVIEW NOTE: We follow  `basename` semantics, unlike `file_name` in+  /// C++ which treats trailing `/` specially, or in Rust which returns nil for `..`+  ///+  /// REVIEW NOTE: shell and C basename will return `/` if the path is `/`. I don't+  /// think we want to, it seems clearer that this only is in the relative portion of the path,+  /// and doing so would make the setter weird.+  public var basename: FilePath.Component? {+    get {+      let (rootEnd, _) = _parseRoot()+      guard let base = self.components.last, base.slice.startIndex >= rootEnd else {+        return nil+      }+      return base+    }+    set {+      // TODO: this is assuming that pop correctly preserves root+      _ = self.pop()+      if let c = newValue { self.append(FilePath(c)) }+    }+  }++  /// Returns the path up to but not including the `basename`.+  ///+  /// If the path only contains a root, returns `self`.+  /// If the path has no root and only includes a single component,+  /// returns an empty FilePath.+  ///+  /// REVIEW NOTE: Shell returns `.` if there is no dirname, while we return the empty path.+  /// REVIEW NOTE: I removed the setter. It seems sketchy and its use indicates programmer error.+  public var dirname: FilePath {+    get {+      // No basename means path is root or empty (which is the dirname)+      if self.basename == nil { return self }+      return FilePath(self.components.dropLast())+    }+  }++  /// The extension of the file or directoy last component.+  ///+  /// If `basename` is `nil` or one of the special path components+  /// `.` or `..`, `get` returns `nil` and `set` does nothing.+  ///+  /// If `basename` does not contain a `.` anywhere, or only+  /// at the start, `get` returns `nil` and `set` will append a+  /// `.` and `newValue` to `basename`

I think there are additional rules on some platforms, such as not treating extensions consisting only of digits as extensions (e.g. "Foo 1.0" doesn't have the extension "0"), but I'm guessing that might be a bit too specialized for this API.

milseman

comment created time in 2 months

Pull request review commentapple/swift-system

[Draft]: FilePath syntactic operations

+// Query API+// @available(...)+extension FilePath {+  /// Returns true if this path uniquely identifies the location of+  /// a file without reference to an additional starting location.+  ///+  /// On Unix platforms, absolute paths begin with a `/`. `isAbsolute` is equivalent+  /// to `root != nil`.+  ///+  /// On Windows, absolute paths are fully qualified paths. UNC paths and device paths+  /// are always absolute. Traditional DOS paths are absolute if they begin with a volume or drive+  /// followed by a `:` and a separator.+  ///+  /// NOTE: This does not perform shell expansion or substitute+  /// environment variables; paths beginning with `~` are considered relative.+  ///+  /// Examples:+  /// * Unix:+  ///   * `/usr/local/bin`+  ///   * `/tmp/foo.txt`+  ///   * `/`+  /// * Windows:+  ///   * `C:\Users\`+  ///   * `\\?\UNC\server\share\bar.exe`+  ///   * `\\server\share\bar.exe`+  ///+  /// TODO: We should add API to system to access environment variables+  /// and perform expansion/substituion, but that's deferred for now. Should+  /// we mention that?+  public var isAbsolute: Bool {+    self.root?._rootIsAbsolute ?? false+  }++  /// Returns true if this path is not absolute (see absolute)+  ///+  /// Examples:+  /// * Unix:+  ///   * `~/bar`+  ///   * `tmp/foo.txt`+  /// * Windows:+  ///   * `bar\baz`+  ///   * `C:Users\`+  ///   * `\Users`+  public var isRelative: Bool { !isAbsolute }++  /// A textual representation of the file path using the generic separator (`/`).+  ///+  /// On Unix, this is equivalent to `description`+  ///+  /// TODO: Should `dump()` use this? Also, should dump escape invalid Unicode?+  public var genericDescription: String {+    fatalError()+  }++  /// Returns whether `self` is a prefix of `other`+  public func starts(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Returns whether `self` is a suffix of `other`+  public func ends(with other: FilePath) -> Bool {+    fatalError()+  }++  /// Whether this path is empty+  public var isEmpty: Bool { self.storage == [0] }+}+++// Decompose a path into its parts+//+//   path = root? relative-path?+//   root = root-name? root-directory?+//   root-directory = separator+//   root-name (Windows only) = disk | UNC | device+extension FilePath {+  /// Returns the root directory of a path if there is one, otherwise `nil`+  ///+  /// On Unix, this will return the leading `/` if the path is absolute+  /// and `nil` if the path is relative.+  ///+  /// On Windows, this will return the path prefix up to and including the+  /// host and share for UNC paths or the volume for device paths. This will+  /// not return any subsequence separator for UNC or device paths.+  ///+  /// On Windows, for traditional DOS paths, this will return+  /// the path prefix up to and including a root directory or+  /// a supplied drive or volume. Otherwise, if the path is relative to+  /// both the current directory and current driver, returns `nil`+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => /`+  ///   * `foo/bar => nil`+  /// * Windows:+  ///   * `C:\foo\bar => C:\`+  ///   * `C:foo\bar => C:`+  ///   * `\foo\bar => \ `+  ///   * `foo\bar => nil`+  ///   * `\\?\UNC\server\share\file => \\?\UNC\server\share`+  ///+  /// TODO: Document setter. Better to document for a path what is root and what is relative.+  ///+  /// REVIEW NOTE: C# calls this the root. C++ implies they pull in the trailing separator (I+  /// haven't run tests). Rust differs in that has_root is just looking for the `\` somewhere.+  ///+  /// REVIEW NOTE: Going with UNC or device paths not including trailing separator,+  /// which is consistent with C# IIUC. I need to perform the experiement to verify this.+  ///+  /// REVIEW NOTE: I'm going with component as return type, which implies the more common+  /// use would be to read or inspect the root, rather than forming new paths from the same root.+  /// For that use case, you can do `FilePath(otherPath.root)`, or use+  /// the `relativePath`'s setter, or use the `/` operator (if we go that route).+  public var root: FilePath.Component? {+    get {+      let rootEnd = _parseRoot().0+      guard rootEnd != storage.startIndex else { return nil }+      return Component(self, ..<rootEnd)+    }+    set {+      fatalError()+    }+  }++  /// REVIEW NOTE: I'm dropping rootName and rootDirectory. I think+  /// C++ was a little misguided here after looking more at C#. I think+  /// a better alternative would be to actually identify and work with+  /// the root when on Windows, more akin to Rust's `Prefix` type.++  /// Gets or sets the relative portion of the path (everything after root)+  ///+  /// Examples:+  /// * Unix:+  ///   * `/foo/bar => foo/bar`+  ///   * `foo/bar => foo/bar`+  ///   * `/ => ""`+  /// * Windows:+  ///   * `C:\foo\bar => foo\bar`+  ///   * `foo\bar => foo\bar`+  ///   * `\\?\UNC\server\share\file => file`+  ///   * `\ => ""`+  ///+  /// REVIEW NOTE: Not optional, can return empty paths. Empty paths+  /// are relative (but are not roots), hence non-optional.+  public var relativePath: FilePath {+    get {+      let (_, relIdx) = _parseRoot()+      return FilePath(slice: storage[relIdx...])+    }+    set { fatalError() }+  }++  /// Returns the final component of the path. If the path is a normal file, this+  /// will be the file name. If the path is a directory, this is the directory name.+  /// Returns `nil` if the path is empty or only contains a root+  ///+  /// REVIEW NOTE: We follow  `basename` semantics, unlike `file_name` in+  /// C++ which treats trailing `/` specially, or in Rust which returns nil for `..`+  ///+  /// REVIEW NOTE: shell and C basename will return `/` if the path is `/`. I don't+  /// think we want to, it seems clearer that this only is in the relative portion of the path,+  /// and doing so would make the setter weird.+  public var basename: FilePath.Component? {+    get {+      let (rootEnd, _) = _parseRoot()+      guard let base = self.components.last, base.slice.startIndex >= rootEnd else {+        return nil+      }+      return base+    }+    set {+      // TODO: this is assuming that pop correctly preserves root+      _ = self.pop()+      if let c = newValue { self.append(FilePath(c)) }+    }+  }++  /// Returns the path up to but not including the `basename`.+  ///+  /// If the path only contains a root, returns `self`.+  /// If the path has no root and only includes a single component,+  /// returns an empty FilePath.

This seems like the right choice to me, but then I would expect basename to also return an empty string rather than optional.

milseman

comment created time in 2 months

more