From a91cf2d32c1b562676a8442c499322a6e160084e Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 12:49:55 -0700 Subject: [PATCH 01/12] Script tests: outputOf on echo & echo pipe to map --- Package.swift | 4 ++ Tests/ScriptTests/Script Tests.swift | 64 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 Tests/ScriptTests/Script Tests.swift diff --git a/Package.swift b/Package.swift index 31ab889..b48ee45 100644 --- a/Package.swift +++ b/Package.swift @@ -41,6 +41,10 @@ let package = Package( "Script" ] ), + .testTarget( + name: "ScriptTests", + dependencies: ["Script"] + ), .testTarget( name: "ShwiftTests", dependencies: ["Shwift"], diff --git a/Tests/ScriptTests/Script Tests.swift b/Tests/ScriptTests/Script Tests.swift new file mode 100644 index 0000000..6c9bd0c --- /dev/null +++ b/Tests/ScriptTests/Script Tests.swift @@ -0,0 +1,64 @@ +import XCTest +import Script + +final class ScriptCoreTests: XCTestCase { + + func testEcho() async throws { + try runInScript { + var result: String + result = try await outputOf { + try await echo("1", "2") + } + // fyi, trailing newline stripped by outputOf + XCTAssertEqual("1 2", result, "echo 1 2") + + result = try await outputOf { + try await echo("1", "2", separator: ",", terminator: ";") + } + XCTAssertEqual("1,2;", result, "echo 1,2;") + } + } + + func testEchoMap() async throws { + try runInScript { + var result: String + result = try await outputOf { + // inject newline so map op runs on head then tail + try await echo("1", "2", "\n", "3", "4") | map(){"<\($0)>"} + } + XCTAssertEqual("<1 2 >\n< 3 4>", result, "echo 1 2 \n 3 4 | map{<$0>}") + } + } + + /// Run test in Script.run() context + private func runInScript( + _ op: @escaping RunInScriptProxy.Op, + caller: StaticString = #function, + callerLine: UInt = #line + ) throws { + let e = expectation(description: "\(caller):\(callerLine)") + try RunInScriptProxy(op, e).run() // sync call preferred + wait(for: [e], timeout: 2) + } + + private struct RunInScriptProxy: Script { + typealias Op = () async throws -> Void + var op: Op? + var e: XCTestExpectation? + init(_ op: @escaping Op, _ e: XCTestExpectation) { + self.op = op + self.e = e + } + func run() async throws { + defer { e?.fulfill() } + try await op?() + } + // Codable + init() {} + var i = 0 + private enum CodingKeys: String, CodingKey { + case i + } + } +} + From 67472c7e365c200d8ca033abf11023290a9001f8 Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 12:51:45 -0700 Subject: [PATCH 02/12] Lines.segmented(by delimiter:..) --- Sources/Shwift/Builtins.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/Shwift/Builtins.swift b/Sources/Shwift/Builtins.swift index 748f759..a3d9762 100644 --- a/Sources/Shwift/Builtins.swift +++ b/Sources/Shwift/Builtins.swift @@ -105,7 +105,7 @@ extension Builtin { at: buffer.readerIndex, length: buffer.readableBytes)! var substring = readString[readString.startIndex...] - while let lineBreak = substring.firstIndex(of: "\n") { + while let lineBreak = substring.firstIndex(of: delimiter) { let line = substring[substring.startIndex.. Lines { + Lines(byteBuffers: byteBuffers, delimiter: delimiter) } typealias ByteBuffers = AsyncCompactMapSequence< From 77cf6fdfe6fba0c91d704cbffbb7559fc742c64b Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 12:54:34 -0700 Subject: [PATCH 03/12] compactMap(segmentingInputAt:, withOutputTerminator:) --- Sources/Script/List Comprehensions.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/Script/List Comprehensions.swift b/Sources/Script/List Comprehensions.swift index 88a295f..d3e1fe0 100644 --- a/Sources/Script/List Comprehensions.swift +++ b/Sources/Script/List Comprehensions.swift @@ -10,15 +10,17 @@ public func map(transform: @Sendable @escaping (String) async throws -> String) compactMap(transform: transform) } -public func compactMap(transform: @Sendable @escaping (String) async throws -> String?) - -> Shell.PipableCommand -{ +public func compactMap( + segmentingInputAt delimiter: Character = "\n", + withOutputTerminator terminator: String = "\n", + transform: @Sendable @escaping (String) async throws -> String? +) -> Shell.PipableCommand { Shell.PipableCommand { try await Shell.invoke { shell, invocation in try await invocation.builtin { channel in - for try await line in channel.input.lines.compactMap(transform) { + for try await line in channel.input.segmented(by: delimiter).compactMap(transform) { try await channel.output.withTextOutputStream { stream in - print(line, to: &stream) + print(line, terminator: terminator, to: &stream) } } } From 5333f620016086b0465c35aaa5763194861fb3e8 Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 13:02:01 -0700 Subject: [PATCH 04/12] Test #22: remainder dropped when no delimiter --- Tests/ScriptTests/Script Tests.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/ScriptTests/Script Tests.swift b/Tests/ScriptTests/Script Tests.swift index 6c9bd0c..c7eb660 100644 --- a/Tests/ScriptTests/Script Tests.swift +++ b/Tests/ScriptTests/Script Tests.swift @@ -30,6 +30,18 @@ final class ScriptCoreTests: XCTestCase { } } + /// Demo [#22 dropped remainder](https://github.com/GeorgeLyon/Shwift/issues/22) + func testEchoMapWithDelimiters() async throws { + try runInScript { + let result = try await outputOf { + try await echo("1", "2", separator: ",", terminator: "") + | compactMap(segmentingInputAt: ",", withOutputTerminator: "|") { "<\($0)>"} + } + let exp = "<1>|<2>|" + XCTAssertEqual(exp, result, "echo 1,2 -map-> <1>|<2>|") + } + } + /// Run test in Script.run() context private func runInScript( _ op: @escaping RunInScriptProxy.Op, From 42156b243f30ebe08e31ffb096c082122e1ec3f9 Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 13:03:56 -0700 Subject: [PATCH 05/12] Fix #22: accumulate remainder in Lines iterator --- Sources/Shwift/Builtins.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Shwift/Builtins.swift b/Sources/Shwift/Builtins.swift index a3d9762..21d9bde 100644 --- a/Sources/Shwift/Builtins.swift +++ b/Sources/Shwift/Builtins.swift @@ -111,7 +111,7 @@ extension Builtin { continuation.yield(remainder + String(line)) remainder = "" } - remainder = String(substring) + remainder += String(substring) } if !remainder.isEmpty { continuation.yield(String(remainder)) From 62606eab280d9cb28ef553968616eda791947eb8 Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 13:35:56 -0700 Subject: [PATCH 06/12] delimiter+terminator in map (to compactMap) --- Sources/Script/List Comprehensions.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/Script/List Comprehensions.swift b/Sources/Script/List Comprehensions.swift index d3e1fe0..ab39dcf 100644 --- a/Sources/Script/List Comprehensions.swift +++ b/Sources/Script/List Comprehensions.swift @@ -4,10 +4,12 @@ import Shwift By default, shell output is processed as a list of lines */ -public func map(transform: @Sendable @escaping (String) async throws -> String) - -> Shell.PipableCommand -{ - compactMap(transform: transform) +public func map( + segmentingInputAt delimiter: Character = "\n", + withOutputTerminator terminator: String = "\n", + transform: @Sendable @escaping (String) async throws -> String +) -> Shell.PipableCommand { + compactMap(segmentingInputAt: delimiter, withOutputTerminator: terminator, transform: transform) } public func compactMap( From 39543128dfab15546b51bfd6e03bf56fad1994e7 Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 14:20:58 -0700 Subject: [PATCH 07/12] XCTAssertOutput delimiters & bin,txt tests --- Package.swift | 4 +++- Tests/ShwiftTests/Chunks.bin | 2 ++ Tests/ShwiftTests/Chunks.txt | 2 ++ Tests/ShwiftTests/Shwift Tests.swift | 29 ++++++++++++++++++++++++++-- 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 Tests/ShwiftTests/Chunks.bin create mode 100644 Tests/ShwiftTests/Chunks.txt diff --git a/Package.swift b/Package.swift index b48ee45..e803ad7 100644 --- a/Package.swift +++ b/Package.swift @@ -49,7 +49,9 @@ let package = Package( name: "ShwiftTests", dependencies: ["Shwift"], resources: [ - .copy("Cat.txt") + .copy("Cat.txt"), + .copy("Chunks.txt"), + .copy("Chunks.bin"), ]), ] ) diff --git a/Tests/ShwiftTests/Chunks.bin b/Tests/ShwiftTests/Chunks.bin new file mode 100644 index 0000000..340b90a --- /dev/null +++ b/Tests/ShwiftTests/Chunks.bin @@ -0,0 +1,2 @@ +1;2 +3;4 \ No newline at end of file diff --git a/Tests/ShwiftTests/Chunks.txt b/Tests/ShwiftTests/Chunks.txt new file mode 100644 index 0000000..e0aca92 --- /dev/null +++ b/Tests/ShwiftTests/Chunks.txt @@ -0,0 +1,2 @@ +1;2 +3;4 diff --git a/Tests/ShwiftTests/Shwift Tests.swift b/Tests/ShwiftTests/Shwift Tests.swift index abcf4a2..3ed1627 100644 --- a/Tests/ShwiftTests/Shwift Tests.swift +++ b/Tests/ShwiftTests/Shwift Tests.swift @@ -86,6 +86,27 @@ final class ShwiftCoreTests: XCTestCase { """) } + func testInputChunksTxt() throws { + try XCTAssertOutput( + of: { context, output in + try await Builtin.read(from: FilePath(Self.chunksTxtFilePath), to: output, in: context) + }, + is: "1 < 2\n3 < 4\n", + afterSplittingWith: ";", + andJoiningWith: " < " + ) + } + + func testInputChunksBin() throws { + try XCTAssertOutput( + of: { context, output in + try await Builtin.read(from: FilePath(Self.chunksBinFilePath), to: output, in: context) + }, + is: "1<2\n3<4", + afterSplittingWith: ";", + andJoiningWith: "<" + ) + } private enum Outcome: ExpressibleByStringInterpolation { init(stringLiteral value: String) { self = .success(value) @@ -97,6 +118,8 @@ final class ShwiftCoreTests: XCTestCase { private func XCTAssertOutput( of operation: @escaping (Context, FileDescriptor) async throws -> Void, is expectedOutcome: Outcome, + afterSplittingWith inDelimiter: Character = "\n", + andJoiningWith outDelimiter: String = "\n", file: StaticString = #file, line: UInt = #line, function: StaticString = #function ) throws { @@ -116,9 +139,9 @@ final class ShwiftCoreTests: XCTestCase { return try await Output.nullDevice.withFileDescriptor(in: context) { output in try await Builtin.withChannel(input: input, output: output, in: context) { channel in - return try await channel.input.lines + return try await channel.input.segmented(by: inDelimiter) .reduce(into: [], { $0.append($1) }) - .joined(separator: "\n") + .joined(separator: outDelimiter) } } } catch { @@ -152,6 +175,8 @@ final class ShwiftCoreTests: XCTestCase { } private static let supportFilePath = Bundle.module.path(forResource: "Cat", ofType: "txt")! + private static let chunksBinFilePath = Bundle.module.path(forResource: "Chunks", ofType: "bin")! + private static let chunksTxtFilePath = Bundle.module.path(forResource: "Chunks", ofType: "txt")! } private extension Shwift.Process { From fa8706058975a9092bedf10b2df551d826e8ee67 Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 14:34:51 -0700 Subject: [PATCH 08/12] delimiter & separator in reduce and outputOf --- Sources/Script/List Comprehensions.swift | 7 +++++-- Sources/Script/Output Capture.swift | 11 ++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/Script/List Comprehensions.swift b/Sources/Script/List Comprehensions.swift index ab39dcf..45f510d 100644 --- a/Sources/Script/List Comprehensions.swift +++ b/Sources/Script/List Comprehensions.swift @@ -32,12 +32,14 @@ public func compactMap( public func reduce( into initialResult: T, + segmentingInputAt delimiter: Character = "\n", _ updateAccumulatingResult: @escaping (inout T, String) async throws -> Void ) -> Shell.PipableCommand { Shell.PipableCommand { try await Shell.invoke { _, invocation in try await invocation.builtin { channel in - try await channel.input.lines.reduce(into: initialResult, updateAccumulatingResult) + try await channel.input.segmented(by: delimiter) + .reduce(into: initialResult, updateAccumulatingResult) } } } @@ -45,12 +47,13 @@ public func reduce( public func reduce( _ initialResult: T, + segmentingInputAt delimiter: Character = "\n", _ nextPartialResult: @escaping (T, String) async throws -> T ) -> Shell.PipableCommand { Shell.PipableCommand { try await Shell.invoke { _, invocation in try await invocation.builtin { channel in - try await channel.input.lines.reduce(initialResult, nextPartialResult) + try await channel.input.segmented(by: delimiter).reduce(initialResult, nextPartialResult) } } } diff --git a/Sources/Script/Output Capture.swift b/Sources/Script/Output Capture.swift index 4bc5477..bf636be 100644 --- a/Sources/Script/Output Capture.swift +++ b/Sources/Script/Output Capture.swift @@ -1,4 +1,9 @@ -public func outputOf(_ operation: @escaping () async throws -> Void) async throws -> String { - let lines = try await Shell.PipableCommand(operation) | reduce(into: []) { $0.append($1) } - return lines.joined(separator: "\n") +public func outputOf( + segmentingInputAt delimiter: Character = "\n", + withOutputSeparator separator: String = "\n", + _ operation: @escaping () async throws -> Void +) async throws -> String { + let lines = try await Shell.PipableCommand(operation) + | reduce(into: [], segmentingInputAt: delimiter) { $0.append($1) } + return lines.joined(separator: separator) } From 5747b7c9ad0617548a08059bedbc88ea667beb9b Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 15:05:10 -0700 Subject: [PATCH 09/12] docc comments for output and list comprehensions --- Sources/Script/List Comprehensions.swift | 36 ++++++++++++++++++++++-- Sources/Script/Output Capture.swift | 16 ++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/Sources/Script/List Comprehensions.swift b/Sources/Script/List Comprehensions.swift index 45f510d..14f0d46 100644 --- a/Sources/Script/List Comprehensions.swift +++ b/Sources/Script/List Comprehensions.swift @@ -1,9 +1,15 @@ import Shwift /** - By default, shell output is processed as a list of lines - */ + Converts shell input stream to output stream via transform. + By default, input is segmented by newline, and a newline is printed at the end of each output segment. + - Parameters: + - delimiter: Character to split input into segments (defaults to newline) + - terminator: String printed at the end of each output (defaults to newline) + - transform: converts String to String + - Returns: ``Shell/PipableCommand`` + */ public func map( segmentingInputAt delimiter: Character = "\n", withOutputTerminator terminator: String = "\n", @@ -12,6 +18,16 @@ public func map( compactMap(segmentingInputAt: delimiter, withOutputTerminator: terminator, transform: transform) } +/** + Converts shell input stream to output stream via transform, ignoring nil input + + By default, input is segmented by newline, and a newline is printed at the end of each output segment. + - Parameters: + - delimiter: Character to split input into segments (defaults to newline) + - terminator: String printed at the end of each output (defaults to newline) + - transform: converts String to String + - Returns: ``Shell/PipableCommand`` + */ public func compactMap( segmentingInputAt delimiter: Character = "\n", withOutputTerminator terminator: String = "\n", @@ -30,6 +46,14 @@ public func compactMap( } } +/** + Returns the result of processing input elements using the given closure and mutable initial value to accumulate the result. + - Parameters: + - initialResult: mutable initial value to accumulate the result + - delimiter: Character to split input into segments passed to closure (defaults to newline) + - updateAccumulatingResult: A closure over the mutable accumulator and the next String. + - Returns: ``Shell/PipableCommand`` +*/ public func reduce( into initialResult: T, segmentingInputAt delimiter: Character = "\n", @@ -45,6 +69,14 @@ public func reduce( } } +/** + Returns the result of processing input elements using the given closure and mutable initial value to accumulate the result. + - Parameters: + - initialResult: initial value passed to initial closure + - delimiter: Character to split input into segments passed to closure (defaults to newline) + - nextPartialResult: A closure over the initial or previous value and the next String segment that returns the next value + - Returns: ``Shell/PipableCommand`` + */ public func reduce( _ initialResult: T, segmentingInputAt delimiter: Character = "\n", diff --git a/Sources/Script/Output Capture.swift b/Sources/Script/Output Capture.swift index bf636be..1f43e51 100644 --- a/Sources/Script/Output Capture.swift +++ b/Sources/Script/Output Capture.swift @@ -1,9 +1,23 @@ +/** + Captures operation output as text. + + By default, input is processed per line (delimited by newline), + and the result String contains each output line separated by newlines (but without an terminating newline). + + - Parameters: + - delimiter: Character to split input into segments (defaults to newline) + - separator: String used to join output items into a single String (defaults to newlin) + - operation: closure writing to output stream to capture + - Throws: rethrows errors thrown by underlying operations + - Returns: String of output items delimited by separator + */ public func outputOf( segmentingInputAt delimiter: Character = "\n", withOutputSeparator separator: String = "\n", _ operation: @escaping () async throws -> Void ) async throws -> String { - let lines = try await Shell.PipableCommand(operation) + let lines = + try await Shell.PipableCommand(operation) | reduce(into: [], segmentingInputAt: delimiter) { $0.append($1) } return lines.joined(separator: separator) } From acf0a0230de57faede1b6bceb91f8023f6a9c2ee Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 15:16:27 -0700 Subject: [PATCH 10/12] Test map and reduce with delimiter to non-String output type --- Tests/ScriptTests/Script Tests.swift | 99 +++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/Tests/ScriptTests/Script Tests.swift b/Tests/ScriptTests/Script Tests.swift index c7eb660..021b9ba 100644 --- a/Tests/ScriptTests/Script Tests.swift +++ b/Tests/ScriptTests/Script Tests.swift @@ -42,7 +42,61 @@ final class ScriptCoreTests: XCTestCase { } } - /// Run test in Script.run() context + /// Verify `reduce` chunks input by splitAt delimiter (and handles non-string output) + func testReduceChunked() async throws { + typealias SF = ChunkFixtures + let result = SF.Result() // reference type + let exp: [SF.Ints] = [.i(1), .pair(2, 3), .i(4), .err("not")] + try runInScript { + let sep: Character = ";" + + // inout, here using class + try await echo("1", "2\n3", "4", "not", + separator: "\(sep)", terminator: "") + | reduce(into: result, segmentingInputAt: sep) { $0.add(.make($1)) } + XCTAssertEqual(exp, result.result, "Reduce (inside)") + + // returning value each time + let array: [SF.Ints] + = try await echo("1", "2\n3", "4", "not", + separator: "\(sep)", terminator: "") + | reduce([], segmentingInputAt: sep) { r, s in r + [SF.Ints.make(s)] } + XCTAssertEqual(exp, array, "Reduce (arrays)") + } + XCTAssertEqual(exp, result.result, "Reduce (outside)") + } + + /// Verify `map` chunks input by splitAt delimiter + func testMapChunked() async throws { + typealias SF = ChunkFixtures + let mapped: [SF.Ints] = [.i(1), .pair(2, 3), .i(4), .err("not")] + let expect = mapped.map{ $0.double() }.joined(separator: "\n") + try runInScript { + let sep: Character = ";" + let result = try await outputOf { + try await echo("1", "2\n3", "4", "not", + separator: "\(sep)", terminator: "") + | map(segmentingInputAt: sep) { SF.Ints.make($0).double() } + } + XCTAssertEqual(expect, result, "Map") + } + } + + /// Verify `compactMap` chunks input by splitAt delimiter + func testCompactMapChunked() async throws { + typealias SF = ChunkFixtures + let all: [SF.Ints] = [.i(1), .pair(2, 3), .i(4), .err("not")] + let evens = all.compactMap{ $0.anyEven() }.joined(separator: "\n") + try runInScript { + let sep: Character = ";" + let result = try await outputOf { + try await echo("1", "2\n3", "4", "not", + separator: "\(sep)", terminator: "") + | compactMap(segmentingInputAt: sep) { SF.Ints.make($0).anyEven() } + } + XCTAssertEqual(evens, result, "Map") + } + } /// Run test in Script.run() context private func runInScript( _ op: @escaping RunInScriptProxy.Op, caller: StaticString = #function, @@ -72,5 +126,48 @@ final class ScriptCoreTests: XCTestCase { case i } } + private enum ChunkFixtures { + class Result { + var result = [T]() + func add(_ next: T) { + result.append(next) + } + } + enum Ints: Codable, Equatable { + case i(Int), pair(Int, Int), err(String) + + static func make(_ s: String) -> Ints { + let nums = s.split(separator: "\n") + switch nums.count { + case 1: + if let i = Int(nums[0]) { + return .i(i) + } + return err(s) + case 2: return .pair(Int(nums[0]) ?? -1, Int(nums[1]) ?? -2) + default: return .err(s) + } + } + + func double() -> String { + switch self { + case .i(let n): return "\(2*n)" + case .pair(let i, let j): return "\(2*i)\n\(2*j)" + case .err(let s): return "\(s)\(s)" + } + } + + func anyEven() -> String? { + func anyEvens(_ i: Int...) -> Bool { + nil != i.first { 0 == $0 % 2} + } + switch self { + case .i(let n): return anyEvens(n) ? "\(n)" : nil + case .pair(let i, let j): return anyEvens(i, j) ? "\(i)\n\(j)" : nil + case .err: return nil + } + } + } + } } From de9488d7d7b6797a651548b4be8dcfa7ee62beb0 Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 15:28:15 -0700 Subject: [PATCH 11/12] Trace default newline using Lines.eol/Str --- Sources/Script/List Comprehensions.swift | 12 ++++++------ Sources/Script/Output Capture.swift | 6 ++++-- Sources/Shwift/Builtins.swift | 4 +++- Tests/ShwiftTests/Shwift Tests.swift | 4 ++-- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Sources/Script/List Comprehensions.swift b/Sources/Script/List Comprehensions.swift index 14f0d46..aee436b 100644 --- a/Sources/Script/List Comprehensions.swift +++ b/Sources/Script/List Comprehensions.swift @@ -11,8 +11,8 @@ import Shwift - Returns: ``Shell/PipableCommand`` */ public func map( - segmentingInputAt delimiter: Character = "\n", - withOutputTerminator terminator: String = "\n", + segmentingInputAt delimiter: Character = Builtin.Input.Lines.eol, + withOutputTerminator terminator: String = Builtin.Input.Lines.eolStr, transform: @Sendable @escaping (String) async throws -> String ) -> Shell.PipableCommand { compactMap(segmentingInputAt: delimiter, withOutputTerminator: terminator, transform: transform) @@ -29,8 +29,8 @@ public func map( - Returns: ``Shell/PipableCommand`` */ public func compactMap( - segmentingInputAt delimiter: Character = "\n", - withOutputTerminator terminator: String = "\n", + segmentingInputAt delimiter: Character = Builtin.Input.Lines.eol, + withOutputTerminator terminator: String = Builtin.Input.Lines.eolStr, transform: @Sendable @escaping (String) async throws -> String? ) -> Shell.PipableCommand { Shell.PipableCommand { @@ -56,7 +56,7 @@ public func compactMap( */ public func reduce( into initialResult: T, - segmentingInputAt delimiter: Character = "\n", + segmentingInputAt delimiter: Character = Builtin.Input.Lines.eol, _ updateAccumulatingResult: @escaping (inout T, String) async throws -> Void ) -> Shell.PipableCommand { Shell.PipableCommand { @@ -79,7 +79,7 @@ public func reduce( */ public func reduce( _ initialResult: T, - segmentingInputAt delimiter: Character = "\n", + segmentingInputAt delimiter: Character = Builtin.Input.Lines.eol, _ nextPartialResult: @escaping (T, String) async throws -> T ) -> Shell.PipableCommand { Shell.PipableCommand { diff --git a/Sources/Script/Output Capture.swift b/Sources/Script/Output Capture.swift index 1f43e51..3d3978c 100644 --- a/Sources/Script/Output Capture.swift +++ b/Sources/Script/Output Capture.swift @@ -1,3 +1,5 @@ +import Shwift + /** Captures operation output as text. @@ -12,8 +14,8 @@ - Returns: String of output items delimited by separator */ public func outputOf( - segmentingInputAt delimiter: Character = "\n", - withOutputSeparator separator: String = "\n", + segmentingInputAt delimiter: Character = Builtin.Input.Lines.eol, + withOutputSeparator separator: String = Builtin.Input.Lines.eolStr, _ operation: @escaping () async throws -> Void ) async throws -> String { let lines = diff --git a/Sources/Shwift/Builtins.swift b/Sources/Shwift/Builtins.swift index 21d9bde..260de53 100644 --- a/Sources/Shwift/Builtins.swift +++ b/Sources/Shwift/Builtins.swift @@ -87,6 +87,8 @@ extension Builtin { public struct Input { public struct Lines: AsyncSequence { + public static let eol: Character = "\n" + public static let eolStr = "\n" public typealias Element = String public struct AsyncIterator: AsyncIteratorProtocol { @@ -138,7 +140,7 @@ extension Builtin { /// /// - Parameter delimiter: Character separating input text to yield (and not itself yielded) Defaults to newline. /// - Returns: Lines segmented by delimiter - public func segmented(by delimiter: Character = "\n") -> Lines { + public func segmented(by delimiter: Character = Lines.eol) -> Lines { Lines(byteBuffers: byteBuffers, delimiter: delimiter) } diff --git a/Tests/ShwiftTests/Shwift Tests.swift b/Tests/ShwiftTests/Shwift Tests.swift index 3ed1627..bbb47d7 100644 --- a/Tests/ShwiftTests/Shwift Tests.swift +++ b/Tests/ShwiftTests/Shwift Tests.swift @@ -118,8 +118,8 @@ final class ShwiftCoreTests: XCTestCase { private func XCTAssertOutput( of operation: @escaping (Context, FileDescriptor) async throws -> Void, is expectedOutcome: Outcome, - afterSplittingWith inDelimiter: Character = "\n", - andJoiningWith outDelimiter: String = "\n", + afterSplittingWith inDelimiter: Character = Builtin.Input.Lines.eol, + andJoiningWith outDelimiter: String = Builtin.Input.Lines.eolStr, file: StaticString = #file, line: UInt = #line, function: StaticString = #function ) throws { From b9b77b47ce19b8f9d8a45f2ab3c1f66a8c5bbc84 Mon Sep 17 00:00:00 2001 From: wti Date: Mon, 18 Mar 2024 15:59:00 -0700 Subject: [PATCH 12/12] Demo space-delimited reduce to type in Main --- Sources/ScriptExample/Main.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ScriptExample/Main.swift b/Sources/ScriptExample/Main.swift index 1dd3b8b..3980698 100644 --- a/Sources/ScriptExample/Main.swift +++ b/Sources/ScriptExample/Main.swift @@ -38,6 +38,10 @@ import Script let numberOfLines = try await echo("Foo", "Bar") | reduce(into: 0, { count, _ in count += 1 }) print(numberOfLines) + let totals = + try await echo("-n", "1", "2", "3") + | reduce(4, segmentingInputAt: " ") {$0 + (Int($1) ?? -10)} + print("10 == \(totals) from 4 + 1 + 2 + 3") case .writeToFile: try await echo("Foo") > "Test.txt" case .infiniteInput: