├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .spi.yml ├── .swift-format ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── SwiftCommand │ ├── AsyncCharacterSequence.swift │ ├── AsyncLineSequence.swift │ ├── AsyncUnicodeScalarSequence.swift │ ├── ChildProcess.swift │ ├── Command.swift │ ├── Either.swift │ ├── ExitStatus.swift │ ├── FileHandle+Async.swift │ ├── FilePath+extension.swift │ ├── InputSource.swift │ ├── OutputDestination.swift │ ├── OutputType.swift │ └── ProcessOutput.swift └── Tests └── SwiftCommandTests └── SwiftCommandTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build_macos: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | build_ubuntu: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Build 27 | run: swift build -v 28 | - name: Run tests 29 | run: swift test -v 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | .vscode 11 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [SwiftCommand] 5 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "lineLength": 80, 4 | "tabWidth": 4, 5 | "maximumBlankLines": 1, 6 | "respectsExistingLineBreaks": true, 7 | "lineBreakBeforeControlFlowKeywords": false, 8 | "lineBreakBeforeEachArgument": true, 9 | "lineBreakBeforeEachGenericRequirement": true, 10 | "prioritizeKeepingFunctionOutputTogether": true, 11 | "indentConditionalCompilationBlocks": false, 12 | "lineBreakAroundMultilineExpressionChainComponents": true, 13 | "spacesAroundRangeFormationOperators": false, 14 | "multiElementCollectionTrailingCommas": true, 15 | "indentation": { 16 | "spaces": 4 17 | }, 18 | "fileScopedDeclarationPrivacy": { 19 | "accessLevel": "private" 20 | }, 21 | "rules": { 22 | "AllPublicDeclarationsHaveDocumentation": true, 23 | "AlwaysUseLiteralForEmptyCollectionInit": true, 24 | "AlwaysUseLowerCamelCase": true, 25 | "AmbiguousTrailingClosureOverload": true, 26 | "BeginDocumentationCommentWithOneLineSummary": true, 27 | "DoNotUseSemicolons": true, 28 | "DontRepeatTypeInStaticProperties": true, 29 | "FileScopedDeclarationPrivacy": true, 30 | "FullyIndirectEnum": true, 31 | "GroupNumericLiterals": true, 32 | "IdentifiersMustBeASCII": true, 33 | "NeverForceUnwrap": false, 34 | "NeverUseForceTry": false, 35 | "NeverUseImplicitlyUnwrappedOptionals": false, 36 | "NoAccessLevelOnExtensionDeclaration": true, 37 | "NoAssignmentInExpressions": true, 38 | "NoBlockComments": true, 39 | "NoCasesWithOnlyFallthrough": true, 40 | "NoEmptyTrailingClosureParentheses": true, 41 | "NoLabelsInCasePatterns": true, 42 | "NoLeadingUnderscores": false, 43 | "NoParensAroundConditions": true, 44 | "NoPlaygroundLiterals": true, 45 | "NoVoidReturnOnFunctionSignature": true, 46 | "OmitExplicitReturns": false, 47 | "OneCasePerLine": true, 48 | "OneVariableDeclarationPerLine": true, 49 | "OnlyOneTrailingClosureArgument": true, 50 | "OrderedImports": true, 51 | "ReplaceForEachWithForLoop": true, 52 | "ReturnVoidInsteadOfEmptyTuple": true, 53 | "TypeNamesShouldBeCapitalized": true, 54 | "UseEarlyExits": true, 55 | "UseExplicitNilCheckInConditions": true, 56 | "UseLetInEveryBoundCaseVariable": true, 57 | "UseShorthandTypeNames": true, 58 | "UseSingleLinePropertyGetter": true, 59 | "UseSynthesizedInitializer": true, 60 | "UseTripleSlashForDocumentationComments": true, 61 | "UseWhereClausesInForLoops": true, 62 | "ValidateDocumentationComments": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Josef Zoller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-async-algorithms", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-async-algorithms", 7 | "state" : { 8 | "revision" : "da4e36f86544cdf733a40d59b3a2267e3a7bbf36", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-collections", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-collections.git", 16 | "state" : { 17 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", 18 | "version" : "1.0.4" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-system", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-system", 25 | "state" : { 26 | "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", 27 | "version" : "1.2.1" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftCommand", 8 | platforms: [ 9 | .macOS(.v10_15) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "SwiftCommand", 15 | targets: ["SwiftCommand"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | .package(url: "https://github.com/apple/swift-system", from: "1.0.0"), 21 | .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "SwiftCommand", 28 | dependencies: [ 29 | .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), 30 | .product(name: "SystemPackage", package: "swift-system"), 31 | ] 32 | ), 33 | .testTarget( 34 | name: "SwiftCommandTests", 35 | dependencies: [ 36 | "SwiftCommand", 37 | .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), 38 | ] 39 | ), 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftCommand 2 | 3 | ![Platforms: macOS/Linux/Windows\*](https://img.shields.io/badge/Platforms-macOS%20%7C%20Linux%20%7C%20Windows%2A-F05138) 4 | [![Supported swift versions](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FZollerboy1%2FSwiftCommand%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Zollerboy1/SwiftCommand) 5 | 6 | *\*Windows support is only experimental for now.* 7 | 8 | --- 9 | 10 | A wrapper around `Foundation.Process`, inspired by Rust's 11 | `std::process::Command`. This package makes it easy to call command line 12 | programs and handle their I/O. 13 | 14 | ## Installation 15 | 16 | You can install this package using the Swift Package Manager, by including it in 17 | the dependencies of your package: 18 | 19 | ```swift 20 | let package = Package( 21 | // ... 22 | dependencies: [ 23 | // other dependencies... 24 | .package( 25 | url: "https://github.com/Zollerboy1/SwiftCommand.git", 26 | from: "1.4.0" 27 | ), 28 | ], 29 | // ... 30 | ) 31 | ``` 32 | 33 | ## Usage 34 | 35 | Using this package is very easy. 36 | 37 | Before you start, make sure that you've imported the `SwiftCommand` module: 38 | 39 | ```swift 40 | import SwiftCommand 41 | ``` 42 | 43 | Now it can be used like this: 44 | 45 | ```swift 46 | let output = try Command.findInPath(withName: "echo")! 47 | .addArgument("Foo") 48 | .waitForOutput() 49 | 50 | print(output.stdout) 51 | // Prints 'Foo\n' 52 | ``` 53 | 54 | This blocks the thread until the command terminates. You can use the 55 | `async`/`await` API instead, if you want to do other work while waiting for the 56 | command to terminate: 57 | 58 | ```swift 59 | let output = try await Command.findInPath(withName: "echo")! 60 | .addArgument("Foo") 61 | .output 62 | 63 | print(output.stdout) 64 | // Prints 'Foo\n' 65 | ``` 66 | 67 | ### Specifying command I/O 68 | 69 | Suppose that you have a file called `SomeFile.txt` that looks like this: 70 | 71 | ``` 72 | Foo 73 | Bar 74 | Baz 75 | ``` 76 | 77 | You can then set stdin and stdout of commands like this: 78 | 79 | ```swift 80 | let catProcess = try Command.findInPath(withName: "cat")! 81 | .setStdin(.read(fromFile: "SomeFile.txt")) 82 | .setStdout(.pipe) 83 | .spawn() 84 | 85 | let grepProcess = try Command.findInPath(withName: "grep")! 86 | .addArgument("Ba") 87 | .setStdin(.pipe(from: catProcess.stdout)) 88 | .setStdout(.pipe) 89 | .spawn() 90 | 91 | for try await line in grepProcess.stdout.lines { 92 | print(line) 93 | } 94 | // Prints 'Bar' and 'Baz' 95 | 96 | try catProcess.wait() 97 | try grepProcess.wait() 98 | // Ensure the processes are terminated before exiting the parent process 99 | ``` 100 | 101 | This is doing in Swift, what you would normally write in a terminal like this: 102 | 103 | ```bash 104 | cat < SomeFile.txt | grep Ba 105 | ``` 106 | 107 | If you don't specify stdin, stdout, or stderr, and also don't capture the output 108 | (using e.g. `waitForOutput()`), then they will by default inherit the 109 | corresponding handle of the parent process. E.g. the stdout of the following 110 | program is `Bar\n`: 111 | 112 | ```swift 113 | import SwiftCommand 114 | 115 | try Command.findInPath(withName: "echo")! 116 | .addArgument("Bar") 117 | .wait() 118 | ``` 119 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/AsyncCharacterSequence.swift: -------------------------------------------------------------------------------- 1 | /// A non-blocking sequence of `Character`s created by decoding the elements of 2 | /// `Base` as utf-8. 3 | public struct AsyncCharacterSequence: AsyncSequence 4 | where Base: AsyncSequence, Base.Element == UInt8 { 5 | @usableFromInline 6 | internal typealias Underlying = AsyncUnicodeScalarSequence 7 | 8 | /// The type of element produced by this asynchronous sequence. 9 | public typealias Element = Character 10 | 11 | /// The type of asynchronous iterator that produces elements of this 12 | /// asynchronous sequence. 13 | public struct AsyncIterator: AsyncIteratorProtocol { 14 | @usableFromInline 15 | internal var _remaining: Underlying.AsyncIterator 16 | @usableFromInline 17 | internal var _accumulator: String 18 | 19 | fileprivate init(_underlying underlying: Underlying.AsyncIterator) { 20 | self._remaining = underlying 21 | self._accumulator = "" 22 | } 23 | 24 | /// Asynchronously advances to the next element and returns it, or ends 25 | /// the sequence if there is no next element. 26 | /// 27 | /// - Returns: The next element, if it exists, or `nil` to signal the 28 | /// end of the sequence. 29 | @inlinable 30 | public mutating func next() async rethrows -> Character? { 31 | while let scalar = try await self._remaining.next() { 32 | self._accumulator.unicodeScalars.append(scalar) 33 | if self._accumulator.count > 1 { 34 | return self._accumulator.removeFirst() 35 | } 36 | } 37 | 38 | guard self._accumulator.count > 0 else { 39 | return nil 40 | } 41 | 42 | return self._accumulator.removeFirst() 43 | } 44 | } 45 | 46 | private let underlying: Underlying 47 | 48 | internal init(_base base: Base) { 49 | self.underlying = .init(_base: base) 50 | } 51 | 52 | /// Creates the asynchronous iterator that produces elements of this 53 | /// asynchronous sequence. 54 | /// 55 | /// - Returns: An instance of the `AsyncIterator` type used to produce 56 | /// elements of the asynchronous sequence. 57 | public func makeAsyncIterator() -> AsyncIterator { 58 | return AsyncIterator(_underlying: self.underlying.makeAsyncIterator()) 59 | } 60 | } 61 | 62 | extension AsyncSequence where Self.Element == UInt8 { 63 | /// A non-blocking sequence of `Character`s created by decoding the 64 | /// elements of `self` as utf-8. 65 | public var characterSequence: AsyncCharacterSequence { 66 | AsyncCharacterSequence(_base: self) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/AsyncLineSequence.swift: -------------------------------------------------------------------------------- 1 | /// A non-blocking sequence of newline-separated `String`s created by decoding 2 | /// the elements of `Base` as utf-8. 3 | public struct AsyncLineSequence: AsyncSequence 4 | where Base: AsyncSequence, Base.Element == UInt8 { 5 | /// The type of element produced by this asynchronous sequence. 6 | public typealias Element = String 7 | 8 | /// The type of asynchronous iterator that produces elements of this 9 | /// asynchronous sequence. 10 | public struct AsyncIterator: AsyncIteratorProtocol { 11 | public typealias Element = String 12 | 13 | @usableFromInline 14 | internal var _base: Base.AsyncIterator 15 | @usableFromInline 16 | internal var _buffer: [UInt8] 17 | @usableFromInline 18 | internal var _leftover: UInt8? 19 | 20 | internal init(_base base: Base.AsyncIterator) { 21 | self._base = base 22 | self._buffer = [] 23 | self._leftover = nil 24 | } 25 | 26 | /// Asynchronously advances to the next element and returns it, or ends 27 | /// the sequence if there is no next element. 28 | /// 29 | /// - Returns: The next element, if it exists, or `nil` to signal the 30 | /// end of the sequence. 31 | @inlinable 32 | public mutating func next() async rethrows -> String? { 33 | /* 34 | 0D 0A: CR-LF 35 | 0A | 0B | 0C | 0D: LF, VT, FF, CR 36 | E2 80 A8: U+2028 (LINE SEPARATOR) 37 | E2 80 A9: U+2029 (PARAGRAPH SEPARATOR) 38 | */ 39 | let _CR: UInt8 = 0x0D 40 | let _LF: UInt8 = 0x0A 41 | let _NEL_PREFIX: UInt8 = 0xC2 42 | let _NEL_SUFFIX: UInt8 = 0x85 43 | let _SEPARATOR_PREFIX: UInt8 = 0xE2 44 | let _SEPARATOR_CONTINUATION: UInt8 = 0x80 45 | let _SEPARATOR_SUFFIX_LINE: UInt8 = 0xA8 46 | let _SEPARATOR_SUFFIX_PARAGRAPH: UInt8 = 0xA9 47 | 48 | func yield() -> String? { 49 | defer { 50 | self._buffer.removeAll(keepingCapacity: true) 51 | } 52 | 53 | if self._buffer.isEmpty { 54 | return nil 55 | } 56 | 57 | return String(decoding: self._buffer, as: UTF8.self) 58 | } 59 | 60 | func nextByte() async throws -> UInt8? { 61 | defer { 62 | self._leftover = nil 63 | } 64 | 65 | if let leftover = self._leftover { 66 | return leftover 67 | } 68 | 69 | return try await self._base.next() 70 | } 71 | 72 | while let first = try await nextByte() { 73 | switch first { 74 | case _CR: 75 | let result = yield() 76 | // Swallow up any subsequent LF 77 | guard let next = try await self._base.next() else { 78 | // if we ran out of bytes, the last byte was a CR 79 | return result 80 | } 81 | 82 | if next != _LF { 83 | self._leftover = next 84 | } 85 | 86 | if let result = result { 87 | return result 88 | } 89 | 90 | continue 91 | case _LF..<_CR: 92 | guard let result = yield() else { 93 | continue 94 | } 95 | 96 | return result 97 | case _NEL_PREFIX: 98 | // this may be used to compose other UTF8 characters 99 | guard let next = try await self._base.next() else { 100 | // technically invalid UTF8 but it should be repaired 101 | // to "\u{FFFD}" 102 | self._buffer.append(first) 103 | return yield() 104 | } 105 | 106 | guard next != _NEL_SUFFIX else { 107 | guard let result = yield() else { 108 | continue 109 | } 110 | 111 | return result 112 | } 113 | 114 | self._buffer.append(first) 115 | self._buffer.append(next) 116 | case _SEPARATOR_PREFIX: 117 | // Try to read: 80 [A8 | A9]. 118 | // If we can't, then we put the byte in the buffer for 119 | // error correction 120 | guard let next = try await self._base.next() else { 121 | self._buffer.append(first) 122 | return yield() 123 | } 124 | 125 | guard next == _SEPARATOR_CONTINUATION else { 126 | self._buffer.append(first) 127 | self._buffer.append(next) 128 | continue 129 | } 130 | 131 | guard let fin = try await self._base.next() else { 132 | self._buffer.append(first) 133 | self._buffer.append(next) 134 | return yield() 135 | } 136 | 137 | guard 138 | fin == _SEPARATOR_SUFFIX_LINE 139 | || fin == _SEPARATOR_SUFFIX_PARAGRAPH 140 | else { 141 | self._buffer.append(first) 142 | self._buffer.append(next) 143 | self._buffer.append(fin) 144 | continue 145 | } 146 | 147 | if let result = yield() { 148 | return result 149 | } 150 | 151 | continue 152 | default: 153 | self._buffer.append(first) 154 | } 155 | } 156 | // Don't emit an empty newline when there is no more content 157 | // (e.g. end of file) 158 | if !self._buffer.isEmpty { 159 | return yield() 160 | } 161 | 162 | return nil 163 | } 164 | 165 | } 166 | 167 | private let base: Base 168 | 169 | internal init(_base base: Base) { 170 | self.base = base 171 | } 172 | 173 | /// Creates the asynchronous iterator that produces elements of this 174 | /// asynchronous sequence. 175 | /// 176 | /// - Returns: An instance of the `AsyncIterator` type used to produce 177 | /// elements of the asynchronous sequence. 178 | public func makeAsyncIterator() -> AsyncIterator { 179 | return AsyncIterator(_base: self.base.makeAsyncIterator()) 180 | } 181 | } 182 | 183 | extension AsyncSequence where Self.Element == UInt8 { 184 | /// A non-blocking sequence of newline-separated `String`s created by 185 | /// decoding the elements of `self` as utf-8. 186 | public var lineSequence: AsyncLineSequence { 187 | AsyncLineSequence(_base: self) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/AsyncUnicodeScalarSequence.swift: -------------------------------------------------------------------------------- 1 | /// A non-blocking sequence of `UnicodeScalar`s created by decoding the elements 2 | /// of `Base` as utf-8. 3 | public struct AsyncUnicodeScalarSequence: AsyncSequence 4 | where Base: AsyncSequence, Base.Element == UInt8 { 5 | /// The type of element produced by this asynchronous sequence. 6 | public typealias Element = UnicodeScalar 7 | 8 | /// The type of asynchronous iterator that produces elements of this 9 | /// asynchronous sequence. 10 | public struct AsyncIterator: AsyncIteratorProtocol { 11 | @usableFromInline 12 | internal var _base: Base.AsyncIterator 13 | @usableFromInline 14 | internal var _leftover: UInt8? 15 | 16 | internal init(_base base: Base.AsyncIterator) { 17 | self._base = base 18 | self._leftover = nil 19 | } 20 | 21 | @inlinable 22 | internal func _expectedContinuationCountForByte(_ byte: UInt8) -> Int? { 23 | if byte & 0b11100000 == 0b11000000 { 24 | return 1 25 | } 26 | 27 | if byte & 0b11110000 == 0b11100000 { 28 | return 2 29 | } 30 | 31 | if byte & 0b11111000 == 0b11110000 { 32 | return 3 33 | } 34 | 35 | if byte & 0b10000000 == 0b00000000 { 36 | return 0 37 | } 38 | 39 | if byte & 0b11000000 == 0b10000000 { 40 | // is a continuation itself 41 | return nil 42 | } 43 | 44 | // is an invalid value 45 | return nil 46 | } 47 | 48 | @inlinable 49 | internal mutating func _nextComplexScalar( 50 | _ first: UInt8 51 | ) async rethrows 52 | -> UnicodeScalar? 53 | { 54 | guard 55 | let expectedContinuationCount = 56 | self._expectedContinuationCountForByte(first) 57 | else { 58 | // We only reach here for invalid UTF8, so just return a 59 | // replacement character directly 60 | return "\u{FFFD}" 61 | } 62 | 63 | var bytes: (UInt8, UInt8, UInt8, UInt8) = (first, 0, 0, 0) 64 | var numContinuations = 0 65 | while numContinuations < expectedContinuationCount, 66 | let next = try await self._base.next() 67 | { 68 | guard UTF8.isContinuation(next) else { 69 | // We read one more byte than we needed due to an invalid 70 | // missing continuation byte. Store it in `leftover` for 71 | // next time 72 | self._leftover = next 73 | break 74 | } 75 | 76 | numContinuations += 1 77 | withUnsafeMutableBytes(of: &bytes) { 78 | $0[numContinuations] = next 79 | } 80 | } 81 | return withUnsafeBytes(of: &bytes) { 82 | return String(decoding: $0, as: UTF8.self).unicodeScalars.first 83 | } 84 | } 85 | 86 | /// Asynchronously advances to the next element and returns it, or ends 87 | /// the sequence if there is no next element. 88 | /// 89 | /// - Returns: The next element, if it exists, or `nil` to signal the 90 | /// end of the sequence. 91 | @inlinable 92 | public mutating func next() async rethrows -> UnicodeScalar? { 93 | if let leftover = self._leftover { 94 | self._leftover = nil 95 | return try await self._nextComplexScalar(leftover) 96 | } 97 | if let byte = try await self._base.next() { 98 | if UTF8.isASCII(byte) { 99 | _onFastPath() 100 | return UnicodeScalar(byte) 101 | } 102 | 103 | return try await self._nextComplexScalar(byte) 104 | } 105 | 106 | return nil 107 | } 108 | } 109 | 110 | private let base: Base 111 | 112 | internal init(_base base: Base) { 113 | self.base = base 114 | } 115 | 116 | /// Creates the asynchronous iterator that produces elements of this 117 | /// asynchronous sequence. 118 | /// 119 | /// - Returns: An instance of the `AsyncIterator` type used to produce 120 | /// elements of the asynchronous sequence. 121 | public func makeAsyncIterator() -> AsyncIterator { 122 | return AsyncIterator(_base: self.base.makeAsyncIterator()) 123 | } 124 | } 125 | 126 | extension AsyncSequence where Self.Element == UInt8 { 127 | /// A non-blocking sequence of `UnicodeScalar`s created by decoding the 128 | /// elements of `self` as utf-8. 129 | public var unicodeScalarSequence: AsyncUnicodeScalarSequence { 130 | AsyncUnicodeScalarSequence(_base: self) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/ChildProcess.swift: -------------------------------------------------------------------------------- 1 | import AsyncAlgorithms 2 | import Foundation 3 | 4 | #if canImport(WinSDK) 5 | import WinSDK 6 | #endif 7 | 8 | /// Handle to a running or exited child process 9 | /// 10 | /// This class is used to represent and manage child processes. A child process 11 | /// is created via the ``Command`` struct, which configures the spawning process 12 | /// and can itself be constructed using a builder-style interface. 13 | /// 14 | /// There is no deinit implementation for child processes, so if you do not 15 | /// ensure the ``ChildProcess`` has exited then it will continue to run, even 16 | /// after the ``ChildProcess`` handle has gone out of scope. 17 | /// 18 | /// Calling ``ChildProcess/wait()``, ``ChildProcess/status``, or similar will 19 | /// make the parent process wait until the child has actually exited before 20 | /// continuing: 21 | /// 22 | /// ```swift 23 | /// let process = try Command.findInPath(withName: "cat") 24 | /// .addArgument("file.txt") 25 | /// .spawn() 26 | /// 27 | /// let exitStatus = try process.wait() 28 | /// ``` 29 | public final class ChildProcess 30 | where Stdin: InputSource, Stdout: OutputDestination, Stderr: OutputDestination { 31 | /// An error that can be thrown while terminating a child process. 32 | public enum Error: Swift.Error, CustomStringConvertible { 33 | /// An error indicating that the output data could not be decoded as 34 | /// utf-8. 35 | case couldNotDecodeOutput 36 | /// An error indicating that the reason for the termination of the 37 | /// process is unknown. 38 | case unknownTerminationReason 39 | 40 | public var description: String { 41 | switch self { 42 | case .couldNotDecodeOutput: 43 | return "Could not decode output data as an utf-8 string" 44 | case .unknownTerminationReason: 45 | return 46 | "The reason for the termination of the process is unknown" 47 | } 48 | } 49 | } 50 | 51 | /// A handle to a child process's standard input (stdin). 52 | /// 53 | /// ``ChildProcess/InputHandle`` allows writing to the stdin of a child 54 | /// process or closing it. Since it conforms to the `TextOutputStream` 55 | /// protocol, you can also use the `print()` function in conjunction with 56 | /// it. 57 | /// 58 | /// The handle can be obtained by accessing ``ChildProcess/stdin`` on a 59 | /// ``ChildProcess`` instance whose stdin is piped: 60 | /// 61 | /// ```swift 62 | /// let process = try Command.findInPath(withName: "cat") 63 | /// .setStdin(.pipe) 64 | /// .setStdout(.pipe) 65 | /// .spawn() 66 | /// 67 | /// var stdin = process.stdin 68 | /// 69 | /// print("Foo", to: &stdin) 70 | /// print("Bar", to: &stdin) 71 | /// 72 | /// let output = try await process.output 73 | /// 74 | /// print(output.stdout) 75 | /// // Prints 'Foo\nBar\n' 76 | /// ``` 77 | public struct InputHandle: TextOutputStream { 78 | internal let pipe: Pipe 79 | 80 | fileprivate init(pipe: Pipe) { 81 | self.pipe = pipe 82 | } 83 | 84 | /// Writes the given string to the child process's stdin stream. 85 | /// 86 | /// An exception is thrown if this handle has been invalidated by a call 87 | /// to ``ChildProcess/InputHandle/close()``. 88 | /// 89 | /// - Parameters: 90 | /// - string: The string to append to the child process's stdin. 91 | public func write(_ string: String) { 92 | if #available(macOS 10.15.4, *) { 93 | try! self.pipe.fileHandleForWriting 94 | .write(contentsOf: string.data(using: .utf8)!) 95 | } else { 96 | self.pipe.fileHandleForWriting.write(string.data(using: .utf8)!) 97 | } 98 | } 99 | 100 | /// Writes the given data to the child process's stdin stream. 101 | /// 102 | /// An exception is thrown if this handle has been invalidated by a call 103 | /// to ``ChildProcess/InputHandle/close()``. 104 | /// 105 | /// - Parameters: 106 | /// - data: The data to append to the child process's stdin. 107 | public func write(contentsOf data: T) { 108 | if #available(macOS 10.15.4, *) { 109 | try! self.pipe.fileHandleForWriting.write(contentsOf: data) 110 | } else { 111 | self.pipe.fileHandleForWriting.write(Data(data)) 112 | } 113 | } 114 | 115 | /// Closes the child process's stdin stream, ensuring that the process 116 | /// does not block waiting for input from the parent anymore. 117 | /// 118 | /// This invalidates the input handle. If you try to keep writing to 119 | /// it, an exception is thrown. 120 | public func close() { 121 | try! self.pipe.fileHandleForWriting.close() 122 | } 123 | } 124 | 125 | /// A handle to a child process's standard output (stdout) or stderr. 126 | /// 127 | /// ``ChildProcess/OutputHandle`` allows reading from the stdout or stderr 128 | /// of a child process. You can access 129 | /// ``ChildProcess/OutputHandle/availableData`` or call 130 | /// ``ChildProcess/OutputHandle/read(upToCount:)`` to get the currently 131 | /// available data of the child process's output, or call 132 | /// ``ChildProcess/OutputHandle/readToEnd()`` to read data until the child 133 | /// process sends an end of file signal. The convenience accessors 134 | /// ``ChildProcess/OutputHandle/characters`` and 135 | /// ``ChildProcess/OutputHandle/lines`` are also available and give access 136 | /// to `AsyncSequence`'s, returning characters or lines output by the child 137 | /// process asynchronously. 138 | /// 139 | /// The handle can be obtained by accessing ``ChildProcess/stdout`` or 140 | /// ``ChildProcess/stderr`` on a ``ChildProcess`` instance whose stdout or 141 | /// stderr is respectively piped: 142 | /// 143 | /// ```swift 144 | /// let process = try Command.findInPath(withName: "echo") 145 | /// .addArguments("Foo", "Bar") 146 | /// .setStdout(.pipe) 147 | /// .spawn() 148 | /// 149 | /// for try await line in process.stdout.lines { 150 | /// print(line) 151 | /// } 152 | /// // Prints 'Foo' and 'Bar' 153 | /// 154 | /// try process.wait() 155 | /// // Ensure the process is terminated before exiting the parent process 156 | /// ``` 157 | public struct OutputHandle { 158 | /// An asynchronous sequence of characters, output by a child process. 159 | // Should be replaced by 'some AsyncSequence' as soon as that 160 | // is available. 161 | public struct AsyncCharacters: AsyncSequence { 162 | @usableFromInline 163 | internal typealias Base = 164 | AsyncCharacterSequence 165 | 166 | /// The type of element produced by this asynchronous sequence. 167 | public typealias Element = Character 168 | 169 | /// The type of asynchronous iterator that produces elements of this 170 | /// asynchronous sequence. 171 | public struct AsyncIterator: AsyncIteratorProtocol { 172 | @usableFromInline 173 | internal var _base: Base.AsyncIterator 174 | 175 | fileprivate init(_base: Base.AsyncIterator) { 176 | self._base = _base 177 | } 178 | 179 | /// Asynchronously advances to the next element and returns it, 180 | /// or ends the sequence if there is no next element. 181 | /// 182 | /// - Returns: The next element, if it exists, or `nil` to 183 | /// signal the end of the sequence. 184 | @inlinable 185 | public mutating func next() async throws -> Character? { 186 | try await self._base.next() 187 | } 188 | } 189 | 190 | private let base: Base 191 | 192 | fileprivate init(_base base: Base) { 193 | self.base = base 194 | } 195 | 196 | /// Creates the asynchronous iterator that produces elements of this 197 | /// asynchronous sequence. 198 | /// 199 | /// - Returns: An instance of the `AsyncIterator` type used to 200 | /// produce elements of the asynchronous sequence. 201 | public func makeAsyncIterator() -> AsyncIterator { 202 | .init(_base: self.base.makeAsyncIterator()) 203 | } 204 | } 205 | 206 | /// An asynchronous sequence of characters, output by a child process. 207 | // Should be replaced by 'some AsyncSequence' as soon as that is 208 | // available. 209 | public struct AsyncLines: AsyncSequence { 210 | @usableFromInline 211 | internal typealias Base = 212 | AsyncLineSequence 213 | 214 | /// The type of element produced by this asynchronous sequence. 215 | public typealias Element = String 216 | 217 | /// The type of asynchronous iterator that produces elements of this 218 | /// asynchronous sequence. 219 | public struct AsyncIterator: AsyncIteratorProtocol { 220 | @usableFromInline 221 | internal var _base: Base.AsyncIterator 222 | 223 | fileprivate init(_base: Base.AsyncIterator) { 224 | self._base = _base 225 | } 226 | 227 | /// Asynchronously advances to the next element and returns it, 228 | /// or ends the sequence if there is no next element. 229 | /// 230 | /// - Returns: The next element, if it exists, or `nil` to 231 | /// signal the end of the sequence. 232 | @inlinable 233 | public mutating func next() async throws -> String? { 234 | try await self._base.next() 235 | } 236 | } 237 | 238 | private let base: Base 239 | 240 | fileprivate init(_base base: Base) { 241 | self.base = base 242 | } 243 | 244 | /// Creates the asynchronous iterator that produces elements of this 245 | /// asynchronous sequence. 246 | /// 247 | /// - Returns: An instance of the `AsyncIterator` type used to 248 | /// produce elements of the asynchronous sequence. 249 | public func makeAsyncIterator() -> AsyncIterator { 250 | .init(_base: self.base.makeAsyncIterator()) 251 | } 252 | } 253 | 254 | /// An asynchronous sequence of raw bytes, output by a child process. 255 | // Should be replaced by 'some AsyncSequence' as soon as that is 256 | // available. 257 | public struct AsyncBytes: AsyncSequence { 258 | @usableFromInline 259 | internal typealias Base = FileHandle.CustomAsyncBytes 260 | 261 | /// The type of element produced by this asynchronous sequence. 262 | public typealias Element = UInt8 263 | 264 | /// The type of asynchronous iterator that produces elements of this 265 | /// asynchronous sequence. 266 | public struct AsyncIterator: AsyncIteratorProtocol { 267 | @usableFromInline 268 | internal var _base: Base.AsyncIterator 269 | 270 | fileprivate init(_base: Base.AsyncIterator) { 271 | self._base = _base 272 | } 273 | 274 | /// Asynchronously advances to the next element and returns it, 275 | /// or ends the sequence if there is no next element. 276 | /// 277 | /// - Returns: The next element, if it exists, or `nil` to 278 | /// signal the end of the sequence. 279 | @inlinable 280 | public mutating func next() async throws -> UInt8? { 281 | try await self._base.next() 282 | } 283 | } 284 | 285 | private let base: Base 286 | 287 | fileprivate init(_base base: Base) { 288 | self.base = base 289 | } 290 | 291 | /// Creates the asynchronous iterator that produces elements of this 292 | /// asynchronous sequence. 293 | /// 294 | /// - Returns: An instance of the `AsyncIterator` type used to 295 | /// produce elements of the asynchronous sequence. 296 | public func makeAsyncIterator() -> AsyncIterator { 297 | .init(_base: self.base.makeAsyncIterator()) 298 | } 299 | } 300 | 301 | internal let pipe: Pipe 302 | 303 | fileprivate init(pipe: Pipe) { 304 | self.pipe = pipe 305 | } 306 | 307 | /// The data currently available in the child process's output stream. 308 | /// 309 | /// This accessor reads up to a buffer of data and returns it; if no 310 | /// data is available, it blocks. Returns an empty `Data` object if the 311 | /// child process closed the stream. 312 | public var availableData: Data { 313 | self.pipe.fileHandleForReading.availableData 314 | } 315 | 316 | /// Reads up to the specified number of bytes of data synchronously from 317 | /// the child process's output stream. 318 | /// 319 | /// This method reads up to `count` bytes from the channel. Returns an 320 | /// empty `Data` object if the child process closed the stream. 321 | /// 322 | /// - Parameters: 323 | /// - count: The number of bytes to read from the child process's 324 | /// output stream. 325 | /// - Returns: The data currently available in the stream, up to `count` 326 | /// number of bytes, or an empty `Data` object if the stream 327 | /// is closed. 328 | public func read(upToCount count: Int) -> Data? { 329 | guard #available(macOS 10.15.4, *) else { 330 | return self.pipe.fileHandleForReading.readData(ofLength: count) 331 | } 332 | 333 | return 334 | try! self.pipe.fileHandleForReading.read(upToCount: count) 335 | } 336 | 337 | /// Reads data synchronously up to the end of file or maximum number of 338 | /// bytes from the child process's output stream. 339 | /// 340 | /// - Returns: The data in the stream until an end-of-file indicator is 341 | /// encountered. 342 | public func readToEnd() -> Data? { 343 | guard #available(macOS 10.15.4, *) else { 344 | return self.pipe.fileHandleForReading.readDataToEndOfFile() 345 | } 346 | 347 | return try! self.pipe.fileHandleForReading.readToEnd() 348 | } 349 | 350 | /// Returns an asynchronous sequence returning the decoded characters 351 | /// output by the child process. 352 | /// 353 | /// ```swift 354 | /// let process = try Command.findInPath(withName: "echo") 355 | /// .addArgument("Foo") 356 | /// .setStdout(.pipe) 357 | /// .spawn() 358 | /// 359 | /// for try await character in process.stdout.characters { 360 | /// print(character) 361 | /// } 362 | /// // Prints 'F', 'o', 'o', and '\n' 363 | /// 364 | /// try process.wait() 365 | /// // Ensure the process is terminated before exiting the parent 366 | /// // process 367 | /// ``` 368 | public var characters: AsyncCharacters { 369 | .init( 370 | _base: 371 | self.pipe.fileHandleForReading.customBytes.characterSequence 372 | ) 373 | } 374 | 375 | #if canImport(Darwin) 376 | /// Returns the `Foundation` provided asynchronous sequence returning 377 | /// the decoded characters output by the child process. 378 | /// 379 | /// ```swift 380 | /// let process = try Command.findInPath(withName: "echo") 381 | /// .addArgument("Foo") 382 | /// .setStdout(.pipe) 383 | /// .spawn() 384 | /// 385 | /// for try await character in process.stdout.nativeCharacters { 386 | /// print(character) 387 | /// } 388 | /// // Prints 'F', 'o', 'o', and '\n' 389 | /// 390 | /// try process.wait() 391 | /// // Ensure the process is terminated before exiting the parent 392 | /// // process 393 | /// ``` 394 | @available(macOS 12.0, *) 395 | public var nativeCharacters: 396 | Foundation.AsyncCharacterSequence 397 | { 398 | self.pipe.fileHandleForReading.bytes.characters 399 | } 400 | #endif 401 | 402 | /// Returns an asynchronous sequence returning the decoded lines output 403 | /// by the child process. 404 | /// 405 | /// ```swift 406 | /// let process = try Command.findInPath(withName: "echo") 407 | /// .addArgument("Foo") 408 | /// .setStdout(.pipe) 409 | /// .spawn() 410 | /// 411 | /// for try await line in process.stdout.lines { 412 | /// print(line) 413 | /// } 414 | /// // Prints 'Foo' and 'Bar' 415 | /// 416 | /// try process.wait() 417 | /// // Ensure the process is terminated before exiting the parent 418 | /// // process 419 | /// ``` 420 | public var lines: AsyncLines { 421 | .init( 422 | _base: self.pipe.fileHandleForReading.customBytes.lineSequence 423 | ) 424 | } 425 | 426 | #if canImport(Darwin) 427 | /// Returns the `Foundation` provided asynchronous sequence returning 428 | /// the decoded lines output by the child process. 429 | /// 430 | /// ```swift 431 | /// let process = try Command.findInPath(withName: "echo") 432 | /// .addArgument("Foo") 433 | /// .setStdout(.pipe) 434 | /// .spawn() 435 | /// 436 | /// for try await line in process.stdout.nativeLines { 437 | /// print(line) 438 | /// } 439 | /// // Prints 'Foo' and 'Bar' 440 | /// 441 | /// try process.wait() 442 | /// // Ensure the process is terminated before exiting the parent 443 | /// // process 444 | /// ``` 445 | @available(macOS 12.0, *) 446 | public var nativeLines: 447 | Foundation.AsyncLineSequence 448 | { 449 | self.pipe.fileHandleForReading.bytes.lines 450 | } 451 | #endif 452 | 453 | /// Returns an asynchronous sequence returning the raw bytes output by 454 | /// the child process. 455 | /// 456 | /// ```swift 457 | /// let process = try Command.findInPath(withName: "echo") 458 | /// .addArgument("Foo") 459 | /// .setStdout(.pipe) 460 | /// .spawn() 461 | /// 462 | /// for try await byte in process.stdout.bytes { 463 | /// print(byte) 464 | /// } 465 | /// // Prints '70', '111', '111', and '10' 466 | /// 467 | /// try process.wait() 468 | /// // Ensure the process is terminated before exiting the parent 469 | /// // process 470 | /// ``` 471 | public var bytes: AsyncBytes { 472 | .init(_base: self.pipe.fileHandleForReading.customBytes) 473 | } 474 | 475 | #if canImport(Darwin) 476 | /// Returns the `Foundation` provided asynchronous sequence returning 477 | /// the raw bytes output by the child process. 478 | /// 479 | /// ```swift 480 | /// let process = try Command.findInPath(withName: "echo") 481 | /// .addArgument("Foo") 482 | /// .setStdout(.pipe) 483 | /// .spawn() 484 | /// 485 | /// for try await byte in process.stdout.nativeBytes { 486 | /// print(byte) 487 | /// } 488 | /// // Prints '70', '111', '111', and '10' 489 | /// 490 | /// try process.wait() 491 | /// // Ensure the process is terminated before exiting the parent 492 | /// // process 493 | /// ``` 494 | @available(macOS 12.0, *) 495 | public var nativeBytes: FileHandle.AsyncBytes { 496 | self.pipe.fileHandleForReading.bytes 497 | } 498 | #endif 499 | } 500 | 501 | public struct MergedAsyncLines: AsyncSequence { 502 | @usableFromInline 503 | internal typealias Base = AsyncMerge2Sequence< 504 | AsyncLineSequence, 505 | AsyncLineSequence 506 | > 507 | 508 | /// The type of element produced by this asynchronous sequence. 509 | public typealias Element = String 510 | 511 | /// The type of asynchronous iterator that produces elements of this 512 | /// asynchronous sequence. 513 | public struct AsyncIterator: AsyncIteratorProtocol { 514 | @usableFromInline 515 | internal var _base: Base.AsyncIterator 516 | 517 | fileprivate init(_base: Base.AsyncIterator) { 518 | self._base = _base 519 | } 520 | 521 | /// Asynchronously advances to the next element and returns it, 522 | /// or ends the sequence if there is no next element. 523 | /// 524 | /// - Returns: The next element, if it exists, or `nil` to 525 | /// signal the end of the sequence. 526 | @inlinable 527 | public mutating func next() async throws -> String? { 528 | try await self._base.next() 529 | } 530 | } 531 | 532 | private let base: Base 533 | 534 | fileprivate init(_base base: Base) { 535 | self.base = base 536 | } 537 | 538 | /// Creates the asynchronous iterator that produces elements of this 539 | /// asynchronous sequence. 540 | /// 541 | /// - Returns: An instance of the `AsyncIterator` type used to 542 | /// produce elements of the asynchronous sequence. 543 | public func makeAsyncIterator() -> AsyncIterator { 544 | .init(_base: self.base.makeAsyncIterator()) 545 | } 546 | } 547 | 548 | internal typealias GeneratingCommand = Command 549 | 550 | private let command: GeneratingCommand 551 | private let process: Process 552 | private let stdinPipe, stdoutPipe, stderrPipe: Pipe? 553 | private let closeStdinImplicitly: Bool 554 | 555 | private init( 556 | command: GeneratingCommand, 557 | process: Process, 558 | stdinPipe: Pipe?, 559 | stdoutPipe: Pipe?, 560 | stderrPipe: Pipe?, 561 | closeStdinImplicitly: Bool 562 | ) { 563 | self.command = command 564 | self.process = process 565 | self.stdinPipe = stdinPipe 566 | self.stdoutPipe = stdoutPipe 567 | self.stderrPipe = stderrPipe 568 | self.closeStdinImplicitly = closeStdinImplicitly 569 | } 570 | 571 | internal static func spawn( 572 | withCommand command: GeneratingCommand 573 | ) throws -> ChildProcess { 574 | let process = Process() 575 | 576 | process.executableURL = command.executablePath.url 577 | process.arguments = command.arguments 578 | 579 | let environment: [String: String] 580 | if command.inheritEnvironment { 581 | environment = ProcessInfo.processInfo 582 | .environment 583 | .merging(command.environment) { $1 } 584 | } else { 585 | environment = command.environment 586 | } 587 | 588 | process.environment = environment 589 | 590 | if let cwd = command.cwd { 591 | process.currentDirectoryURL = cwd.url 592 | } 593 | 594 | let stdinPipe: Pipe? 595 | let closeStdinImplicitly: Bool 596 | switch command.stdin { 597 | case let pipeSource as PipeInputSource: 598 | stdinPipe = Pipe() 599 | closeStdinImplicitly = pipeSource.closeImplicitly 600 | process.standardInput = stdinPipe 601 | case let stdin: 602 | stdinPipe = nil 603 | closeStdinImplicitly = false 604 | switch try stdin.processInput { 605 | case let .first(fileHandle): 606 | process.standardInput = fileHandle 607 | case let .second(pipe): 608 | process.standardInput = pipe 609 | } 610 | } 611 | 612 | let stdoutPipe: Pipe? 613 | switch command.stdout { 614 | case is PipeOutputDestination: 615 | stdoutPipe = Pipe() 616 | process.standardOutput = stdoutPipe 617 | case let stdout: 618 | stdoutPipe = nil 619 | switch try stdout.processOutput(forType: .stdout) { 620 | case let .first(fileHandle): 621 | process.standardOutput = fileHandle 622 | case let .second(pipe): 623 | process.standardOutput = pipe 624 | } 625 | } 626 | 627 | let stderrPipe: Pipe? 628 | switch command.stderr { 629 | case is PipeOutputDestination: 630 | stderrPipe = Pipe() 631 | process.standardError = stderrPipe 632 | case let stderr: 633 | stderrPipe = nil 634 | switch try stderr.processOutput(forType: .stderr) { 635 | case let .first(fileHandle): 636 | process.standardError = fileHandle 637 | case let .second(pipe): 638 | process.standardError = pipe 639 | } 640 | } 641 | 642 | try process.run() 643 | 644 | return .init( 645 | command: command, 646 | process: process, 647 | stdinPipe: stdinPipe, 648 | stdoutPipe: stdoutPipe, 649 | stderrPipe: stderrPipe, 650 | closeStdinImplicitly: closeStdinImplicitly 651 | ) 652 | } 653 | 654 | /// The pid of the child process. 655 | public var identifier: Int32 { 656 | self.process.processIdentifier 657 | } 658 | 659 | /// Indicates, if the child process is still running. 660 | public var isRunning: Bool { 661 | self.process.isRunning 662 | } 663 | 664 | /// Checks to see if the child process has already terminated and returns 665 | /// the process's exit status if that's the case. 666 | /// 667 | /// - Note: This accessor is deprecated. Use 668 | /// ``ChildProcess/statusIfAvailable`` instead. 669 | @available(*, deprecated, renamed: "statusIfAvailable") 670 | public var exitStatus: ExitStatus? { 671 | get throws { 672 | try self.statusIfAvailable 673 | } 674 | } 675 | 676 | /// Checks to see if the child process has already terminated and returns 677 | /// the process's exit status if that's the case. 678 | public var statusIfAvailable: ExitStatus? { 679 | get throws { 680 | guard self.process.isRunning else { 681 | return try self.createExitStatus().get() 682 | } 683 | 684 | return nil 685 | } 686 | } 687 | 688 | /// Sends an interrupt signal (`SIGINT`) to the child process. This means 689 | /// that the child process is terminated, if it isn't intentionally ignoring 690 | /// this signal. 691 | /// 692 | /// Calling this method has no effect, if the child process has already 693 | /// terminated. 694 | public func interrupt() { 695 | self.process.interrupt() 696 | } 697 | 698 | /// Sends a terminate signal (`SIGTERM`) to the child process. This means 699 | /// that the child process is terminated, if it isn't intentionally ignoring 700 | /// this signal. 701 | /// 702 | /// Calling this method has no effect, if the child process has already 703 | /// terminated. 704 | public func terminate() { 705 | self.process.terminate() 706 | } 707 | 708 | /// Sends a kill signal (`SIGKILL`) to the child process. This normally 709 | /// means that the child process is immediately terminated, no matter what. 710 | /// 711 | /// Use this method cautiously, since the child process cannot react to it 712 | /// in any way. 713 | /// 714 | /// Calling this method has no effect, if the child process has already 715 | /// terminated. 716 | public func kill() { 717 | if self.process.isRunning { 718 | #if canImport(WinSDK) 719 | WinSDK.TerminateProcess(self.process.processHandle, 1) 720 | #elseif canImport(Darwin) 721 | Darwin.kill(self.process.processIdentifier, SIGKILL) 722 | #elseif canImport(Glibc) 723 | Glibc.kill(self.process.processIdentifier, SIGKILL) 724 | #else 725 | #error("Unsupported platform!") 726 | #endif 727 | } 728 | } 729 | 730 | /// Waits for the child process to exit completely, returning the status 731 | /// that it exited with. 732 | /// 733 | /// This method will continue to have the same return value after it has 734 | /// been called at least once. 735 | /// 736 | /// This blocks the current thread until the child process has terminated. 737 | /// 738 | /// The stdin handle to the child process – if stdin is piped – will be 739 | /// closed before waiting. This helps avoid deadlock: it ensures that the 740 | /// child process does not block waiting for input from the parent process, 741 | /// while the parent waits for the child to exit. If you want to disable 742 | /// implicit closing of the pipe, you can do so, by setting 743 | /// ``PipeInputSource/closeImplicitly`` to `false` on 744 | /// ``PipeInputSource``. 745 | /// 746 | /// - Returns: The exit status of the child process. 747 | @discardableResult 748 | public func wait() throws -> ExitStatus { 749 | try self.closePipedStdin() 750 | 751 | self.process.waitUntilExit() 752 | 753 | return try self.createExitStatus().get() 754 | } 755 | 756 | /// Waits for the child process to exit completely, returning the status 757 | /// that it exited with. 758 | /// 759 | /// This accessor will continue to have the same return value after it has 760 | /// been called at least once. 761 | /// 762 | /// This doesn't block the current thread and allows other tasks to run 763 | /// before the child process terminates. 764 | /// 765 | /// The stdin handle to the child process – if stdin is piped – will be 766 | /// closed before waiting. This helps avoid deadlock: it ensures that the 767 | /// child process does not block waiting for input from the parent process, 768 | /// while the parent waits for the child to exit. If you want to disable 769 | /// implicit closing of the pipe, you can do so, by setting 770 | /// ``PipeInputSource/closeImplicitly`` to `false` on 771 | /// ``PipeInputSource``. 772 | public var status: ExitStatus { 773 | get async throws { 774 | try self.closePipedStdin() 775 | 776 | return try await withCheckedThrowingContinuation { continuation in 777 | if self.process.isRunning { 778 | self.process.terminationHandler = { [weak self] _ in 779 | if let self = self { 780 | continuation.resume(with: self.createExitStatus()) 781 | } else { 782 | fatalError() 783 | } 784 | } 785 | } else { 786 | continuation.resume(with: self.createExitStatus()) 787 | } 788 | } 789 | } 790 | } 791 | 792 | private func createExitStatus() -> Result { 793 | switch self.process.terminationReason { 794 | case .exit: 795 | let status = self.process.terminationStatus 796 | guard status == 0 else { 797 | return .success(.error(exitCode: status)) 798 | } 799 | 800 | return .success(.success) 801 | case .uncaughtSignal: 802 | #if os(Windows) 803 | return .success(.terminatedBySignal) 804 | #else 805 | let signal = self.process.terminationStatus 806 | return .success(.terminatedBySignal(signal: signal)) 807 | #endif 808 | #if canImport(Darwin) 809 | @unknown default: 810 | return .failure(Error.unknownTerminationReason) 811 | #endif 812 | } 813 | } 814 | 815 | private func closePipedStdin() throws { 816 | if self.closeStdinImplicitly { 817 | try self.stdinPipe!.fileHandleForWriting.close() 818 | } 819 | } 820 | } 821 | 822 | extension ChildProcess where Stdin == PipeInputSource { 823 | /// The handle for writing to the child process’s standard input (stdin), if 824 | /// it is piped. 825 | public var stdin: InputHandle { 826 | .init(pipe: self.stdinPipe!) 827 | } 828 | } 829 | 830 | extension ChildProcess where Stdout == PipeOutputDestination { 831 | /// The handle for reading from the child process’s standard output 832 | /// (stdout), if it is piped. 833 | public var stdout: OutputHandle { 834 | .init(pipe: self.stdoutPipe!) 835 | } 836 | 837 | /// Simultaneously waits for the child process to exit and collects all 838 | /// remaining output on the stdout/stderr handles, returning a 839 | /// ``ProcessOutput`` instance. 840 | /// 841 | /// This blocks the current thread until the child process has terminated. 842 | /// 843 | /// The stdin handle to the child process – if stdin is piped – will be 844 | /// closed before waiting. This helps avoid deadlock: it ensures that the 845 | /// child process does not block waiting for input from the parent process, 846 | /// while the parent waits for the child to exit. If you want to disable 847 | /// implicit closing of the pipe, you can do so, by setting 848 | /// ``PipeInputSource/closeImplicitly`` to `false` on 849 | /// ``PipeInputSource``. 850 | /// 851 | /// - Note: This method can only be called when stdout is piped. 852 | /// 853 | /// - Returns: The collected output of the child process. 854 | public func waitWithOutput() throws -> ProcessOutput { 855 | try self.closePipedStdin() 856 | 857 | self.process.waitUntilExit() 858 | 859 | return try self.createProcessOutput().get() 860 | } 861 | 862 | /// Simultaneously waits for the child process to exit and collects all 863 | /// remaining output on the stdout/stderr handles, returning a 864 | /// ``ProcessOutput`` instance. 865 | /// 866 | /// This doesn't block the current thread and allows other tasks to run 867 | /// before the child process terminates. 868 | /// 869 | /// The stdin handle to the child process – if stdin is piped – will be 870 | /// closed before waiting. This helps avoid deadlock: it ensures that the 871 | /// child process does not block waiting for input from the parent process, 872 | /// while the parent waits for the child to exit. If you want to disable 873 | /// implicit closing of the pipe, you can do so, by setting 874 | /// ``PipeInputSource/closeImplicitly`` to `false` on 875 | /// ``PipeInputSource``. 876 | /// 877 | /// - Note: This accessor can only be called when stdout is piped. 878 | public var output: ProcessOutput { 879 | get async throws { 880 | try self.closePipedStdin() 881 | 882 | return try await withCheckedThrowingContinuation { continuation in 883 | if self.process.isRunning { 884 | self.process.terminationHandler = { [weak self] _ in 885 | if let self = self { 886 | continuation 887 | .resume(with: self.createProcessOutput()) 888 | } else { 889 | fatalError() 890 | } 891 | } 892 | } else { 893 | continuation.resume(with: self.createProcessOutput()) 894 | } 895 | } 896 | } 897 | } 898 | 899 | private func createProcessOutput() -> Result { 900 | self.createExitStatus() 901 | .flatMap { status in 902 | let stdoutData = self.stdoutPipe!.fileHandleForReading 903 | .availableData 904 | guard 905 | let stdout = String( 906 | data: stdoutData, 907 | encoding: .utf8 908 | ) 909 | else { 910 | return .failure(Error.couldNotDecodeOutput) 911 | } 912 | 913 | let stderrData = self 914 | .stderrPipe? 915 | .fileHandleForReading 916 | .availableData 917 | let stderr = stderrData.flatMap { 918 | String(data: $0, encoding: .utf8) 919 | } 920 | 921 | return .success( 922 | .init( 923 | status: status, 924 | stdoutData: stdoutData, 925 | stdout: stdout, 926 | stderrData: stderrData, 927 | stderr: stderr 928 | ) 929 | ) 930 | } 931 | } 932 | } 933 | 934 | extension ChildProcess where Stderr == PipeOutputDestination { 935 | /// The handle for reading from the child process’s standard error output 936 | /// (stderr), if it is piped. 937 | public var stderr: OutputHandle { 938 | .init(pipe: self.stderrPipe!) 939 | } 940 | } 941 | 942 | extension ChildProcess 943 | where 944 | Stdout == PipeOutputDestination, 945 | Stderr == PipeOutputDestination 946 | { 947 | /// Returns an asynchronous sequence returning the decoded lines output 948 | /// by the child process both from stdout and stderr. 949 | /// 950 | /// ```swift 951 | /// let echoProcess = try Command.findInPath(withName: "echo") 952 | /// .addArguments("Foo", "Bar") 953 | /// .setStdout(.pipe) 954 | /// .spawn() 955 | /// 956 | /// let teeProcess = try Command.findInPath(withName: "tee") 957 | /// .addArgument("/dev/stderr") 958 | /// .setStdin(.pipe(from: echoProcess.stdout)) 959 | /// .setOutputs(.pipe) 960 | /// .spawn() 961 | /// 962 | /// for try await line in teeProcess.mergedOutputLines { 963 | /// print(line) 964 | /// } 965 | /// // Prints 'Foo' and 'Bar' twice, maybe interleaved 966 | /// 967 | /// try teeProcess.wait() 968 | /// // Ensure the process is terminated before exiting the parent 969 | /// // process 970 | /// ``` 971 | public var mergedOutputLines: MergedAsyncLines { 972 | .init( 973 | _base: merge( 974 | self.stdout.pipe.fileHandleForReading.customBytes.lineSequence, 975 | self.stderr.pipe.fileHandleForReading.customBytes.lineSequence 976 | ) 977 | ) 978 | } 979 | } 980 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/Command.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @preconcurrency import SystemPackage 3 | 4 | /// A process builder, providing fine-grained control over how a new process 5 | /// should be spawned. 6 | /// 7 | /// A default configuration can be generated using 8 | /// ``Command/init(executablePath:)``, where `executablePath` gives a path to 9 | /// the program to be executed, or using ``Command/findInPath(withName:)``, 10 | /// where `name` is the name of a command line program available in `$PATH`. 11 | /// Additional builder methods allow the configuration to be changed (for 12 | /// example, by adding arguments) prior to spawning: 13 | /// 14 | /// ```swift 15 | /// let output = try Command.findInPath(withName: "echo")! 16 | /// .addArgument("Foo") 17 | /// .waitForOutput() 18 | /// 19 | /// let foo = output.stdout 20 | /// ``` 21 | /// 22 | /// A ``Command`` instance can be reused to spawn multiple processes. The 23 | /// builder methods change the command without needing to immediately spawn the 24 | /// process. 25 | /// 26 | /// ```swift 27 | /// let echoFoo = Command.findInPath(withName: "echo")! 28 | /// .addArgument("Foo") 29 | /// let foo1 = try echoFoo.waitForOutput() 30 | /// let foo2 = try echoFoo.waitForOutput() 31 | /// ``` 32 | /// 33 | /// Similarly, you can call builder methods after spawning a process and then 34 | /// spawn a new process with the modified settings. 35 | /// 36 | /// ```swift 37 | /// let listDir = Command.findInPath(withName: "ls")! 38 | /// 39 | /// // Execute `ls` in the current directory of the program. 40 | /// try listDir.wait() 41 | /// 42 | /// print() 43 | /// 44 | /// // Change `ls` to execute in the root directory. 45 | /// let listRootDir = listDir.setCWD("/") 46 | /// 47 | /// // And then execute `ls` again but in the root directory. 48 | /// try listRootDir.wait() 49 | /// ``` 50 | /// 51 | /// To wait for the child process to terminate, but not block the current 52 | /// thread, you can use the `async`/`await` API on ``Command``, e.g.: 53 | /// 54 | /// ```swift 55 | /// let output = try await Command.findInPath(withName: "echo")! 56 | /// .addArgument("Foo") 57 | /// .output 58 | /// 59 | /// print(output.stdout) 60 | /// // Prints 'Foo\n' 61 | /// ``` 62 | public struct Command: Equatable, Sendable 63 | where Stdin: InputSource, Stdout: OutputDestination, Stderr: OutputDestination { 64 | #if os(Windows) 65 | @inline(__always) 66 | private static var pathVariable: String { "Path" } 67 | @inline(__always) 68 | private static var pathSeparator: Character { ";" } 69 | @inline(__always) 70 | private static var executableExtension: String { ".exe" } 71 | #else 72 | @inline(__always) 73 | private static var pathVariable: String { "PATH" } 74 | @inline(__always) 75 | private static var pathSeparator: Character { ":" } 76 | @inline(__always) 77 | private static var executableExtension: String { "" } 78 | #endif 79 | 80 | /// The path of the executable file that will be invoked when this command 81 | /// is spawned. 82 | /// 83 | /// Can be used in conjunction with ``Command/findInPath(withName:)`` to 84 | /// find out the path of an executable in one of the directories contained 85 | /// in the `$PATH` environment variable: 86 | /// 87 | /// ```swift 88 | /// let path = Command.findInPath(withName: "echo")!.executablePath 89 | /// 90 | /// print(path) 91 | /// // Prints e.g. '/bin/echo' 92 | /// ``` 93 | public let executablePath: FilePath 94 | 95 | /// The list of arguments that will be passed to the program when this 96 | /// command is spawned. 97 | public let arguments: [String] 98 | 99 | /// The environment dictionary that will be set for the program when this 100 | /// command is spawned. 101 | /// 102 | /// If ``Command/inheritEnvironment`` is `true`, this environment dictionary 103 | /// will be merged with the environment of the parent process before the 104 | /// command is spawned. 105 | public let environment: [String: String] 106 | 107 | /// Determines, if the environment of the child process inherits from the 108 | /// parent process's one. 109 | /// 110 | /// ``Command/inheritEnvironment`` is initially `true` but can be set to 111 | /// `false` by calling ``Command/clearEnv()``: 112 | /// 113 | /// ```swift 114 | /// try Command.findInPath(withName: "foo") 115 | /// .clearEnv() 116 | /// .wait() 117 | /// // Command 'foo' is executed without any environment variables. 118 | /// ``` 119 | public let inheritEnvironment: Bool 120 | 121 | /// Determines the child process's working directory. 122 | /// 123 | /// If ``Command/cwd`` is `nil`, the working directory will not be changed. 124 | public let cwd: FilePath? 125 | 126 | internal let stdin: Stdin 127 | internal let stdout: Stdout 128 | internal let stderr: Stderr 129 | 130 | private init( 131 | executablePath: FilePath, 132 | arguments: [String], 133 | environment: [String: String], 134 | inheritEnvironment: Bool, 135 | cwd: FilePath?, 136 | stdin: Stdin, 137 | stdout: Stdout, 138 | stderr: Stderr 139 | ) { 140 | self.executablePath = executablePath 141 | self.arguments = arguments 142 | self.environment = environment 143 | self.inheritEnvironment = inheritEnvironment 144 | self.cwd = cwd 145 | 146 | self.stdin = stdin 147 | self.stdout = stdout 148 | self.stderr = stderr 149 | } 150 | 151 | /// Initializes a command that executes the program at the given 152 | /// `executablePath` when spawned. 153 | /// 154 | /// This initializer checks, if an executable file exists at the given 155 | /// `executablePath`. 156 | /// 157 | /// - Parameters: 158 | /// - executablePath: A `FilePath`, representing the program that should 159 | /// be executed when this command is spawned. 160 | public init(executablePath: FilePath) 161 | where 162 | Stdin == UnspecifiedInputSource, 163 | Stdout == UnspecifiedOutputDestination, 164 | Stderr == UnspecifiedOutputDestination 165 | { 166 | self.init( 167 | executablePath: executablePath, 168 | arguments: [], 169 | environment: [:], 170 | inheritEnvironment: true, 171 | cwd: nil, 172 | stdin: .init(), 173 | stdout: .init(), 174 | stderr: .init() 175 | ) 176 | } 177 | 178 | /// Initializes a command by finding a command line program with the given 179 | /// `name` in one of the directories listed in the `$PATH` environment 180 | /// variable. 181 | /// 182 | /// Searches the directories listed in the `$PATH` environment variable for 183 | /// an executable file with the given `name`, exactly like the shell does, 184 | /// when you type a command in the terminal. 185 | /// 186 | /// This can be used in conjunction with ``Command/executablePath`` to get 187 | /// the path of a command line program: 188 | /// 189 | /// ```swift 190 | /// let path = Command.findInPath(withName: "echo")!.executablePath 191 | /// 192 | /// print(path) 193 | /// // Prints e.g. '/bin/echo' 194 | /// ``` 195 | /// 196 | /// - Parameters: 197 | /// - name: The name of the command line program to search for in `$PATH`. 198 | /// - Returns: An initialized command with the found program in 199 | /// ``Command/executablePath``, or `nil`, if no program with the 200 | /// given `name` could be found. 201 | public static func findInPath(withName name: String) -> Command? 202 | where 203 | Stdin == UnspecifiedInputSource, 204 | Stdout == UnspecifiedOutputDestination, 205 | Stderr == UnspecifiedOutputDestination 206 | { 207 | let nameWithExtension: String 208 | if !Self.executableExtension.isEmpty 209 | && !name.hasSuffix(Self.executableExtension) 210 | { 211 | nameWithExtension = name + Self.executableExtension 212 | } else { 213 | nameWithExtension = name 214 | } 215 | 216 | guard 217 | let environmentPath = 218 | ProcessInfo.processInfo.environment[Self.pathVariable] 219 | else { 220 | return nil 221 | } 222 | 223 | guard 224 | let executablePath = 225 | environmentPath.split(separator: Self.pathSeparator).lazy 226 | .compactMap(FilePath.init(substring:)) 227 | .map({ $0.appending(nameWithExtension) }) 228 | .first(where: { 229 | FileManager.default.isExecutableFile(atPath: $0.string) 230 | }) 231 | else { 232 | return nil 233 | } 234 | 235 | return .init( 236 | executablePath: executablePath, 237 | arguments: [], 238 | environment: [:], 239 | inheritEnvironment: true, 240 | cwd: nil, 241 | stdin: .init(), 242 | stdout: .init(), 243 | stderr: .init() 244 | ) 245 | } 246 | 247 | /// Adds the provided list of arguments in order to the end of the current 248 | /// argument list. 249 | /// 250 | /// - Parameters: 251 | /// - newArguments: An `Array` of argument strings, that will be added to 252 | /// the current arguments. 253 | /// - Returns: A new command instance with the updated argument list. 254 | public __consuming func addArguments(_ newArguments: [String]) -> Self { 255 | .init( 256 | executablePath: self.executablePath, 257 | arguments: self.arguments + newArguments, 258 | environment: self.environment, 259 | inheritEnvironment: self.inheritEnvironment, 260 | cwd: self.cwd, 261 | stdin: self.stdin, 262 | stdout: self.stdout, 263 | stderr: self.stderr 264 | ) 265 | } 266 | 267 | /// Adds the provided list of arguments in order to the end of the current 268 | /// argument list. 269 | /// 270 | /// - Parameters: 271 | /// - newArguments: One or more argument strings, that will be added to 272 | /// the current arguments. 273 | /// - Returns: A new command instance with the updated argument list. 274 | public __consuming func addArguments(_ newArguments: String...) -> Self { 275 | self.addArguments(newArguments) 276 | } 277 | 278 | /// Adds the provided argument to the end of the current argument list. 279 | /// 280 | /// - Parameters: 281 | /// - newArguments: An argument string, that will be added to the current 282 | /// arguments. 283 | /// - Returns: A new command instance with the updated argument list. 284 | public __consuming func addArgument(_ newArgument: String) -> Self { 285 | self.addArguments(newArgument) 286 | } 287 | 288 | @available(*, deprecated, renamed: "setEnvVariable") 289 | public __consuming func addEnvVariable(key: String, value: String) -> Self { 290 | self.setEnvVariable(key: key, value: value) 291 | } 292 | 293 | /// Adds the provided environment variable to the current environment 294 | /// dictionary, or updates an already existing environment variable. 295 | /// 296 | /// - Parameters: 297 | /// - key: The name of the environment variable to set. 298 | /// - value: The value set to the environment variable. 299 | /// - Returns: A new command instance with the updated environment. 300 | public __consuming func setEnvVariable(key: String, value: String) -> Self { 301 | var newEnvironment = self.environment 302 | 303 | newEnvironment[key] = value 304 | 305 | return .init( 306 | executablePath: self.executablePath, 307 | arguments: self.arguments, 308 | environment: newEnvironment, 309 | inheritEnvironment: self.inheritEnvironment, 310 | cwd: self.cwd, 311 | stdin: self.stdin, 312 | stdout: self.stdout, 313 | stderr: self.stderr 314 | ) 315 | } 316 | 317 | @available(*, deprecated, renamed: "setEnvVariables") 318 | public __consuming func addEnvVariables( 319 | _ newEnvVariables: [String: String] 320 | ) -> Self { 321 | self.setEnvVariables(newEnvVariables) 322 | } 323 | 324 | /// Merges the given environment dictionary with the current environment. 325 | /// 326 | /// If there are variables with the same name, the values of the given 327 | /// dictionary are used. 328 | /// 329 | /// - Parameters: 330 | /// - newEnvVariables: A `Dictionary` containing the environment variables 331 | /// that should be updated/added to the current 332 | /// environment. 333 | /// - Returns: A new command instance with the updated environment. 334 | public __consuming func setEnvVariables( 335 | _ newEnvVariables: [String: String] 336 | ) -> Self { 337 | .init( 338 | executablePath: self.executablePath, 339 | arguments: self.arguments, 340 | environment: self.environment.merging(newEnvVariables) { $1 }, 341 | inheritEnvironment: self.inheritEnvironment, 342 | cwd: self.cwd, 343 | stdin: self.stdin, 344 | stdout: self.stdout, 345 | stderr: self.stderr 346 | ) 347 | } 348 | 349 | /// Clears the current environment dictionary and sets 350 | /// ``Command/inheritEnvironment`` to `false`. 351 | /// 352 | /// This method should be used if the child process should not inherit the 353 | /// environment of the parent process: 354 | /// 355 | /// ```swift 356 | /// try Command.findInPath(withName: "foo") 357 | /// .clearEnv() 358 | /// .wait() 359 | /// // Command 'foo' is executed without any environment variables. 360 | /// ``` 361 | /// 362 | /// - Returns: A new command instance with a cleared environment. 363 | public __consuming func clearEnv() -> Self { 364 | .init( 365 | executablePath: self.executablePath, 366 | arguments: self.arguments, 367 | environment: [:], 368 | inheritEnvironment: false, 369 | cwd: self.cwd, 370 | stdin: self.stdin, 371 | stdout: self.stdout, 372 | stderr: self.stderr 373 | ) 374 | } 375 | 376 | /// Sets the working directory of the child process to the given path. 377 | /// 378 | /// - Parameters: 379 | /// - newCWD: A `FilePath`, representing the working directory of the 380 | /// child process. 381 | /// - Returns: A new command instance with the given cwd. 382 | public __consuming func setCWD(_ newCWD: FilePath) -> Self { 383 | .init( 384 | executablePath: self.executablePath, 385 | arguments: self.arguments, 386 | environment: self.environment, 387 | inheritEnvironment: self.inheritEnvironment, 388 | cwd: newCWD, 389 | stdin: self.stdin, 390 | stdout: self.stdout, 391 | stderr: self.stderr 392 | ) 393 | } 394 | 395 | /// Sets a different source for the child process's stdin handle. 396 | /// 397 | /// This can be used to give the child process input from a file, from the 398 | /// parent process itself, or even from the output of another child process: 399 | /// 400 | /// ```swift 401 | /// let catProcess = try Command.findInPath(withName: "cat")! 402 | /// .setStdin(.read(fromFile: "SomeFile.txt")) 403 | /// .setStdout(.pipe) 404 | /// .spawn() 405 | /// 406 | /// let grepProcess = try Command.findInPath(withName: "grep")! 407 | /// .addArgument("Ba") 408 | /// .setStdin(.pipe(from: catProcess.stdout)) 409 | /// .setStdout(.pipe) 410 | /// .spawn() 411 | /// 412 | /// for try await line in grepProcess.stdout.lines { 413 | /// print(line) 414 | /// } 415 | /// // Prints all lines in 'SomeFile.txt' containing 'Ba' 416 | /// ``` 417 | /// 418 | /// - Parameters: 419 | /// - newStdin: An ``InputSource``, corresponding to the method of input, 420 | /// the child process should use. 421 | /// - Returns: A new command instance with the given stdin source set. 422 | public __consuming func setStdin( 423 | _ newStdin: NewStdin 424 | ) -> Command { 425 | .init( 426 | executablePath: self.executablePath, 427 | arguments: self.arguments, 428 | environment: self.environment, 429 | inheritEnvironment: self.inheritEnvironment, 430 | cwd: self.cwd, 431 | stdin: newStdin, 432 | stdout: self.stdout, 433 | stderr: self.stderr 434 | ) 435 | } 436 | 437 | /// Sets a different destination for the child process's stdout handle. 438 | /// 439 | /// This can be used to channel the child process's output into a file, or 440 | /// to read it directly from the parent process: 441 | /// 442 | /// ```swift 443 | /// let output = try await Command.findInPath(withName: "echo")! 444 | /// .addArgument("Foo") 445 | /// .setStdout(.pipe) 446 | /// .output 447 | /// 448 | /// print(output.stdout) 449 | /// // Prints 'Foo\n' 450 | /// ``` 451 | /// 452 | /// - Parameters: 453 | /// - newStdout: An ``OutputDestination``, corresponding to the method of 454 | /// output, the child process should use. 455 | /// - Returns: A new command instance with the given stdout destination set. 456 | public __consuming func setStdout( 457 | _ newStdout: NewStdout 458 | ) -> Command { 459 | .init( 460 | executablePath: self.executablePath, 461 | arguments: self.arguments, 462 | environment: self.environment, 463 | inheritEnvironment: self.inheritEnvironment, 464 | cwd: self.cwd, 465 | stdin: self.stdin, 466 | stdout: newStdout, 467 | stderr: self.stderr 468 | ) 469 | } 470 | 471 | /// Sets a different destination for the child process's stderr handle. 472 | /// 473 | /// This can be used to channel the child process's error output into a 474 | /// file, or to read it directly from the parent process: 475 | /// 476 | /// ```swift 477 | /// let output = try await Command.findInPath(withName: "cat")! 478 | /// .addArgument("non_existing.txt") 479 | /// .setStderr(.pipe) 480 | /// .output 481 | /// 482 | /// print(output.stderr) 483 | /// // Prints 'cat: non_existing.txt: No such file or directory\n' 484 | /// // or similar 485 | /// ``` 486 | /// 487 | /// - Parameters: 488 | /// - newStderr: An ``OutputDestination``, corresponding to the method of 489 | /// error output, the child process should use. 490 | /// - Returns: A new command instance with the given stderr destination set. 491 | public __consuming func setStderr( 492 | _ newStderr: NewStderr 493 | ) -> Command { 494 | .init( 495 | executablePath: self.executablePath, 496 | arguments: self.arguments, 497 | environment: self.environment, 498 | inheritEnvironment: self.inheritEnvironment, 499 | cwd: self.cwd, 500 | stdin: self.stdin, 501 | stdout: self.stdout, 502 | stderr: newStderr 503 | ) 504 | } 505 | 506 | /// Sets a different destination for the child process's stdout and stderr 507 | /// handle. 508 | /// 509 | /// This can be used to channel the child process's output and error output 510 | /// into a file, or to read it directly from the parent process: 511 | /// 512 | /// ```swift 513 | /// let status = try await Command.findInPath(withName: "cat")! 514 | /// .addArguments("existing.txt", "non_existing.txt") 515 | /// .setOutputs(.write(toFile: "output.txt")) 516 | /// .status 517 | /// 518 | /// // Writes the contents of 'existing.txt' and 519 | /// // 'cat: non_existing.txt: No such file or directory' 520 | /// // or similar to 'output.txt' 521 | /// 522 | /// assert(!status.terminatedSuccessfully) 523 | /// ``` 524 | /// 525 | /// - Parameters: 526 | /// - newStderr: An ``OutputDestination``, corresponding to the method of 527 | /// error output, the child process should use. 528 | /// - Returns: A new command instance with the given stderr destination set. 529 | public __consuming func setOutputs( 530 | _ newOutput: NewOutputDestination 531 | ) -> Command { 532 | .init( 533 | executablePath: self.executablePath, 534 | arguments: self.arguments, 535 | environment: self.environment, 536 | inheritEnvironment: self.inheritEnvironment, 537 | cwd: self.cwd, 538 | stdin: self.stdin, 539 | stdout: newOutput, 540 | stderr: newOutput 541 | ) 542 | } 543 | 544 | /// Executes the command as a child process, returning a handle to it. 545 | /// 546 | /// By default, stdin, stdout, and stderr are inherited from the parent 547 | /// process. 548 | /// 549 | /// - Returns: A handle to the child process. 550 | public func spawn() throws -> ChildProcess { 551 | try .spawn(withCommand: self) 552 | } 553 | 554 | /// Executes the command as a child process, waits for it to complete, and 555 | /// returns its exit status. 556 | /// 557 | /// This blocks the current thread until the child process has terminated. 558 | /// 559 | /// By default, stdin, stdout, and stderr are inherited from the parent 560 | /// process. 561 | /// 562 | /// - Returns: The exit status of the child process. 563 | @discardableResult 564 | public func wait() throws -> ExitStatus { 565 | try self.spawn().wait() 566 | } 567 | 568 | /// Executes the command as a child process, waits for it to complete, and 569 | /// returns its exit status. 570 | /// 571 | /// This doesn't block the current thread and allows other tasks to run 572 | /// before the child process terminates. 573 | /// 574 | /// By default, stdin, stdout, and stderr are inherited from the parent 575 | /// process. 576 | public var status: ExitStatus { 577 | get async throws { 578 | try await self.spawn().status 579 | } 580 | } 581 | } 582 | 583 | extension Command where Stdout == UnspecifiedOutputDestination { 584 | /// Executes the command as a child process, waits for it to complete, and 585 | /// returns its collected output. 586 | /// 587 | /// This blocks the current thread until the child process has terminated. 588 | /// 589 | /// By default, stdout and stderr are captured (and used to provide the re- 590 | /// sulting output), while stdin is connected to `/dev/null`. 591 | /// 592 | /// - Note: This method can only be called when stdout is either still 593 | /// unspecfied or when it is piped. 594 | /// 595 | /// - Returns: The collected output of the child process. 596 | public func waitForOutput() throws -> ProcessOutput { 597 | guard Stdin.self == UnspecifiedInputSource.self else { 598 | guard Stderr.self == UnspecifiedInputSource.self else { 599 | return try self.setStdout(.pipe) 600 | .spawn() 601 | .waitWithOutput() 602 | } 603 | 604 | return try self.setStdout(.pipe) 605 | .setStderr(.pipe) 606 | .spawn() 607 | .waitWithOutput() 608 | } 609 | 610 | guard Stderr.self == UnspecifiedInputSource.self else { 611 | return 612 | try self 613 | .setStdin(.null) 614 | .setStdout(.pipe) 615 | .spawn() 616 | .waitWithOutput() 617 | } 618 | 619 | return try self.setStdin(.null) 620 | .setStdout(.pipe) 621 | .setStderr(.pipe) 622 | .spawn() 623 | .waitWithOutput() 624 | } 625 | 626 | /// Executes the command as a child process, waits for it to complete, and 627 | /// returns its collected stdout data. 628 | /// 629 | /// This blocks the current thread until the child process has terminated. 630 | /// 631 | /// By default, stdout and stderr are captured (and used to provide the re- 632 | /// sulting output), while stdin is connected to `/dev/null`. 633 | /// 634 | /// - Note: This method can only be called when stdout is either still 635 | /// unspecfied or when it is piped. 636 | /// 637 | /// - Returns: The collected stdout data of the child process. 638 | public func waitForOutputData() throws -> Data { 639 | try self.waitForOutput().stdoutData 640 | } 641 | 642 | /// Executes the command as a child process, waits for it to complete, and 643 | /// returns its collected output. 644 | /// 645 | /// This doesn't block the current thread and allows other tasks to run 646 | /// before the child process terminates. 647 | /// 648 | /// By default, stdout and stderr are captured (and used to provide the re- 649 | /// sulting output), while stdin is connected to `/dev/null`. 650 | /// 651 | /// - Note: This accessor can only be called when stdout is either still 652 | /// unspecfied or when it is piped. 653 | public var output: ProcessOutput { 654 | get async throws { 655 | guard Stdin.self == UnspecifiedInputSource.self else { 656 | guard Stderr.self == UnspecifiedInputSource.self else { 657 | return try await self.setStdout(.pipe) 658 | .spawn() 659 | .output 660 | } 661 | return try await self.setStdout(.pipe) 662 | .setStderr(.pipe) 663 | .spawn() 664 | .output 665 | } 666 | guard Stderr.self == UnspecifiedInputSource.self else { 667 | return try await self.setStdin(.null) 668 | .setStdout(.pipe) 669 | .spawn() 670 | .output 671 | } 672 | return try await self.setStdin(.null) 673 | .setStdout(.pipe) 674 | .setStderr(.pipe) 675 | .spawn() 676 | .output 677 | } 678 | } 679 | 680 | /// Executes the command as a child process, waits for it to complete, and 681 | /// returns its collected stdout data. 682 | /// 683 | /// This doesn't block the current thread and allows other tasks to run 684 | /// before the child process terminates. 685 | /// 686 | /// By default, stdout and stderr are captured (and used to provide the re- 687 | /// sulting output), while stdin is connected to `/dev/null`. 688 | /// 689 | /// - Note: This accessor can only be called when stdout is either still 690 | /// unspecfied or when it is piped. 691 | public var outputData: Data { 692 | get async throws { 693 | try await self.output.stdoutData 694 | } 695 | } 696 | } 697 | 698 | extension Command where Stdout == PipeOutputDestination { 699 | /// Executes the command as a child process, waits for it to complete, and 700 | /// returns its collected output. 701 | /// 702 | /// This blocks the current thread until the child process has terminated. 703 | /// 704 | /// By default, stdout and stderr are captured (and used to provide the re- 705 | /// sulting output), while stdin is connected to `/dev/null`. 706 | /// 707 | /// - Note: This method can only be called when stdout is either still 708 | /// unspecfied or when it is piped. 709 | /// 710 | /// - Returns: The collected output of the child process. 711 | public func waitForOutput() throws -> ProcessOutput { 712 | guard Stdin.self == UnspecifiedInputSource.self else { 713 | guard Stderr.self == UnspecifiedInputSource.self else { 714 | return try self.spawn() 715 | .waitWithOutput() 716 | } 717 | return try self.setStderr(.pipe) 718 | .spawn() 719 | .waitWithOutput() 720 | } 721 | guard Stderr.self == UnspecifiedInputSource.self else { 722 | return try self.setStdin(.null) 723 | .spawn() 724 | .waitWithOutput() 725 | } 726 | return try self.setStdin(.null) 727 | .setStderr(.pipe) 728 | .spawn() 729 | .waitWithOutput() 730 | } 731 | 732 | /// Executes the command as a child process, waits for it to complete, and 733 | /// returns its collected stdout data. 734 | /// 735 | /// This blocks the current thread until the child process has terminated. 736 | /// 737 | /// By default, stdout and stderr are captured (and used to provide the re- 738 | /// sulting output), while stdin is connected to `/dev/null`. 739 | /// 740 | /// - Note: This method can only be called when stdout is either still 741 | /// unspecfied or when it is piped. 742 | /// 743 | /// - Returns: The collected stdout data of the child process. 744 | public func waitForOutputData() throws -> Data { 745 | try self.waitForOutput().stdoutData 746 | } 747 | 748 | /// Executes the command as a child process, waits for it to complete, and 749 | /// returns its collected output. 750 | /// 751 | /// This doesn't block the current thread and allows other tasks to run 752 | /// before the child process terminates. 753 | /// 754 | /// By default, stdout and stderr are captured (and used to provide the re- 755 | /// sulting output), while stdin is connected to `/dev/null`. 756 | /// 757 | /// - Note: This accessor can only be called when stdout is either still 758 | /// unspecfied or when it is piped. 759 | public var output: ProcessOutput { 760 | get async throws { 761 | guard Stdin.self == UnspecifiedInputSource.self else { 762 | guard Stderr.self == UnspecifiedInputSource.self else { 763 | return try await self.spawn() 764 | .output 765 | } 766 | return try await self.setStderr(.pipe) 767 | .spawn() 768 | .output 769 | } 770 | guard Stderr.self == UnspecifiedInputSource.self else { 771 | return try await self.setStdin(.null) 772 | .spawn() 773 | .output 774 | } 775 | return try await self.setStdin(.null) 776 | .setStderr(.pipe) 777 | .spawn() 778 | .output 779 | } 780 | } 781 | 782 | /// Executes the command as a child process, waits for it to complete, and 783 | /// returns its collected stdout data. 784 | /// 785 | /// This doesn't block the current thread and allows other tasks to run 786 | /// before the child process terminates. 787 | /// 788 | /// By default, stdout and stderr are captured (and used to provide the re- 789 | /// sulting output), while stdin is connected to `/dev/null`. 790 | /// 791 | /// - Note: This accessor can only be called when stdout is either still 792 | /// unspecfied or when it is piped. 793 | public var outputData: Data { 794 | get async throws { 795 | try await self.output.stdoutData 796 | } 797 | } 798 | } 799 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/Either.swift: -------------------------------------------------------------------------------- 1 | /// The enum ``Either`` with cases ``Either/first(_:)`` and 2 | /// ``Either/second(_:)`` is a general purpose sum type with two cases. 3 | /// 4 | /// The ``Either`` type is symmetric and treats its cases the same way, without 5 | /// preference (for representing success or error, use the `Result` enum from 6 | /// Swift's standard library instead). 7 | public enum Either { 8 | /// A value of type `First`. 9 | case first(First) 10 | /// A value of type `Second`. 11 | case second(Second) 12 | 13 | /// A convenience initializer for when no type context is available 14 | public init(first: First) { 15 | self = .first(first) 16 | } 17 | 18 | /// A convenience initializer for when no type context is available 19 | public init(second: Second) { 20 | self = .second(second) 21 | } 22 | 23 | /// Returns `true` if the value is the ``Either/first(_:)`` case. 24 | public var isFirst: Bool { 25 | guard case .first = self else { 26 | return false 27 | } 28 | return true 29 | } 30 | 31 | /// Converts the first case of `Either` to `F?`. 32 | public var first: First? { 33 | guard case .first(let first) = self else { 34 | return nil 35 | } 36 | return first 37 | } 38 | 39 | /// Returns `true` if the value is the ``Either/second(_:)`` case. 40 | public var isSecond: Bool { 41 | guard case .second = self else { 42 | return false 43 | } 44 | return true 45 | } 46 | 47 | /// Converts the second case of `Either` to `S?`. 48 | public var second: Second? { 49 | guard case .second(let second) = self else { 50 | return nil 51 | } 52 | return second 53 | } 54 | 55 | /// Converts `Either` to `Either`. 56 | public var flipped: Either { 57 | switch self { 58 | case .first(let first): 59 | return .second(first) 60 | case .second(let second): 61 | return .first(second) 62 | } 63 | } 64 | 65 | /// Applies the closure `transformFirst` to the value in case 66 | /// ``Either/first(_:)`` if it is present rewrapping the result in 67 | /// ``Either/first(_:)``. 68 | /// 69 | /// - Parameter transformFirst: A closure transforming a value of type 70 | /// `First` into a value of type `NewFirst`. 71 | /// - Returns: A new instance of ``Either``, containing either the result 72 | /// of the `transformFirst` closure or the value in case 73 | /// ``Either/second(_:)``. 74 | public func mapFirst( 75 | _ transformFirst: (First) throws -> NewFirst 76 | ) rethrows -> Either { 77 | switch self { 78 | case .first(let first): 79 | return .first(try transformFirst(first)) 80 | case .second(let second): 81 | return .second(second) 82 | } 83 | } 84 | 85 | /// Applies the closure `transformSecond` to the value in case 86 | /// ``Either/second(_:)`` if it is present rewrapping the result in 87 | /// ``Either/second(_:)``. 88 | /// 89 | /// - Parameter transformSecond: A closure transforming a value of type 90 | /// `Second` into a value of type `NewSecond`. 91 | /// - Returns: A new instance of ``Either``, containing either the value in 92 | /// case ``Either/first(_:)`` or the result of the 93 | /// `transformSecond` closure. 94 | public func mapSecond( 95 | _ transformSecond: (Second) throws -> NewSecond 96 | ) rethrows -> Either { 97 | switch self { 98 | case .first(let first): 99 | return .first(first) 100 | case .second(let second): 101 | return .second(try transformSecond(second)) 102 | } 103 | } 104 | } 105 | 106 | extension Either: Equatable where First: Equatable, Second: Equatable {} 107 | extension Either: Hashable where First: Hashable, Second: Hashable {} 108 | extension Either: Sendable where First: Sendable, Second: Sendable {} 109 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/ExitStatus.swift: -------------------------------------------------------------------------------- 1 | /// Describes the result of a child process after it has terminated. 2 | public struct ExitStatus: Equatable, Sendable { 3 | private enum Status: Equatable, Sendable { 4 | case success 5 | #if os(Windows) 6 | case terminatedBySignal 7 | #else 8 | case terminatedBySignal(Int32) 9 | #endif 10 | case error(Int32) 11 | } 12 | 13 | private let status: Status 14 | 15 | private init(status: Status) { 16 | self.status = status 17 | } 18 | 19 | 20 | internal static let success = ExitStatus(status: .success) 21 | 22 | #if os(Windows) 23 | internal static let terminatedBySignal = 24 | ExitStatus(status: .terminatedBySignal) 25 | #else 26 | internal static func terminatedBySignal(signal: Int32) -> ExitStatus { 27 | .init(status: .terminatedBySignal(signal)) 28 | } 29 | #endif 30 | 31 | internal static func error(exitCode: Int32) -> ExitStatus { 32 | .init(status: .error(exitCode)) 33 | } 34 | 35 | 36 | /// Indicates, if termination of the child process was successful. 37 | /// 38 | /// Signal termination is not considered a success, and success is defined 39 | /// as a zero exit status. 40 | public var terminatedSuccessfully: Bool { self.status == .success } 41 | 42 | /// Indicates, if the child process was terminated by a signal. 43 | public var wasTerminatedBySignal: Bool { 44 | if case .terminatedBySignal = self.status { 45 | return true 46 | } else { 47 | return false 48 | } 49 | } 50 | 51 | /// Returns the exit code of the child process, if any. 52 | /// 53 | /// In Unix terms the return value is the exit code: the value passed to 54 | /// `exit`, if the process finished by calling `exit`. Note that on Unix the 55 | /// exit code is truncated to 8 bits, and that values that didn’t come from 56 | /// a program’s call to `exit` may be invented by the runtime system (often, 57 | /// for example, 255, 254, 127 or 126). 58 | /// 59 | /// This will return `nil` if the child process terminated successfully 60 | /// (this can also be checked using ``ExitStatus/terminatedSuccessfully``) 61 | /// or if it was terminated by a signal. 62 | public var exitCode: Int32? { 63 | if case let .error(exitCode) = self.status { 64 | return exitCode 65 | } else { 66 | return nil 67 | } 68 | } 69 | 70 | #if os(Windows) 71 | @available(Windows, unavailable) 72 | public var terminationSignal: Int32? { 73 | nil 74 | } 75 | #else 76 | public var terminationSignal: Int32? { 77 | if case let .terminatedBySignal(signal) = self.status { 78 | return signal 79 | } else { 80 | return nil 81 | } 82 | } 83 | #endif 84 | } 85 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/FileHandle+Async.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | fileprivate final actor IOActor { 4 | #if !os(Windows) 5 | fileprivate func read( 6 | from fd: Int32, 7 | into buffer: UnsafeMutableRawBufferPointer 8 | ) async throws -> Int { 9 | while true { 10 | #if canImport(Darwin) 11 | let read = Darwin.read 12 | #elseif canImport(Glibc) 13 | let read = Glibc.read 14 | #else 15 | #error("Unsupported platform!") 16 | #endif 17 | let amount = read(fd, buffer.baseAddress, buffer.count) 18 | if amount >= 0 { 19 | return amount 20 | } 21 | let posixErrno = errno 22 | if errno != EINTR { 23 | throw NSError( 24 | domain: NSPOSIXErrorDomain, 25 | code: Int(posixErrno), 26 | userInfo: [:] 27 | ) 28 | } 29 | } 30 | } 31 | #endif 32 | 33 | fileprivate func read( 34 | from handle: FileHandle, 35 | into buffer: UnsafeMutableRawBufferPointer 36 | ) async throws -> Int { 37 | try await withUnsafeThrowingContinuation { continuation in 38 | handle.readabilityHandler = { handle in 39 | handle.readabilityHandler = nil 40 | 41 | let data: Data 42 | if #available(macOS 10.15.4, *) { 43 | do { 44 | guard let _data = 45 | try handle.read(upToCount: buffer.count) else { 46 | continuation.resume(returning: 0) 47 | return 48 | } 49 | 50 | data = _data 51 | } catch { 52 | continuation.resume(throwing: error) 53 | return 54 | } 55 | } else { 56 | data = handle.readData(ofLength: buffer.count) 57 | } 58 | 59 | data.copyBytes(to: buffer) 60 | continuation.resume(returning: data.count) 61 | } 62 | } 63 | } 64 | 65 | fileprivate static let `default` = IOActor() 66 | } 67 | 68 | 69 | @usableFromInline 70 | internal struct _AsyncBytesBuffer { 71 | private struct Header { 72 | var readFunction: ((inout _AsyncBytesBuffer) async throws -> Int)? = nil 73 | var finished = false 74 | } 75 | 76 | private class Storage: ManagedBuffer { 77 | var finished: Bool { 78 | get { 79 | self.header.finished 80 | } 81 | set { 82 | self.header.finished = newValue 83 | } 84 | } 85 | } 86 | 87 | fileprivate var readFunction: (inout Self) async throws -> Int { 88 | get { 89 | (self.storage as! Storage).header.readFunction! 90 | } 91 | set { 92 | (self.storage as! Storage).header.readFunction = newValue 93 | } 94 | } 95 | 96 | fileprivate var baseAddress: UnsafeMutableRawPointer { 97 | (self.storage as! Storage).withUnsafeMutablePointerToElements { 98 | .init($0) 99 | } 100 | } 101 | 102 | fileprivate var capacity: Int { 103 | (self.storage as! Storage).capacity 104 | } 105 | 106 | private var storage: AnyObject? = nil 107 | 108 | @usableFromInline 109 | internal var _nextPointer: UnsafeMutableRawPointer 110 | @usableFromInline 111 | internal var _endPointer: UnsafeMutableRawPointer 112 | 113 | fileprivate init(_capacity capacity: Int) { 114 | let s = Storage.create(minimumCapacity: capacity) { _ in 115 | return Header(readFunction: nil, finished: false) 116 | } 117 | 118 | self.storage = s 119 | self._nextPointer = s.withUnsafeMutablePointerToElements { .init($0) } 120 | self._endPointer = self._nextPointer 121 | } 122 | 123 | @inline(never) @usableFromInline 124 | internal mutating func _reloadBufferAndNext() async throws -> UInt8? { 125 | let storage = self.storage as! Storage 126 | if storage.finished { 127 | return nil 128 | } 129 | 130 | try Task.checkCancellation() 131 | 132 | self._nextPointer = storage.withUnsafeMutablePointerToElements { 133 | .init($0) 134 | } 135 | 136 | do { 137 | let readSize = try await self.readFunction(&self) 138 | if readSize == 0 { 139 | storage.finished = true 140 | } 141 | } catch { 142 | storage.finished = true 143 | throw error 144 | } 145 | 146 | return try await self._next() 147 | } 148 | 149 | @inlinable @inline(__always) 150 | internal mutating func _next() async throws -> UInt8? { 151 | if _fastPath(self._nextPointer != self._endPointer) { 152 | let byte = self._nextPointer.load(fromByteOffset: 0, as: UInt8.self) 153 | self._nextPointer = self._nextPointer + 1 154 | return byte 155 | } 156 | 157 | return try await self._reloadBufferAndNext() 158 | } 159 | } 160 | 161 | extension FileHandle { 162 | @usableFromInline 163 | internal struct CustomAsyncBytes: AsyncSequence { 164 | @usableFromInline 165 | internal typealias Element = UInt8 166 | 167 | @usableFromInline 168 | internal struct AsyncIterator: AsyncIteratorProtocol { 169 | static let bufferSize = 16384 170 | 171 | @usableFromInline 172 | internal var _buffer: _AsyncBytesBuffer 173 | 174 | fileprivate init(file: FileHandle) { 175 | self._buffer = _AsyncBytesBuffer(_capacity: Self.bufferSize) 176 | 177 | #if !os(Windows) 178 | let fileDescriptor = file.fileDescriptor 179 | #endif 180 | 181 | self._buffer.readFunction = { buf in 182 | buf._nextPointer = buf.baseAddress 183 | 184 | let capacity = buf.capacity 185 | 186 | let bufPtr = UnsafeMutableRawBufferPointer( 187 | start: buf._nextPointer, 188 | count: capacity 189 | ) 190 | 191 | #if os(Windows) 192 | let readSize = try await IOActor.default.read( 193 | from: file, 194 | into: bufPtr 195 | ) 196 | #else 197 | let readSize: Int 198 | if fileDescriptor >= 0 { 199 | readSize = try await IOActor.default.read( 200 | from: fileDescriptor, 201 | into: bufPtr 202 | ) 203 | } else { 204 | readSize = try await IOActor.default.read( 205 | from: file, 206 | into: bufPtr 207 | ) 208 | } 209 | #endif 210 | 211 | buf._endPointer = buf._nextPointer + readSize 212 | return readSize 213 | } 214 | } 215 | 216 | @inlinable @inline(__always) 217 | public mutating func next() async throws -> UInt8? { 218 | return try await self._buffer._next() 219 | } 220 | } 221 | 222 | var handle: FileHandle 223 | 224 | fileprivate init(file: FileHandle) { 225 | handle = file 226 | } 227 | 228 | @usableFromInline 229 | internal func makeAsyncIterator() -> AsyncIterator { 230 | .init(file: handle) 231 | } 232 | } 233 | 234 | internal var customBytes: CustomAsyncBytes { 235 | return CustomAsyncBytes(file: self) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/FilePath+extension.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | import Foundation 3 | 4 | extension FilePath { 5 | /// Convenience initializer for creating a `FilePath` from a `Substring`. 6 | public init(substring: Substring) { 7 | self.init(String(substring)) 8 | } 9 | 10 | 11 | /// Creates a `Foundation.URL` from this `FilePath`. 12 | public var url: URL { 13 | #if canImport(Darwin) && swift(>=5.7) 14 | if #available(macOS 13.0, *) { 15 | return .init(filePath: self.string) 16 | } else { 17 | return .init(fileURLWithPath: self.string) 18 | } 19 | #else 20 | .init(fileURLWithPath: self.string) 21 | #endif 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/InputSource.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Darwin) && swift(>=5.7) 2 | import Foundation 3 | #else 4 | @preconcurrency import Foundation 5 | #endif 6 | 7 | @preconcurrency import SystemPackage 8 | 9 | /// Describes the type of input source of a child process. 10 | public protocol InputSource: Equatable, Sendable { 11 | /// Returns either a `FileHandle` or a `Pipe` that will function as the 12 | /// stdin handle of the child process. 13 | var processInput: Either { get throws } 14 | } 15 | 16 | /// An input source that is yet unspecified and acts as if it was an 17 | /// ``InheritInputSource``. 18 | public struct UnspecifiedInputSource: InputSource { 19 | internal init() {} 20 | 21 | /// Returns either a`FileHandle` or a `Pipe` that will function as the 22 | /// stdin handle of the child process. 23 | public var processInput: Either { 24 | .first(.standardInput) 25 | } 26 | } 27 | 28 | /// An input source that connects the child process's stdin to `/dev/null`. 29 | public struct NullInputSource: InputSource { 30 | /// Returns either a`FileHandle` or a `Pipe` that will function as the 31 | /// stdin handle of the child process. 32 | public var processInput: Either { 33 | .first(.nullDevice) 34 | } 35 | } 36 | 37 | /// An input source that inherits the stdin handle of the parent process. 38 | public struct InheritInputSource: InputSource { 39 | /// Returns either a`FileHandle` or a `Pipe` that will function as the 40 | /// stdin handle of the child process. 41 | public var processInput: Either { 42 | .first(.standardInput) 43 | } 44 | } 45 | 46 | /// An input source that arranges a pipe between the parent and child processes. 47 | public struct PipeInputSource: InputSource { 48 | /// Indicates, if the created pipe should be implicitly closed when 49 | /// ``ChildProcess/wait()`` or ``ChildProcess/waitWithOutput()`` (or 50 | /// one of their async counterparts ``ChildProcess/status`` or 51 | /// ``ChildProcess/output``) is called on a ``ChildProcess`` instance. 52 | public let closeImplicitly: Bool 53 | 54 | internal init(closeImplicitly: Bool = true) { 55 | self.closeImplicitly = closeImplicitly 56 | } 57 | 58 | /// Returns either a`FileHandle` or a `Pipe` that will function as the 59 | /// stdin handle of the child process. 60 | public var processInput: Either { 61 | .second(.init()) 62 | } 63 | } 64 | 65 | /// An input source that lets the stdin of the child process read from a file. 66 | public struct FileInputSource: InputSource { 67 | /// The `FilePath` where the stdin of the child process should read from. 68 | public let path: FilePath 69 | 70 | internal init(path: FilePath) { 71 | self.path = path 72 | } 73 | 74 | /// Returns either a`FileHandle` or a `Pipe` that will function as the 75 | /// stdin handle of the child process. 76 | /// 77 | /// - Throws: An error if a file at the given ``FileInputSource/path`` does 78 | /// not exist. 79 | public var processInput: Either { 80 | get throws { 81 | try .first(.init(forReadingFrom: self.path.url)) 82 | } 83 | } 84 | } 85 | 86 | /// An input source that arranges a pipe from the output of another child 87 | /// process to this child process's stdin. 88 | public struct PipeFromInputSource: InputSource { 89 | internal let pipe: Pipe 90 | 91 | internal init( 92 | handle: ChildProcess.OutputHandle 93 | ) { 94 | self.pipe = handle.pipe 95 | } 96 | 97 | /// Returns either a`FileHandle` or a `Pipe` that will function as the 98 | /// stdin handle of the child process. 99 | public var processInput: Either { 100 | .second(self.pipe) 101 | } 102 | 103 | public static func ==(lhs: Self, rhs: Self) -> Bool { 104 | lhs.pipe === rhs.pipe 105 | } 106 | } 107 | 108 | extension InputSource where Self == NullInputSource { 109 | /// Creates a ``NullInputSource``. 110 | public static var null: Self { .init() } 111 | } 112 | 113 | extension InputSource where Self == InheritInputSource { 114 | /// Creates an ``InheritInputSource``. 115 | public static var inherit: Self { .init() } 116 | } 117 | 118 | extension InputSource where Self == PipeInputSource { 119 | /// Creates a ``PipeInputSource``. 120 | public static var pipe: Self { .init(closeImplicitly: true) } 121 | 122 | /// Creates a ``PipeInputSource``. 123 | /// 124 | /// - Parameters: 125 | /// - closeImplicitly: Indicates, if the created pipe should be implicitly 126 | /// closed when ``ChildProcess/wait()`` or 127 | /// ``ChildProcess/waitWithOutput()`` (or one of their 128 | /// async counterparts ``ChildProcess/status`` or 129 | /// ``ChildProcess/output``) is called on a 130 | /// ``ChildProcess`` instance. 131 | /// - Returns: A newly created ``PipeInputSource``. 132 | public static func pipe(closeImplicitly: Bool) -> Self { 133 | .init(closeImplicitly: closeImplicitly) 134 | } 135 | } 136 | 137 | extension InputSource where Self == FileInputSource { 138 | /// Creates a ``FileInputSource``. 139 | /// 140 | /// - Parameters: 141 | /// - path: The `FilePath` where the stdin of the child process should 142 | /// read from. 143 | /// - Returns: A newly created ``FileInputSource``. 144 | public static func read(fromFile path: FilePath) -> Self { 145 | .init(path: path) 146 | } 147 | } 148 | 149 | extension InputSource where Self == PipeFromInputSource { 150 | /// Creates a ``PipeFromInputSource``. 151 | /// 152 | /// - Parameters: 153 | /// - handle: The ``ChildProcess/OutputHandle`` corresponding to the child 154 | /// process's output, which should be piped to the stdin of this 155 | /// child process. 156 | /// - Returns: A newly created ``PipeFromInputSource``. 157 | public static func pipe( 158 | from handle: ChildProcess.OutputHandle 159 | ) -> Self { 160 | .init(handle: handle) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/OutputDestination.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @preconcurrency import SystemPackage 3 | 4 | /// Describes the type of output destination of a child process. 5 | public protocol OutputDestination: Equatable, Sendable { 6 | /// Returns either a `FileHandle` or a `Pipe` that will function as the 7 | /// stdout or stderr handle of the child process. 8 | /// 9 | /// - Parameters: 10 | /// - type: The type of the output, represented as an instance of 11 | /// ``OutputType``. 12 | /// - Returns: Either a `FileHandle` or a `Pipe` that will be used as the 13 | /// handle for the child process's corresponding output stream. 14 | func processOutput( 15 | forType type: OutputType 16 | ) throws -> Either 17 | } 18 | 19 | /// An output destination that is yet unspecified and acts as if it was an 20 | /// ``InheritOutputDestination``. 21 | public struct UnspecifiedOutputDestination: OutputDestination { 22 | internal init() {} 23 | 24 | /// Returns either a `FileHandle` or a `Pipe` that will function as the 25 | /// stdout or stderr handle of the child process. 26 | /// 27 | /// - Parameters: 28 | /// - type: The type of the output, represented as an instance of 29 | /// ``OutputType``. 30 | /// - Returns: Either a `FileHandle` or a `Pipe` that will be used as the 31 | /// handle for the child process's corresponding output stream. 32 | public func processOutput( 33 | forType type: OutputType 34 | ) throws -> Either { 35 | switch type { 36 | case .stdout: 37 | return .first(.standardOutput) 38 | case .stderr: 39 | return .first(.standardError) 40 | } 41 | } 42 | } 43 | 44 | /// An output destination that connects the child process's stdout or stderr to 45 | /// `/dev/null`. 46 | public struct NullOutputDestination: OutputDestination { 47 | /// Returns either a `FileHandle` or a `Pipe` that will function as the 48 | /// stdout or stderr handle of the child process. 49 | /// 50 | /// - Parameters: 51 | /// - type: The type of the output, represented as an instance of 52 | /// ``OutputType``. 53 | /// - Returns: Either a `FileHandle` or a `Pipe` that will be used as the 54 | /// handle for the child process's corresponding output stream. 55 | public func processOutput( 56 | forType type: OutputType 57 | ) throws -> Either { 58 | .first(.nullDevice) 59 | } 60 | } 61 | 62 | /// An output destination that inherits the stdout or stderr handle of the 63 | /// parent process. 64 | public struct InheritOutputDestination: OutputDestination { 65 | /// Returns either a `FileHandle` or a `Pipe` that will function as the 66 | /// stdout or stderr handle of the child process. 67 | /// 68 | /// - Parameters: 69 | /// - type: The type of the output, represented as an instance of 70 | /// ``OutputType``. 71 | /// - Returns: Either a `FileHandle` or a `Pipe` that will be used as the 72 | /// handle for the child process's corresponding output stream. 73 | public func processOutput( 74 | forType type: OutputType 75 | ) throws -> Either { 76 | switch type { 77 | case .stdout: 78 | return .first(.standardOutput) 79 | case .stderr: 80 | return .first(.standardError) 81 | } 82 | } 83 | } 84 | 85 | /// An output destination that arranges a pipe between the parent and child 86 | /// processes. 87 | public struct PipeOutputDestination: OutputDestination { 88 | /// Returns either a `FileHandle` or a `Pipe` that will function as the 89 | /// stdout or stderr handle of the child process. 90 | /// 91 | /// - Parameters: 92 | /// - type: The type of the output, represented as an instance of 93 | /// ``OutputType``. 94 | /// - Returns: Either a `FileHandle` or a `Pipe` that will be used as the 95 | /// handle for the child process's corresponding output stream. 96 | public func processOutput( 97 | forType type: OutputType 98 | ) throws -> Either { 99 | .second(.init()) 100 | } 101 | } 102 | 103 | /// An output destination that lets the child process's stdout or stderr write 104 | /// or append to a file. 105 | public struct FileOutputDestination: OutputDestination { 106 | /// The `FilePath` where the stdout or stderr of the child process should 107 | /// write to. 108 | public let path: FilePath 109 | /// Indicates, if the file should be appended to rather than being 110 | /// overwritten. 111 | public let shouldAppend: Bool 112 | 113 | internal init(path: FilePath, appending shouldAppend: Bool) { 114 | self.path = path 115 | self.shouldAppend = shouldAppend 116 | } 117 | 118 | /// Returns either a `FileHandle` or a `Pipe` that will function as the 119 | /// stdout or stderr handle of the child process. 120 | /// 121 | /// - Parameters: 122 | /// - type: The type of the output, represented as an instance of 123 | /// ``OutputType``. 124 | /// - Returns: Either a `FileHandle` or a `Pipe` that will be used as the 125 | /// handle for the child process's corresponding output stream. 126 | /// - Throws: An error if a file at the given ``FileOutputDestination/path`` 127 | /// does not exist. 128 | public func processOutput( 129 | forType type: OutputType 130 | ) throws -> Either { 131 | let fileHandle = try FileHandle(forWritingTo: self.path.url) 132 | if self.shouldAppend { 133 | if #available(macOS 10.15.4, *) { 134 | try fileHandle.seekToEnd() 135 | } else { 136 | fileHandle.seekToEndOfFile() 137 | } 138 | } 139 | 140 | return .first(fileHandle) 141 | } 142 | } 143 | 144 | extension OutputDestination where Self == NullOutputDestination { 145 | /// Creates a ``NullOutputDestination``. 146 | public static var null: Self { .init() } 147 | } 148 | 149 | extension OutputDestination where Self == InheritOutputDestination { 150 | /// Creates an ``InheritOutputDestination``. 151 | public static var inherit: Self { .init() } 152 | } 153 | 154 | extension OutputDestination where Self == PipeOutputDestination { 155 | /// Creates a ``PipeOutputDestination``. 156 | public static var pipe: Self { .init() } 157 | } 158 | 159 | extension OutputDestination where Self == FileOutputDestination { 160 | /// Creates a ``FileOutputDestination``. 161 | /// 162 | /// - Parameters: 163 | /// - path: The `FilePath` where the stdout or stderr of the child process 164 | /// should write to. 165 | /// - shouldAppend: Indicates, if the file should be appended to rather 166 | /// than being overwritten. 167 | /// - Returns: A newly created ``FileOutputDestination``. 168 | public static func write( 169 | toFile path: FilePath, 170 | appending shouldAppend: Bool = false 171 | ) -> Self { 172 | .init(path: path, appending: shouldAppend) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/OutputType.swift: -------------------------------------------------------------------------------- 1 | /// Represents the two types of output handles of a process: stdout and stderr. 2 | public enum OutputType { 3 | /// Represents the stdout handle of a process. 4 | case stdout 5 | /// Represents the stderr handle of a process. 6 | case stderr 7 | } 8 | -------------------------------------------------------------------------------- /Sources/SwiftCommand/ProcessOutput.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | /// The output of a finished child process. 5 | /// 6 | /// This is returned by either the ``Command/waitForOutput()-5zvk9`` method (or 7 | /// the asynchronous ``Command/output-9f0ug`` accessor) of a ``Command``, or the 8 | /// ``ChildProcess/waitWithOutput()`` method (or the asynchronous 9 | /// ``ChildProcess/output`` accessor) of a ``ChildProcess``. 10 | public struct ProcessOutput: Equatable, Sendable { 11 | /// The exit status of the child process. 12 | public let status: ExitStatus 13 | /// The output `Data`, captured from the stdout handle of the child process. 14 | public let stdoutData: Data 15 | /// The output `String`, captured from the stdout handle of the child 16 | /// process. 17 | public let stdout: String 18 | /// The output `Data`, captured from the stderr handle of the child process, 19 | /// if stderr was piped; `nil` otherwise. 20 | public let stderrData: Data? 21 | /// The output `String`, captured from the stderr handle of the child 22 | /// process, if stderr was piped; `nil` otherwise. 23 | public let stderr: String? 24 | 25 | internal init( 26 | status: ExitStatus, 27 | stdoutData: Data, 28 | stdout: String, 29 | stderrData: Data?, 30 | stderr: String? 31 | ) { 32 | self.status = status 33 | self.stdoutData = stdoutData 34 | self.stdout = stdout 35 | self.stderrData = stderrData 36 | self.stderr = stderr 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/SwiftCommandTests/SwiftCommandTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftCommand 3 | 4 | import AsyncAlgorithms 5 | 6 | final class SwiftCommandTests: XCTestCase { 7 | static let lines = ["Foo", "Bar", "Baz", "Test1", "Test2"] 8 | static let joinedLines = lines.joined(separator: "\n") 9 | 10 | func testEcho() async throws { 11 | guard let command = Command.findInPath(withName: "echo") else { 12 | XCTFail() 13 | return 14 | } 15 | 16 | let process = 17 | try command.addArgument(Self.joinedLines) 18 | .setStdout(.pipe) 19 | .spawn() 20 | 21 | var linesIterator = Self.lines.makeIterator() 22 | 23 | for try await line in process.stdout.lines { 24 | XCTAssertEqual(line, linesIterator.next()) 25 | } 26 | 27 | try process.wait() 28 | } 29 | 30 | func testComposition() async throws { 31 | let echoProcess = 32 | try Command.findInPath(withName: "echo")! 33 | .addArgument(Self.joinedLines) 34 | .setStdout(.pipe) 35 | .spawn() 36 | 37 | let grepProcess = 38 | try Command.findInPath(withName: "grep")! 39 | .addArgument("Test") 40 | .setStdin(.pipe(from: echoProcess.stdout)) 41 | .setStdout(.pipe) 42 | .spawn() 43 | 44 | var linesIterator = Self.lines.filter({ 45 | $0.contains("Test") 46 | }).makeIterator() 47 | 48 | for try await line in grepProcess.stdout.lines { 49 | XCTAssertEqual(line, linesIterator.next()) 50 | } 51 | 52 | try echoProcess.wait() 53 | try grepProcess.wait() 54 | } 55 | 56 | func testStdin() async throws { 57 | let process = try Command.findInPath(withName: "cat")! 58 | .setStdin(.pipe) 59 | .setStdout(.pipe) 60 | .spawn() 61 | 62 | var stdin = process.stdin 63 | 64 | print("Foo", to: &stdin) 65 | print("Bar", to: &stdin) 66 | 67 | let output = try await process.output 68 | 69 | XCTAssertEqual(output.stdout, "Foo\nBar\n") 70 | } 71 | 72 | func testStderr() async throws { 73 | let catCommand = Command.findInPath(withName: "cat")! 74 | 75 | let output = try await catCommand.addArgument("non_existing.txt") 76 | .setStderr(.pipe) 77 | .output 78 | 79 | XCTAssertNotEqual(output.status, .success) 80 | 81 | let stderr = output.stderr?.replacingOccurrences( 82 | of: catCommand.executablePath.string, 83 | with: "cat" 84 | ) 85 | 86 | XCTAssertEqual( 87 | stderr, 88 | "cat: non_existing.txt: No such file or directory\n" 89 | ) 90 | } 91 | 92 | func testParallelProcesses() async throws { 93 | let command = Command.findInPath(withName: "cat")! 94 | .setStdin(.pipe(closeImplicitly: false)) 95 | .setStdout(.pipe) 96 | 97 | try await withThrowingTaskGroup( 98 | of: ChildProcess< 99 | PipeInputSource, 100 | PipeOutputDestination, 101 | UnspecifiedOutputDestination 102 | >.self 103 | ) { 104 | group in 105 | for _ in 0..<10 { 106 | group.addTask { 107 | let process = try command.spawn() 108 | 109 | Task.detached { 110 | for line in Self.lines { 111 | process.stdin.write(line) 112 | try await Task.sleep( 113 | nanoseconds: .random(in: 1_000..<500_000_000) 114 | ) 115 | } 116 | 117 | process.stdin.close() 118 | } 119 | 120 | return process 121 | } 122 | } 123 | 124 | for try await process in group { 125 | let output = try await process.output 126 | 127 | XCTAssertEqual(output.stdout, Self.lines.joined()) 128 | } 129 | } 130 | } 131 | 132 | func testTermination() async throws { 133 | let command = Command.findInPath(withName: "cat")! 134 | .setStdin(.pipe(closeImplicitly: false)) 135 | .setStdout(.pipe) 136 | 137 | // For some strange reasons, 'cat' doesn't respond to SIGINT and SIGTERM 138 | // on linux, while testing. This code works in a normal executable 139 | // though, so I'm just ignoring it here for now... 140 | #if canImport(Darwin) 141 | let process1 = try command.spawn() 142 | let process2 = try command.spawn() 143 | #endif 144 | let process3 = try command.spawn() 145 | 146 | #if canImport(Darwin) 147 | process1.interrupt() 148 | let status1 = try await process1.status 149 | XCTAssertTrue(status1.wasTerminatedBySignal) 150 | XCTAssertEqual(status1.terminationSignal, SIGINT) 151 | 152 | process2.terminate() 153 | let status2 = try await process2.status 154 | XCTAssertTrue(status2.wasTerminatedBySignal) 155 | XCTAssertEqual(status2.terminationSignal, SIGTERM) 156 | #endif 157 | 158 | process3.kill() 159 | let status3 = try await process3.status 160 | XCTAssertTrue(status3.wasTerminatedBySignal) 161 | XCTAssertEqual(status3.terminationSignal, SIGKILL) 162 | } 163 | 164 | func testAsyncSequences() async throws { 165 | let command = Command.findInPath(withName: "echo")! 166 | .addArgument(Self.joinedLines) 167 | .setStdout(.pipe) 168 | 169 | let outputString = Self.joinedLines + "\n" 170 | 171 | 172 | let process1 = try command.spawn() 173 | 174 | var charactersIterator = outputString.makeIterator() 175 | 176 | for try await char in process1.stdout.characters { 177 | XCTAssertEqual(char, charactersIterator.next()) 178 | } 179 | 180 | try process1.wait() 181 | 182 | 183 | let process2 = try command.spawn() 184 | 185 | var linesIterator = Self.lines.makeIterator() 186 | 187 | for try await line in process2.stdout.lines { 188 | XCTAssertEqual(line, linesIterator.next()) 189 | } 190 | 191 | try process2.wait() 192 | 193 | let process3 = try command.spawn() 194 | 195 | var bytesIterator = outputString.utf8.makeIterator() 196 | 197 | for try await byte in process3.stdout.bytes { 198 | XCTAssertEqual(byte, bytesIterator.next()) 199 | } 200 | 201 | try process3.wait() 202 | 203 | 204 | #if canImport(Darwin) 205 | if #available(macOS 12, *) { 206 | let process1 = try command.spawn() 207 | 208 | var charactersIterator = outputString.makeIterator() 209 | 210 | for try await char in process1.stdout.nativeCharacters { 211 | XCTAssertEqual(char, charactersIterator.next()) 212 | } 213 | 214 | try process1.wait() 215 | 216 | 217 | let process2 = try command.spawn() 218 | 219 | var linesIterator = Self.lines.makeIterator() 220 | 221 | for try await line in process2.stdout.nativeLines { 222 | XCTAssertEqual(line, linesIterator.next()) 223 | } 224 | 225 | try process2.wait() 226 | 227 | let process3 = try command.spawn() 228 | 229 | var bytesIterator = outputString.utf8.makeIterator() 230 | 231 | for try await byte in process3.stdout.nativeBytes { 232 | XCTAssertEqual(byte, bytesIterator.next()) 233 | } 234 | 235 | try process3.wait() 236 | } 237 | #endif 238 | } 239 | 240 | func testMergedOutput() async throws { 241 | let echoProcess = 242 | try Command.findInPath(withName: "echo")! 243 | .addArgument(Self.joinedLines) 244 | .setStdout(.pipe) 245 | .spawn() 246 | 247 | let bashProcess = 248 | try Command.findInPath(withName: "bash")! 249 | .addArgument("-c") 250 | .addArgument("tee /dev/stderr | sed 's/.*/stdout: &/'") 251 | .setStdin(.pipe(from: echoProcess.stdout)) 252 | .setOutputs(.pipe) 253 | .spawn() 254 | 255 | let lines = try await Array(bashProcess.mergedOutputLines) 256 | 257 | try bashProcess.wait() 258 | 259 | for line in Self.lines { 260 | XCTAssertTrue(lines.contains(line)) 261 | XCTAssertTrue(lines.contains("stdout: \(line)")) 262 | } 263 | } 264 | } 265 | --------------------------------------------------------------------------------