├── .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 | 
4 | [](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 |
--------------------------------------------------------------------------------