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) + } +}