From 7f8a31be42ddc6c899214100d4bc73ce6b202861 Mon Sep 17 00:00:00 2001 From: Oliver Drobnik Date: Thu, 11 Jun 2026 23:19:19 +0200 Subject: [PATCH] InputSource: expose isCanonicalEmpty for stdin-or-cwd routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commands that emulate tools with 'read stdin OR walk the cwd' behaviour (ripgrep's no-path default) must know whether the embedder attached real input. The host process's fd 0 can't answer that for an embedded shell — an iBash 'echo x | rg pat' must read the pipe even though the app's fd 0 is a terminal or /dev/null. The binding can answer: shells bind the canonical .empty when no pipe or redirect feeds a command, anything else was attached on purpose. Identity-based on the shared cursor, so copies stay canonical while a manually built zero-byte stream counts as attached-but-empty — matching bash, where 'true | cmd' hands cmd a real (empty) pipe. Wanted by Cocoanetics/SwiftPorts#71 (rg no-path routing, issue SwiftPorts#65) to honour the in-process InputSource contract flagged in its review. Co-Authored-By: Claude Fable 5 --- Sources/ShellKit/IO/InputSource.swift | 19 ++++++++++++ .../InputSourceCanonicalEmptyTests.swift | 29 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 Tests/ShellKitTests/InputSourceCanonicalEmptyTests.swift diff --git a/Sources/ShellKit/IO/InputSource.swift b/Sources/ShellKit/IO/InputSource.swift index 8c2ffa6..e5bf9b9 100644 --- a/Sources/ShellKit/IO/InputSource.swift +++ b/Sources/ShellKit/IO/InputSource.swift @@ -43,6 +43,25 @@ public struct InputSource: Sendable { await cursor.readLine() } + /// True when this source is the canonical ``empty`` instance — + /// the marker a shell binds when no pipe or redirect feeds a + /// command. + /// + /// Commands that emulate tools with "read stdin OR walk the cwd" + /// behaviour (ripgrep's no-path default) need to know whether + /// the embedder attached real input. The host process's fd 0 + /// can't answer that for an embedded shell; the binding can: + /// `.empty` means "nothing attached", anything else was bound + /// on purpose. + /// + /// Identity-based: only the shared ``empty`` instance (and its + /// copies) answer true. A manually built zero-byte stream is + /// still "attached input that happens to be empty" — matching + /// bash, where `true | cmd` hands `cmd` a real (empty) pipe. + public var isCanonicalEmpty: Bool { + cursor === Self.empty.cursor + } + // MARK: Factories /// An already-finished stream with no data. diff --git a/Tests/ShellKitTests/InputSourceCanonicalEmptyTests.swift b/Tests/ShellKitTests/InputSourceCanonicalEmptyTests.swift new file mode 100644 index 0000000..e601457 --- /dev/null +++ b/Tests/ShellKitTests/InputSourceCanonicalEmptyTests.swift @@ -0,0 +1,29 @@ +import Foundation +import Testing +@testable import ShellKit + +/// ``InputSource/isCanonicalEmpty`` — the "did the shell attach real +/// input?" probe used by commands with read-stdin-or-walk-cwd +/// behaviour (rg's no-path default). +@Suite struct InputSourceCanonicalEmptyTests { + + @Test func canonicalEmptyAnswersTrue() { + #expect(InputSource.empty.isCanonicalEmpty) + // Copies share the cursor, so the marker survives passing + // the source around by value. + let copy = InputSource.empty + #expect(copy.isCanonicalEmpty) + } + + @Test func attachedSourcesAnswerFalse() { + #expect(!InputSource.string("x").isCanonicalEmpty) + // Deliberately attached zero-byte input is NOT canonical — + // bash's `true | cmd` hands cmd a real (empty) pipe, and a + // command must read it (and see EOF), not walk the cwd. + #expect(!InputSource.string("").isCanonicalEmpty) + #expect(!InputSource.data(Data()).isCanonicalEmpty) + let (stream, continuation) = AsyncStream.makeStream() + continuation.finish() + #expect(!InputSource(bytes: stream).isCanonicalEmpty) + } +}