diff --git a/src/run.ts b/src/run.ts index a66968d..d7263dd 100644 --- a/src/run.ts +++ b/src/run.ts @@ -29,12 +29,23 @@ type RunOptions = { } const SENTINEL_PREFIX = "__SHOUT_SENTINEL_" +const VERBOSE_MARKER = "__SHOUT_CMD_" -function buildScript(commands: Command[], sentinel: string): string { - const lines: string[] = ["exec 2>&1"] +function buildScript(commands: Command[], sentinel: string, verbose: boolean): string { + const lines: string[] = [] + + if (verbose) { + // Save original stderr to fd 3 before merging stderr into stdout + lines.push("exec 3>&2 2>&1") + } else { + lines.push("exec 2>&1") + } for (let i = 0; i < commands.length; i++) { const cmd = commands[i]! + if (verbose) { + lines.push(`printf '${VERBOSE_MARKER}${i}\\n' >&3`) + } lines.push(cmd.command) // Sentinel: printf to avoid echo interpretation issues // Format: __SHOUT_SENTINEL____ @@ -111,6 +122,36 @@ function escapeRegex(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") } +function streamVerboseMarkers( + stderr: ReadableStream, + commands: Command[], + onCommand: (cmd: Command) => void, +): void { + const reader = stderr.getReader() + const decoder = new TextDecoder() + let buffer = "" + + const pump = (): void => { + reader.read().then(({ done, value }) => { + if (done) return + if (value) buffer += decoder.decode(value, { stream: true }) + let nlIdx: number + while ((nlIdx = buffer.indexOf("\n")) !== -1) { + const line = buffer.slice(0, nlIdx) + buffer = buffer.slice(nlIdx + 1) + if (line.startsWith(VERBOSE_MARKER)) { + const i = parseInt(line.slice(VERBOSE_MARKER.length), 10) + if (i >= 0 && i < commands.length) { + onCommand(commands[i]!) + } + } + } + pump() + }).catch(() => {}) + } + pump() +} + export async function runFile( file: ShoutFile, options: RunOptions, @@ -122,7 +163,8 @@ export async function runFile( } const sentinel = SENTINEL_PREFIX - const script = buildScript(file.commands, sentinel) + const verbose = options.verbose && !!options.onCommand + const script = buildScript(file.commands, sentinel, verbose) const env: Record = options.cleanEnv ? {} @@ -155,11 +197,19 @@ export async function runFile( }) try { + if (verbose) { + // Stream stderr for verbose command markers before writing script + streamVerboseMarkers(proc.stderr, file.commands, options.onCommand!) + } + proc.stdin.write(script) proc.stdin.end() - const stdout = await readWithTimeout(proc.stdout, options.timeout * file.commands.length) - const stderr = await readWithTimeout(proc.stderr, 1000).catch(() => "") + const totalTimeout = options.timeout * file.commands.length + const stdout = await readWithTimeout(proc.stdout, totalTimeout) + if (!verbose) { + await readWithTimeout(proc.stderr, 1000).catch(() => "") + } await proc.exited @@ -169,16 +219,11 @@ export async function runFile( file.commands.length, ) - const results: CommandResult[] = file.commands.map((cmd, i) => { - if (options.verbose && options.onCommand) { - options.onCommand(cmd) - } - return { - command: cmd, - actual: outputs[i] ?? [], - exitCode: exitCodes[i] ?? 1, - } - }) + const results: CommandResult[] = file.commands.map((cmd, i) => ({ + command: cmd, + actual: outputs[i] ?? [], + exitCode: exitCodes[i] ?? 1, + })) return { file, results, tmpDir } } catch (err) {