├── .swift-version ├── .gitignore ├── Sources ├── ExitError │ ├── include │ │ └── .gitkeep │ └── exit_error_init.c └── SwiftIO │ ├── exit.swift │ ├── ProcessOptions.swift │ ├── POSIXErrorExtensions.swift │ ├── FileOutputStream.swift │ ├── print.swift │ ├── ProcessExtensions.swift │ ├── TextOutputStream+print.swift │ ├── exec.swift │ ├── system.swift │ ├── FileInfo.swift │ ├── Directory.swift │ ├── Path.swift │ ├── FileHandle.swift │ └── ExitError.swift ├── .swiftlint.yml ├── .swiftformat ├── Tests ├── Support │ ├── BundleExtensions.swift │ ├── FileHandleExtensions.swift │ ├── Assertions.swift │ └── ProcessExtensions.swift ├── exec-test │ └── main.swift ├── SwiftIOTests │ ├── systemTests.swift │ ├── execTests.swift │ ├── ExitErrorTests.swift │ └── PathTests.swift └── exit-error-test │ └── main.swift ├── Package.swift ├── LICENSE └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 5.2 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.build/ 2 | /.swiftpm/ 3 | -------------------------------------------------------------------------------- /Sources/ExitError/include/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | line_length: 2 | ignores_comments: true 3 | trailing_comma: 4 | mandatory_comma: true 5 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --ifdef outdent 2 | --indent 2 3 | --self init-only 4 | --stripunusedargs closure-only 5 | --xcodeindentation enabled 6 | -------------------------------------------------------------------------------- /Sources/SwiftIO/exit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func exit(_ code: ExitErrorCode = .success) -> Never { 4 | let status = Int32(code.rawValue) 5 | exit(status) 6 | } 7 | -------------------------------------------------------------------------------- /Sources/ExitError/exit_error_init.c: -------------------------------------------------------------------------------- 1 | extern void swiftio_init_userInfoProvider(void); 2 | 3 | __attribute__((constructor)) 4 | void 5 | _swiftio_exit_error_init_userInfoProvider(void) 6 | { 7 | swiftio_init_userInfoProvider(); 8 | } 9 | -------------------------------------------------------------------------------- /Tests/Support/BundleExtensions.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | public static var bundle: Bundle { 5 | Bundle(for: self) 6 | } 7 | 8 | public static var bundleURL: URL { 9 | bundle.bundleURL 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/SwiftIO/ProcessOptions.swift: -------------------------------------------------------------------------------- 1 | public struct ProcessOptions: OptionSet { 2 | public var rawValue: UInt 3 | 4 | public init(rawValue: UInt) { 5 | self.rawValue = rawValue 6 | } 7 | 8 | public static let requireAbsolutePath = ProcessOptions(rawValue: 1) 9 | } 10 | -------------------------------------------------------------------------------- /Tests/exec-test/main.swift: -------------------------------------------------------------------------------- 1 | import SwiftIO 2 | 3 | var arguments = CommandLine.arguments.dropFirst().makeIterator() 4 | guard let command = arguments.next() else { 5 | errorPrint("fatal: No arguments") 6 | exit(.EX_USAGE) 7 | } 8 | 9 | try exec(command, arguments: arguments) 10 | -------------------------------------------------------------------------------- /Tests/Support/FileHandleExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension FileHandle { 4 | public func readToEndOfFile() -> Data { 5 | if #available(OSX 10.15.4, *) { 6 | return (try? readToEnd()) ?? Data() 7 | } else { 8 | return readDataToEndOfFile() 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/Support/Assertions.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | public func XCTUnwrap(_ value: Value?, orThrow error: @autoclosure () -> Failure, file: StaticString = #file, line: UInt = #line) throws -> Value { 4 | try XCTUnwrap(value, "\(error().localizedDescription)", file: file, line: line) 5 | } 6 | -------------------------------------------------------------------------------- /Tests/SwiftIOTests/systemTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftIO 2 | import XCTest 3 | 4 | final class SystemTests: XCTestCase { 5 | func testItReturnsOnExitSuccess() { 6 | XCTAssertNoThrow(try system("true")) 7 | } 8 | 9 | func testItThrowsOnExitFailure() { 10 | XCTAssertThrowsError(try system("false")) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SwiftIO/POSIXErrorExtensions.swift: -------------------------------------------------------------------------------- 1 | @_exported import struct Foundation.POSIXError 2 | @_exported import enum Foundation.POSIXErrorCode 3 | import var Foundation.errno 4 | 5 | extension POSIXError { 6 | public static var errno: POSIXError { 7 | let code = POSIXErrorCode(rawValue: Foundation.errno)! 8 | return POSIXError(code) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/SwiftIO/FileOutputStream.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct FileOutputStream: TextOutputStream { 4 | var handle: UnsafeMutablePointer 5 | 6 | public func write(_ string: String) { 7 | fputs(string, handle) 8 | } 9 | } 10 | 11 | public var standardError = FileOutputStream(handle: stderr) 12 | public var standardOutput = FileOutputStream(handle: stdout) 13 | -------------------------------------------------------------------------------- /Sources/SwiftIO/print.swift: -------------------------------------------------------------------------------- 1 | typealias VPrintFunction = (Any..., String, String, inout S) -> Void 2 | typealias PrintFunction = ([Any], String, String, inout S) -> Void 3 | 4 | private let print = unsafeBitCast( 5 | Swift.print(_:separator:terminator:to:) as VPrintFunction, 6 | to: PrintFunction.self 7 | ) 8 | 9 | public func errorPrint(_ items: Any..., separator: String = " ", terminator: String = "\n") { 10 | print(items, separator, terminator, &standardError) 11 | } 12 | -------------------------------------------------------------------------------- /Sources/SwiftIO/ProcessExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Process { 4 | static func _run(_ command: URL) throws -> Process { 5 | try _run(command, arguments: []) 6 | } 7 | 8 | static func _run( 9 | _ command: URL, 10 | arguments: Arguments 11 | ) throws -> Process 12 | where Arguments.Iterator.Element == String 13 | { 14 | if #available(OSX 10.13, *) { 15 | return try run(command, arguments: arguments.map(String.init(_:))) 16 | } else { 17 | return launchedProcess(launchPath: command.path, arguments: arguments.map(String.init(_:))) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftIO", 7 | products: [ 8 | .library(name: "SwiftIO", targets: ["SwiftIO"]), 9 | ], 10 | targets: [ 11 | .target(name: "SwiftIO", dependencies: ["ExitError"]), 12 | .target(name: "ExitError"), 13 | 14 | .testTarget(name: "SwiftIOTests", dependencies: ["Support", "SwiftIO", "exec-test", "exit-error-test"]), 15 | .target(name: "exec-test", dependencies: ["SwiftIO"], path: "Tests/exec-test"), 16 | .target(name: "exit-error-test", dependencies: ["SwiftIO"], path: "Tests/exit-error-test"), 17 | .target(name: "Support", dependencies: ["SwiftIO"], path: "Tests/Support"), 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /Sources/SwiftIO/TextOutputStream+print.swift: -------------------------------------------------------------------------------- 1 | extension TextOutputStream { 2 | public mutating func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { 3 | let print = unsafeBitCast( 4 | Swift.print(_:separator:terminator:to:) as VPrintFunction, 5 | to: PrintFunction.self 6 | ) 7 | print(items, separator, terminator, &self) 8 | } 9 | 10 | public mutating func debugPrint(_ items: Any..., separator: String = " ", terminator: String = "\n") { 11 | let print = unsafeBitCast( 12 | Swift.debugPrint(_:separator:terminator:to:) as VPrintFunction, 13 | to: PrintFunction.self 14 | ) 15 | print(items, separator, terminator, &self) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/SwiftIOTests/execTests.swift: -------------------------------------------------------------------------------- 1 | import Support 2 | import XCTest 3 | 4 | final class ExecTests: XCTestCase { 5 | func testItThrowsExitError() throws { 6 | let url = ExecTests.bundleURL 7 | .deletingLastPathComponent() 8 | .appendingPathComponent("exec-test", isDirectory: false) 9 | 10 | let pipe = Pipe() 11 | let process = Process() 12 | process.executableURL = url 13 | process.arguments = ["false"] 14 | process.standardError = pipe 15 | process.standardOutput = pipe 16 | try process.run() 17 | process.waitUntilExit() 18 | let data = pipe.fileHandleForReading.readToEndOfFile() 19 | let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) 20 | 21 | XCTAssertEqual(process.terminationStatus, EXIT_FAILURE) 22 | XCTAssertEqual(output, "") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftIO/exec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func exec( 4 | _ command: String, 5 | _ arguments: String..., 6 | options: ProcessOptions = [] 7 | ) throws -> Never { 8 | try exec(command, arguments: arguments, options: options) 9 | } 10 | 11 | public func exec( 12 | _ command: String, 13 | arguments: Arguments, 14 | options: ProcessOptions = [] 15 | ) throws -> Never where Arguments.Element == String { 16 | let argv = ([command] + Array(arguments)).map { strdup($0) } 17 | defer { argv.forEach { free($0) } } 18 | 19 | let status: Int32 20 | if options.contains(.requireAbsolutePath) { 21 | status = execv(command, argv + [nil]) 22 | } else { 23 | status = execvp(command, argv + [nil]) 24 | } 25 | 26 | if status == -1 { 27 | throw POSIXError.errno 28 | } else { 29 | fatalError("Unreachable") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/exit-error-test/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftIO 3 | 4 | func main() throws { 5 | var arguments = CommandLine.arguments.dropFirst() 6 | var showDebugMessages = false 7 | guard !arguments.isEmpty else { throw ExitError(.EX_USAGE) } 8 | 9 | if let index = arguments.firstIndex(of: "--debug") ?? arguments.firstIndex(of: "-d") { 10 | showDebugMessages = true 11 | arguments.remove(at: index) 12 | } 13 | 14 | for argument in arguments { 15 | guard let code = Int32(argument) else { 16 | throw ExitError(.EX_USAGE, userInfo: [ 17 | NSLocalizedFailureReasonErrorKey: 18 | "'\(argument)' is not a valid exit code.", 19 | ]) 20 | } 21 | 22 | let error = NSError(domain: "sysexits", code: Int(code)) 23 | 24 | if showDebugMessages { 25 | print(error) 26 | } else { 27 | print(error.localizedDescription) 28 | } 29 | } 30 | } 31 | 32 | do { 33 | try main() 34 | } catch { 35 | let code = (error as? ExitError)?.code ?? .failure 36 | errorPrint("Error:", error) 37 | exit(code) 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Adam Sharp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Tests/Support/ProcessExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftIO 3 | 4 | extension Process { 5 | public static func output(fromRunning command: URL) throws -> String? { 6 | try output(fromRunning: command, arguments: []) 7 | } 8 | 9 | public static func output( 10 | fromRunning command: URL, 11 | arguments: Arguments 12 | ) throws -> String? 13 | where Arguments.Iterator.Element == String 14 | { 15 | let pipe = Pipe() 16 | let process = Process() 17 | if #available(OSX 10.13, *) { 18 | process.executableURL = command 19 | } else { 20 | process.launchPath = command.path 21 | } 22 | process.arguments = arguments.map(String.init(_:)) 23 | process.standardError = pipe 24 | process.standardOutput = pipe 25 | 26 | if #available(OSX 10.13, *) { 27 | try process.run() 28 | } else { 29 | process.launch() 30 | } 31 | process.waitUntilExit() 32 | 33 | guard process.terminationStatus == EX_OK else { 34 | throw ExitError(status: process.terminationStatus) 35 | } 36 | 37 | let data = pipe.fileHandleForReading.readToEndOfFile() 38 | return String(data: data, encoding: .utf8) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/SwiftIO/system.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func system( 4 | _ command: String, 5 | _ arguments: String..., 6 | options: ProcessOptions = [] 7 | ) throws { 8 | try system(command, arguments: arguments, options: options) 9 | } 10 | 11 | public func system( 12 | _ command: String, 13 | arguments: Arguments, 14 | options: ProcessOptions = [] 15 | ) throws where Arguments.Iterator.Element == String { 16 | let arguments = Array(arguments) 17 | let process: Process 18 | 19 | if options.contains(.requireAbsolutePath) { 20 | let command = URL(fileURLWithPath: command) 21 | process = try Process._run(command, arguments: arguments) 22 | } else { 23 | let shell = URL(fileURLWithPath: "/bin/sh") 24 | let argv = [command] + arguments 25 | let shellArguments = ["-c", "\(command) \"$@\""] + argv 26 | process = try Process._run(shell, arguments: shellArguments) 27 | } 28 | 29 | process.waitUntilExit() 30 | 31 | guard process.terminationStatus == EXIT_SUCCESS else { 32 | throw ExitError(status: process.terminationStatus, userInfo: [ 33 | ExitError.commandErrorKey: command, 34 | ExitError.argumentsErrorKey: arguments, 35 | ExitError.processErrorKey: process, 36 | ]) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/SwiftIOTests/ExitErrorTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftIO 2 | import Support 3 | import XCTest 4 | 5 | final class ExitErrorTests: XCTestCase { 6 | func testItGeneratesAFailureReasonString() throws { 7 | let url = ExecTests.bundleURL 8 | .deletingLastPathComponent() 9 | .appendingPathComponent("exit-error-test", isDirectory: false) 10 | 11 | let codes = (ExitErrorCode.EX__BASE.rawValue ... ExitErrorCode.EX__MAX.rawValue) 12 | let arguments = ["--debug"] + codes.map(String.init(describing:)) 13 | let output = try Process.output(fromRunning: url, arguments: arguments)? 14 | .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" 15 | 16 | XCTAssertEqual(output, """ 17 | Error Domain=sysexits Code=64 "The command arguments were incorrect." 18 | Error Domain=sysexits Code=65 "The input data was incorrect." 19 | Error Domain=sysexits Code=66 "No such file or directory." 20 | Error Domain=sysexits Code=67 "No such user." 21 | Error Domain=sysexits Code=68 "No such host." 22 | Error Domain=sysexits Code=69 "The service was unavailable." 23 | Error Domain=sysexits Code=70 "An internal error occurred." 24 | Error Domain=sysexits Code=71 "An internal system error occurred." 25 | Error Domain=sysexits Code=72 "An error occurred while accessing a system file." 26 | Error Domain=sysexits Code=73 "The output file could not be created." 27 | Error Domain=sysexits Code=74 "An unexpected I/O error occurred." 28 | Error Domain=sysexits Code=75 "A temporary failure occurred; try again later." 29 | Error Domain=sysexits Code=76 "The remote system returned an incorrect result." 30 | Error Domain=sysexits Code=77 "Permission denied." 31 | Error Domain=sysexits Code=78 "A configuration error occurred." 32 | """) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SwiftIO/FileInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Wraps a `struct stat` value and allows querying information about a file 4 | /// as returned by `stat(2)`. 5 | public struct FileInfo { 6 | private var info = stat() 7 | 8 | /// Create a `FileInfo` by calling `stat(2)` with the provided path. 9 | /// 10 | /// - Throws: `POSIXError.errno` if the call returns a value other than 0. 11 | public init(path: Path) throws { 12 | if stat(path.rawValue, &info) != 0 { 13 | throw POSIXError.errno 14 | } 15 | } 16 | 17 | /// Return `true` if the path references a directory. 18 | /// 19 | /// - Note: This property is equivalent to using the `S_ISDIR` macro on the 20 | /// `st_mode` field of `struct stat`. 21 | public var isDirectory: Bool { 22 | info.st_mode & S_IFMT == S_IFDIR 23 | } 24 | 25 | /// Return `true` if the path references a regular file. 26 | /// 27 | /// - Note: This property is equivalent to using the `S_ISREG` macro on the 28 | /// `st_mode` field of `struct stat`. 29 | public var isRegularFile: Bool { 30 | // S_ISREG(info.st_mode) 31 | info.st_mode & S_IFMT == S_IFREG 32 | } 33 | 34 | /// Return `true` if the path references a symbolic link. 35 | /// 36 | /// - Note: This property is equivalent to using the `S_ISLNK` macro on the 37 | /// `st_mode` field of `struct stat`. 38 | public var isSymbolicLink: Bool { 39 | info.st_mode & S_IFMT == S_IFLNK 40 | } 41 | } 42 | 43 | extension FileInfo { 44 | /// Create a `FileInfo` for the provided path and return the value of its 45 | /// `isRegularFile` property. 46 | /// 47 | /// - Returns: `true` if the path points to a regular file, or `false` for 48 | /// other kinds of file. Also returns `false` if an error occurs. 49 | public static func isRegularFile(at path: Path) -> Bool { 50 | do { 51 | return try FileInfo(path: path).isRegularFile 52 | } catch { 53 | return false 54 | } 55 | } 56 | 57 | /// Create a `FileInfo` for the provided path and return the value of its 58 | /// `isDirectory` property. 59 | /// 60 | /// - Returns: `true` if the path points to a directory, or `false` for 61 | /// other kinds of file. Also returns `false` if an error occurs. 62 | public static func isDirectory(at path: Path) -> Bool { 63 | do { 64 | return try FileInfo(path: path).isDirectory 65 | } catch { 66 | return false 67 | } 68 | } 69 | 70 | /// Create a `FileInfo` for the provided path and return the value of its 71 | /// `isSymbolicLink` property. 72 | /// 73 | /// - Returns: `true` if the path points to a symbolic link, or `false` for 74 | /// other kinds of file. Also returns `false` if an error occurs. 75 | public static func isSymbolicLink(at path: Path) -> Bool { 76 | do { 77 | return try FileInfo(path: path).isSymbolicLink 78 | } catch { 79 | return false 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/SwiftIOTests/PathTests.swift: -------------------------------------------------------------------------------- 1 | import Support 2 | import SwiftIO 3 | import XCTest 4 | 5 | final class PathTests: XCTestCase { 6 | func testExtension() { 7 | var file: Path = "/foo/bar.baz" 8 | var directory: Path = "/foo/dir.baz/" 9 | XCTAssertEqual(file.extension, "baz") 10 | XCTAssertEqual(directory.extension, "baz") 11 | 12 | file.extension = "what" 13 | XCTAssertEqual(file, "/foo/bar.what") 14 | directory.extension = "hey" 15 | XCTAssertEqual(directory, "/foo/dir.hey/") 16 | 17 | var new: Path = "blah" 18 | new.extension = "blah" 19 | XCTAssertEqual(new, "blah.blah") 20 | var newdir: Path = "dir/" 21 | newdir.extension = "ext" 22 | XCTAssertEqual(newdir, "dir.ext/") 23 | 24 | var root: Path = "/" 25 | root.extension = "notroot" 26 | XCTAssertEqual(root, "/.notroot") 27 | root.extension = "" 28 | XCTAssertEqual(root, "/.") 29 | } 30 | 31 | func testDeleteExtension() { 32 | var file: Path = "foo.txt" 33 | file.deleteExtension() 34 | XCTAssertEqual(file, "foo") 35 | file.deleteExtension() 36 | XCTAssertEqual(file, "foo") 37 | 38 | var dir: Path = "foo.build/" 39 | dir.deleteExtension() 40 | XCTAssertEqual(dir, "foo/") 41 | } 42 | 43 | func testDirectory() { 44 | var file: Path = "/tmp/foo/bar/baz.db" 45 | file = file.directory 46 | XCTAssertEqual(file, "/tmp/foo/bar") 47 | file = file.directory 48 | XCTAssertEqual(file, "/tmp/foo") 49 | file = file.directory 50 | XCTAssertEqual(file, "/tmp") 51 | file = file.directory 52 | XCTAssertEqual(file, "/") 53 | file = file.directory 54 | XCTAssertEqual(file, "/") 55 | 56 | var relative: Path = "foo/bar" 57 | relative = relative.directory 58 | XCTAssertEqual(relative, "foo") 59 | relative = relative.directory 60 | XCTAssertEqual(relative, ".") 61 | } 62 | 63 | func testBasename() { 64 | XCTAssertEqual(Path("foo/bar.txt").basename, "bar.txt") 65 | XCTAssertEqual(Path("/").basename, "/") 66 | XCTAssertEqual(Path("").basename, ".") 67 | } 68 | 69 | func testConcatenation() { 70 | XCTAssertEqual(Path("a") + "b", "a/b") 71 | XCTAssertEqual(Path("/a") + "b", "/a/b") 72 | XCTAssertEqual(Path("a") + "b/", "a/b/") 73 | XCTAssertEqual(Path("/a") + "b/", "/a/b/") 74 | XCTAssertEqual(Path("/a/") + "b", "/a/b") 75 | XCTAssertEqual(Path("/a/") + "/b", "/a/b") 76 | } 77 | 78 | func testCurrentDirectory() throws { 79 | let cwd1 = try XCTUnwrap(Directory.current, orThrow: POSIXError.errno) 80 | 81 | try Directory.make(at: "test") 82 | addTeardownBlock { 83 | try! Directory.remove("test") 84 | } 85 | 86 | try Directory.change(to: "test") { 87 | let cwd2 = try XCTUnwrap(Directory.current, orThrow: POSIXError.errno) 88 | XCTAssertEqual(cwd2, cwd1 + "test") 89 | } 90 | 91 | let cwd3 = try XCTUnwrap(Directory.current, orThrow: POSIXError.errno) 92 | XCTAssertEqual(cwd3, cwd1) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/SwiftIO/Directory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Directory { 4 | /// Returns the path of the this process's current working directory. 5 | /// 6 | /// - Returns: A `Path` holding the current directory, or `nil` for any of 7 | /// of the errors specified in `getcwd(3)`. Callers may choose to 8 | /// immediately throw `POSIXError.errno` in this case. 9 | public static var current: Path? { 10 | guard let path = getcwd(nil, 0) else { return nil } 11 | return Path(String(cString: path)) 12 | } 13 | 14 | /// Change the current directory to `path`. 15 | /// 16 | /// - Throws: `POSIXError`, for any of the errors specified in `chdir(2)`. 17 | public static func change(to path: Path) throws { 18 | if chdir(path.rawValue) != 0 { 19 | throw POSIXError.errno 20 | } 21 | } 22 | 23 | /// Executes `block` with the current directory changed to `path`, 24 | /// then restores the previous directory. 25 | /// 26 | /// - Invariant: The original directory is always restored before this 27 | /// function returns. If an I/O error or some other fault occurs, this 28 | /// function will trap rather than risk the program continuing with an 29 | /// unexpected working directory. If this behaviour is unacceptable, you 30 | /// should use the non-block-based `Directory.change(to:)` function and 31 | /// handle lower-level errors manually. 32 | /// 33 | /// - Throws: `POSIXError`, for any of the errors specified in `fopen(3)` or 34 | /// `chdir(2)`; errors thrown during execution of `block`. 35 | public static func change( 36 | to path: Path, 37 | execute block: () throws -> Result 38 | ) throws -> Result { 39 | var oldDirectory = try FileHandle.open(".") 40 | 41 | defer { 42 | do { 43 | try oldDirectory.close() 44 | } catch { 45 | preconditionFailure("Expected to close directory file handle but encountered error: \(error)") 46 | } 47 | } 48 | 49 | try change(to: path) 50 | 51 | let result = Swift.Result { 52 | try block() 53 | } 54 | 55 | if fchdir(oldDirectory.fileDescriptor) != 0 { 56 | fatalError("Error \(POSIXError.errno) while restoring previous directory.") 57 | } 58 | 59 | return try result.get() 60 | } 61 | 62 | /// Create a directory at the specified `path`. 63 | /// 64 | /// - Note: The directory is created with the base mode `a=rwx` (like 65 | /// `mkdir(1)`), and restriced by the current process's `umask(2)`. 66 | /// 67 | /// - Throws: `POSIXError`, for the errors specified in `mkdir(2)`. 68 | public static func make(at path: Path) throws { 69 | if mkdir(path.rawValue, S_IRWXU | S_IRWXG | S_IRWXO) != 0 { 70 | throw POSIXError.errno 71 | } 72 | } 73 | 74 | /// Remove the directory at the specified `path`. 75 | /// 76 | /// - Throws: `POSIXError`, for the errors specified in `rmdir(2)`. 77 | public static func remove(_ path: Path) throws { 78 | if rmdir(path.rawValue) != 0 { 79 | throw POSIXError.errno 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftIO – Simple Tools for File I/O 2 | 3 | SwiftIO aims to provide a small set of useful file input & output capabilities. 4 | Rather than invent significant new paradigms, the goal is to provide the 5 | smallest abstraction necessary to make a nice Swift API, implemented using the 6 | C standard library (or Foundation where appropriate). 7 | 8 | Another goal is portability: SwiftIO is distributed via source and Swift 9 | Package Manager, has no external dependencies, and supports Swift Package 10 | Manager's default lowest deployment target. 11 | 12 | This is a work in progress, and contributions are welcome! 13 | 14 | ## Installation 15 | 16 | Drop this package declaration into your Package.swift file: 17 | 18 | ```swift 19 | let package = Package( 20 | name: "MyPackage", 21 | dependencies: [ 22 | .package(url: "https://github.com/sharplet/SwiftIO.git", from: "0.1.1"), 23 | ], 24 | targets: [ 25 | .target(name: "MyTarget", dependencies: ["SwiftIO"]), 26 | ] 27 | ) 28 | ``` 29 | 30 | ## Usage 31 | 32 | Add the following import statement to the top of your file to use SwiftIO: 33 | 34 | ```swift 35 | import SwiftIO 36 | ``` 37 | 38 | ### `FileOutputStream` 39 | 40 | The global variables `standardOutput` and `standardError` are provided for use 41 | with the `print(_:to:)` function: 42 | 43 | ```swift 44 | print("Oh no!", to: &standardError) 45 | ``` 46 | 47 | Or use the `errorPrint(_:)` functionality, which fits in alongside the standard 48 | library's `debugPrint(_:)` function: 49 | 50 | ```swift 51 | errorPrint("Oh no!") 52 | ``` 53 | 54 | ### Launch processes and shell out 55 | 56 | Use the `system` function to run a command. If the exit status is non-zero, an 57 | error is raised. 58 | 59 | ```swift 60 | do { 61 | try system("false") 62 | } catch { 63 | print("Here we are again.") 64 | } 65 | ``` 66 | 67 | Use the `exec` function to replace the currently running process: 68 | 69 | ```swift 70 | let arguments = Array(CommandLine.arguments.dropFirst()) 71 | try exec("xcrun", arguments: ["swift", "run", "mycommand"] + arguments) 72 | ``` 73 | 74 | Both functions accept options. For example, by default the user's PATH will be 75 | searched for matching executables. Disable this behaviour like so: 76 | 77 | ```swift 78 | // exec-absolute.swift 79 | let command = CommandLine.arguments[1] 80 | try exec(command, options: .requireAbsolutePath) 81 | 82 | // exec-absolute foo 83 | // => foo: No such file or directory 84 | ``` 85 | 86 | ### Exiting the process 87 | 88 | The `exit` function is powered up with a nice enum of exit status codes, as 89 | defined in `sysexits.h`: 90 | 91 | ```swift 92 | exit(.EX_USAGE) // user error 93 | exit(.EX_SOFTWARE) // programmer error 94 | exit(.failure) // something just went wrong 95 | 96 | // these two are the same 97 | exit(.success) 98 | exit() 99 | ``` 100 | 101 | ### Working with file paths 102 | 103 | SwiftIO avoids depending on Foundation's `URL` type to represent file paths, 104 | instead providing a lightweight `Path` struct that wraps a raw `String`. 105 | 106 | ```swift 107 | // String literal support 108 | var path: Path = "/tmp/foo.txt" 109 | 110 | path.basename // "foo.txt" 111 | path.dirname // "/tmp" 112 | 113 | path.extension // "txt" 114 | path.extension = "db" // "/tmp/foo.db" 115 | path.components // ["/", "tmp", "foo.db"] 116 | 117 | // Path concatenation 118 | path.deleteExtension() 119 | path += "bar" // "/tmp/foo/bar" 120 | ``` 121 | 122 | ## Prior Art 123 | 124 | SwiftIO is fairly similar in role to [swift-tools-support-core](https://github.com/apple/swift-tools-support-core). 125 | It's more mature, and used throughout Swift tooling, so if it works for you 126 | then please use it! This library aims to be community-driven and provide 127 | unopinionated and minimal abstractions atop C. 128 | 129 | ## License 130 | 131 | SwiftIO is Copyright © 2020 Adam Sharp, and is distributed under the 132 | [MIT License](LICENSE). 133 | -------------------------------------------------------------------------------- /Sources/SwiftIO/Path.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Path: RawRepresentable { 4 | public private(set) var rawValue: String 5 | 6 | public private(set) var components: [Substring] = [] 7 | private var extensionRange: Range? 8 | 9 | public init(rawValue: String) { 10 | if rawValue.isEmpty { 11 | self.rawValue = "." 12 | } else { 13 | self.rawValue = rawValue 14 | } 15 | updateRanges() 16 | } 17 | 18 | public init(_ string: S) { 19 | self.init(rawValue: String(string)) 20 | } 21 | 22 | private init( 23 | components: C, 24 | includeTrailingSlash: Bool = false 25 | ) where C.Element == Substring { 26 | var rawValue = components.first == "/" 27 | ? "/" + components.dropFirst().joined(separator: "/") 28 | : components.joined(separator: "/") 29 | if includeTrailingSlash { 30 | rawValue += "/" 31 | } 32 | self.init(rawValue: rawValue) 33 | } 34 | 35 | public var basename: Substring { 36 | components.last ?? "." 37 | } 38 | 39 | public var directory: Path { 40 | guard let rawValue = strdup(self.rawValue) else { return self } 41 | defer { free(rawValue) } 42 | guard let dirname = dirname(rawValue) else { return self } 43 | return Path(rawValue: String(cString: dirname)) 44 | } 45 | 46 | public var `extension`: Substring { 47 | get { 48 | extensionRange.map { rawValue[$0] } ?? "" 49 | } 50 | set { 51 | if let range = extensionRange { 52 | rawValue.replaceSubrange(range, with: newValue) 53 | } else { 54 | let end = components.last?.endIndex ?? rawValue.endIndex 55 | rawValue.replaceSubrange(end ..< end, with: ".\(newValue)") 56 | } 57 | updateRanges() 58 | } 59 | } 60 | 61 | public mutating func appendComponent(_ component: S) { 62 | guard !component.isEmpty else { return } 63 | var start = rawValue.endIndex 64 | if rawValue.last != "/" { 65 | rawValue += "/" 66 | } 67 | rawValue += component.drop(while: { $0 == "/" }) 68 | rawValue.formIndex(after: &start) 69 | components.append(rawValue[start...]) 70 | extensionRange = computeExtensionRange() 71 | } 72 | 73 | public mutating func deleteExtension() { 74 | let target = components.last ?? Substring(rawValue) 75 | let end = target.endIndex 76 | let separator = target.lastIndex(of: ".") ?? target.endIndex 77 | rawValue.removeSubrange(separator ..< end) 78 | updateRanges() 79 | } 80 | } 81 | 82 | extension Path { 83 | public static func + (lhs: Path, rhs: S) -> Path { 84 | lhs + Path(rhs) 85 | } 86 | 87 | public static func + (lhs: Path, rhs: Path) -> Path { 88 | var lhs = lhs 89 | lhs += rhs 90 | return lhs 91 | } 92 | 93 | public static func += (lhs: inout Path, rhs: S) { 94 | lhs += Path(rhs) 95 | } 96 | 97 | public static func += (lhs: inout Path, rhs: Path) { 98 | for component in rhs.components where component != "/" { 99 | lhs.appendComponent(component) 100 | } 101 | if rhs.rawValue.hasSuffix("/") { 102 | lhs.rawValue += "/" 103 | } 104 | } 105 | } 106 | 107 | private extension Path { 108 | mutating func updateRanges() { 109 | components = computeComponents() 110 | extensionRange = computeExtensionRange() 111 | } 112 | 113 | private func computeComponents() -> [Substring] { 114 | var components = rawValue.split(separator: "/", omittingEmptySubsequences: true) 115 | if let range = rawValue.range(of: "/", options: .anchored) { 116 | components.insert(rawValue[range], at: 0) 117 | } 118 | return components 119 | } 120 | 121 | private mutating func computeExtensionRange() -> Range? { 122 | guard let basename = components.last, 123 | var separator = basename.lastIndex(of: ".") 124 | else { return nil } 125 | 126 | _ = basename.formIndex( 127 | &separator, 128 | offsetBy: 1, 129 | limitedBy: basename.endIndex 130 | ) 131 | 132 | return separator ..< basename.endIndex 133 | } 134 | } 135 | 136 | extension Path: CustomStringConvertible { 137 | public var description: String { 138 | rawValue 139 | } 140 | } 141 | 142 | extension Path: CustomDebugStringConvertible { 143 | public var debugDescription: String { 144 | "Path(\(rawValue))" 145 | } 146 | } 147 | 148 | extension Path: Equatable { 149 | public static func == (lhs: Path, rhs: Path) -> Bool { 150 | lhs.rawValue == rhs.rawValue 151 | } 152 | } 153 | 154 | extension Path: ExpressibleByStringLiteral { 155 | public init(stringLiteral value: StringLiteralType) { 156 | self.init(rawValue: value) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Sources/SwiftIO/FileHandle.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct FileHandle { 4 | public struct Options: OptionSet { 5 | public var rawValue: UInt 6 | 7 | public init(rawValue: UInt) { 8 | self.rawValue = rawValue 9 | } 10 | 11 | public static let binary = Options(rawValue: 1 << 0) 12 | } 13 | 14 | public static func open( 15 | _ path: Path, 16 | mode: Mode = .read, 17 | options: Options = [] 18 | ) throws -> FileHandle { 19 | var mode = mode.rawValue 20 | if options.contains(.binary) { 21 | mode += "b" 22 | } 23 | guard let handle = fopen(path.rawValue, mode) else { 24 | throw POSIXError.errno 25 | } 26 | return FileHandle(handle: handle) 27 | } 28 | 29 | public static func open( 30 | _ path: Path, 31 | mode: Mode = .read, 32 | options: Options = [], 33 | fileHandler: (inout FileHandle) throws -> Result 34 | ) throws -> Result { 35 | var file = try open(path, mode: mode, options: options) 36 | do { 37 | let result = try fileHandler(&file) 38 | try file.closeIfNeeded() 39 | return result 40 | } catch { 41 | try file.closeIfNeeded() 42 | throw error 43 | } 44 | } 45 | 46 | private let handle: UnsafeMutablePointer 47 | private var isKnownClosed = false 48 | private var isKnownInvalid = false 49 | 50 | /// The file descripter owned by this file handle object. 51 | /// 52 | /// - SeeAlso: `fileno(3)` 53 | public var fileDescriptor: Int32 { 54 | fileno(handle) 55 | } 56 | 57 | public var writeErrorHandler: ((POSIXError) -> Void)? 58 | 59 | public mutating func close() throws { 60 | precondition(!isKnownClosed) 61 | 62 | defer { isKnownInvalid = true } 63 | 64 | if fclose(handle) == 0 { 65 | isKnownClosed = true 66 | } else { 67 | throw POSIXError.errno 68 | } 69 | } 70 | 71 | private mutating func closeIfNeeded() throws { 72 | guard !isKnownClosed else { return } 73 | try close() 74 | } 75 | 76 | public func readLine(strippingNewline: Bool = true) throws -> String? { 77 | precondition(!isKnownInvalid, "Attempted to read an invalid file stream.") 78 | 79 | var count = 0 80 | guard let pointer = fgetln(handle, &count) else { 81 | if ferror(handle) != 0 { 82 | throw POSIXError.errno 83 | } else { 84 | return nil 85 | } 86 | } 87 | 88 | let utf8 = UnsafeRawPointer(pointer).assumingMemoryBound(to: UInt8.self) 89 | let buffer = UnsafeBufferPointer(start: utf8, count: count) 90 | var end = buffer.endIndex 91 | 92 | if strippingNewline { 93 | strip: while end > buffer.startIndex { 94 | let index = buffer.index(before: end) 95 | switch Unicode.Scalar(buffer[index]) { 96 | case "\r", "\n": 97 | end = index 98 | default: 99 | break strip 100 | } 101 | } 102 | } 103 | 104 | return String(decoding: buffer[..(_ bytes: Bytes) throws where Bytes.Element == UInt8 { 108 | var count = 0 109 | 110 | var countWritten = bytes.withContiguousStorageIfAvailable { buffer -> Int in 111 | count = buffer.count 112 | precondition(count > 0) 113 | return fwrite(buffer.baseAddress, 1, count, handle) 114 | } 115 | 116 | if countWritten == nil { 117 | let array = Array(bytes) 118 | count = array.count 119 | precondition(count > 0) 120 | countWritten = fwrite(array, 1, count, handle) 121 | } 122 | 123 | if countWritten != count { 124 | throw POSIXError.errno 125 | } 126 | } 127 | } 128 | 129 | extension FileHandle { 130 | public enum Mode: String { 131 | case read = "r" 132 | case readWrite = "r+" 133 | case truncate = "w" 134 | case readTruncate = "w+" 135 | case new = "wx" 136 | case readWriteNew = "w+x" 137 | case append = "a" 138 | case readAppend = "a+" 139 | } 140 | } 141 | 142 | extension FileHandle { 143 | public mutating func read(_: I.Type) throws -> I? { 144 | var i = I.zero 145 | let size = MemoryLayout.size(ofValue: i) 146 | precondition(size != 0, "fread(3): cannot read value of size 0") 147 | if fread(&i, size, 1, handle) < 1 { 148 | if ferror(handle) > 0 { 149 | throw POSIXError.errno 150 | } else { 151 | precondition(feof(handle) > 0, "fread(3): read fewer than expected items before EOF") 152 | return nil 153 | } 154 | } else { 155 | return i 156 | } 157 | } 158 | } 159 | 160 | extension FileHandle: TextOutputStream { 161 | public func write(_ string: String) { 162 | precondition(!isKnownInvalid, "Attempted to write to an invalid file stream.") 163 | if fputs(string, handle) == EOF, let errorHandler = writeErrorHandler { 164 | errorHandler(.errno) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Sources/SwiftIO/ExitError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct ExitErrorCode: RawRepresentable { 4 | public var rawValue: Int32 5 | 6 | public init(rawValue: Int32) { 7 | self.rawValue = rawValue 8 | } 9 | 10 | /// Successful termination 11 | public static var EX_OK: ExitErrorCode { return ExitErrorCode(rawValue: 0) } 12 | 13 | public static var EX__BASE: ExitErrorCode { return .EX_USAGE } 14 | 15 | /// Command line usage error 16 | public static var EX_USAGE: ExitErrorCode { return ExitErrorCode(rawValue: 64) } 17 | /// Data format error 18 | public static var EX_DATAERR: ExitErrorCode { return ExitErrorCode(rawValue: 65) } 19 | /// Cannot open input 20 | public static var EX_NOINPUT: ExitErrorCode { return ExitErrorCode(rawValue: 66) } 21 | /// Addressee unknown 22 | public static var EX_NOUSER: ExitErrorCode { return ExitErrorCode(rawValue: 67) } 23 | /// Host name unknown 24 | public static var EX_NOHOST: ExitErrorCode { return ExitErrorCode(rawValue: 68) } 25 | /// Service unavailable 26 | public static var EX_UNAVAILABLE: ExitErrorCode { return ExitErrorCode(rawValue: 69) } 27 | /// Internal software error 28 | public static var EX_SOFTWARE: ExitErrorCode { return ExitErrorCode(rawValue: 70) } 29 | /// System error (e.g., can't fork) 30 | public static var EX_OSERR: ExitErrorCode { return ExitErrorCode(rawValue: 71) } 31 | /// Critical OS file missing 32 | public static var EX_OSFILE: ExitErrorCode { return ExitErrorCode(rawValue: 72) } 33 | /// Can't create (user) output file 34 | public static var EX_CANTCREAT: ExitErrorCode { return ExitErrorCode(rawValue: 73) } 35 | /// Input/output error 36 | public static var EX_IOERR: ExitErrorCode { return ExitErrorCode(rawValue: 74) } 37 | /// Temp failure; user is invited to retry 38 | public static var EX_TEMPFAIL: ExitErrorCode { return ExitErrorCode(rawValue: 75) } 39 | /// Remote error in protocol 40 | public static var EX_PROTOCOL: ExitErrorCode { return ExitErrorCode(rawValue: 76) } 41 | /// Permission denied 42 | public static var EX_NOPERM: ExitErrorCode { return ExitErrorCode(rawValue: 77) } 43 | /// Configuration error 44 | public static var EX_CONFIG: ExitErrorCode { return ExitErrorCode(rawValue: 78) } 45 | 46 | public static var EX__MAX: ExitErrorCode { return .EX_CONFIG } 47 | } 48 | 49 | extension ExitErrorCode { 50 | public static var success: ExitErrorCode { return .EXIT_SUCCESS } 51 | public static var failure: ExitErrorCode { return .EXIT_FAILURE } 52 | 53 | public static var EXIT_SUCCESS: ExitErrorCode { return .EX_OK } 54 | public static var EXIT_FAILURE: ExitErrorCode { return ExitErrorCode(rawValue: 1) } 55 | } 56 | 57 | public struct ExitError: _BridgedStoredNSError { 58 | public static let errorDomain = "sysexits" 59 | 60 | public let _nsError: NSError 61 | 62 | public init(_nsError error: NSError) { 63 | precondition(error.domain == ExitError.errorDomain) 64 | self._nsError = error 65 | } 66 | 67 | public static var _nsErrorDomain: String { return ExitError.errorDomain } 68 | 69 | public typealias Code = ExitErrorCode 70 | } 71 | 72 | extension ExitErrorCode: _ErrorCodeProtocol { 73 | public typealias _ErrorType = ExitError 74 | } 75 | 76 | extension ExitError { 77 | /// Successful termination 78 | public static var EX_OK: ExitErrorCode { return .EX_OK } 79 | /// Command line usage error 80 | public static var EX_USAGE: ExitErrorCode { return .EX_USAGE } 81 | /// Data format error 82 | public static var EX_DATAERR: ExitErrorCode { return .EX_DATAERR } 83 | /// Cannot open input 84 | public static var EX_NOINPUT: ExitErrorCode { return .EX_NOINPUT } 85 | /// Addressee unknown 86 | public static var EX_NOUSER: ExitErrorCode { return .EX_NOUSER } 87 | /// Host name unknown 88 | public static var EX_NOHOST: ExitErrorCode { return .EX_NOHOST } 89 | /// Service unavailable 90 | public static var EX_UNAVAILABLE: ExitErrorCode { return .EX_UNAVAILABLE } 91 | /// Internal software error 92 | public static var EX_SOFTWARE: ExitErrorCode { return .EX_SOFTWARE } 93 | /// System error (e.g., can't fork) 94 | public static var EX_OSERR: ExitErrorCode { return .EX_OSERR } 95 | /// Critical OS file missing 96 | public static var EX_OSFILE: ExitErrorCode { return .EX_OSFILE } 97 | /// Can't create (user) output file 98 | public static var EX_CANTCREAT: ExitErrorCode { return .EX_CANTCREAT } 99 | /// Input/output error 100 | public static var EX_IOERR: ExitErrorCode { return .EX_IOERR } 101 | /// Temp failure; user is invited to retry 102 | public static var EX_TEMPFAIL: ExitErrorCode { return .EX_TEMPFAIL } 103 | /// Remote error in protocol 104 | public static var EX_PROTOCOL: ExitErrorCode { return .EX_PROTOCOL } 105 | /// Permission denied 106 | public static var EX_NOPERM: ExitErrorCode { return .EX_NOPERM } 107 | /// Configuration error 108 | public static var EX_CONFIG: ExitErrorCode { return .EX_CONFIG } 109 | } 110 | 111 | extension ExitError { 112 | public static let argumentsErrorKey = "Arguments" 113 | public static let commandErrorKey = "Command" 114 | public static let processErrorKey = "Process" 115 | 116 | public init(status: Int32, userInfo: [String: Any]? = nil) { 117 | let code = ExitErrorCode(rawValue: status) 118 | if let userInfo = userInfo { 119 | self.init(code, userInfo: userInfo) 120 | } else { 121 | self.init(code) 122 | } 123 | } 124 | } 125 | 126 | extension ExitError: CustomStringConvertible { 127 | public var description: String { 128 | return String(describing: _nsError) 129 | } 130 | } 131 | 132 | #if swift(>=4) 133 | private typealias UserInfoProvider = (Error, String) -> Any? 134 | #else 135 | private typealias UserInfoProvider = (NSError, String) -> NSString? 136 | #endif 137 | 138 | @_cdecl("swiftio_init_userInfoProvider") 139 | internal func initializeUserInfoProvider() { 140 | guard #available(macOS 11.0, *) else { return } 141 | NSError.setUserInfoValueProvider( 142 | forDomain: ExitError.errorDomain, 143 | provider: { error, key in 144 | guard key == NSLocalizedDescriptionKey else { return nil } 145 | switch error { 146 | case ExitError.EX_USAGE: 147 | return "The command arguments were incorrect." 148 | case ExitError.EX_DATAERR: 149 | return "The input data was incorrect." 150 | case ExitError.EX_NOINPUT: 151 | return "No such file or directory." 152 | case ExitError.EX_NOUSER: 153 | return "No such user." 154 | case ExitError.EX_NOHOST: 155 | return "No such host." 156 | case ExitError.EX_UNAVAILABLE: 157 | return "The service was unavailable." 158 | case ExitError.EX_SOFTWARE: 159 | return "An internal error occurred." 160 | case ExitError.EX_OSERR: 161 | return "An internal system error occurred." 162 | case ExitError.EX_OSFILE: 163 | return "An error occurred while accessing a system file." 164 | case ExitError.EX_CANTCREAT: 165 | return "The output file could not be created." 166 | case ExitError.EX_IOERR: 167 | return "An unexpected I/O error occurred." 168 | case ExitError.EX_TEMPFAIL: 169 | return "A temporary failure occurred; try again later." 170 | case ExitError.EX_PROTOCOL: 171 | return "The remote system returned an incorrect result." 172 | case ExitError.EX_NOPERM: 173 | return "Permission denied." 174 | case ExitError.EX_CONFIG: 175 | return "A configuration error occurred." 176 | default: 177 | return nil 178 | } 179 | } as UserInfoProvider 180 | ) 181 | } 182 | --------------------------------------------------------------------------------