Skip to content

Commit ca1cf25

Browse files
fix(rg): search piped stdin when no paths are given (#281)
When rg receives piped stdin with no explicit path arguments, it now searches stdin instead of defaulting to the current directory. This matches real ripgrep behavior and is consistent with how grep already handles stdin in just-bash. The fix checks for non-empty stdin before defaulting to ".". When stdin has content and no paths are given, searchContent is called directly on the stdin text with all applicable options (invert, count, context, etc). Also fixes the existing rg.utf8-stdin test which was previously passing by accident — it tested piped stdin but rg was actually searching the filesystem. Fixes #280
1 parent 7a5a0b9 commit ca1cf25

3 files changed

Lines changed: 144 additions & 7 deletions

File tree

packages/just-bash/src/commands/rg/rg-search.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,6 @@ export async function executeSearch(
116116
};
117117
}
118118

119-
// Default to current directory
120-
const paths = inputPaths.length === 0 ? ["."] : inputPaths;
121-
122119
// Determine case sensitivity
123120
const effectiveIgnoreCase = determineIgnoreCase(options, patterns);
124121

@@ -141,6 +138,70 @@ export async function executeSearch(
141138
};
142139
}
143140

141+
// If no paths given and stdin has content, search stdin (like real rg).
142+
// In a pipeline, the previous command's stdout becomes this command's
143+
// stdin — search that text instead of defaulting to the current directory.
144+
// Skip when `-f -` already consumed stdin for patterns to avoid
145+
// double-consuming it as both pattern source and search input.
146+
const stdinConsumedByPatternFile = options.patternFiles.includes("-");
147+
const stdinText = decodeBytesToUtf8(ctx.stdin);
148+
if (
149+
inputPaths.length === 0 &&
150+
stdinText.length > 0 &&
151+
!stdinConsumedByPatternFile
152+
) {
153+
const content = stdinText;
154+
const result = searchContent(content, regex, {
155+
invertMatch: options.invertMatch,
156+
showLineNumbers: options.lineNumber,
157+
countOnly: options.count,
158+
countMatches: options.countMatches,
159+
filename: "",
160+
onlyMatching: options.onlyMatching,
161+
beforeContext: options.beforeContext,
162+
afterContext: options.afterContext,
163+
maxCount: options.maxCount,
164+
contextSeparator: options.contextSeparator,
165+
showColumn: options.column,
166+
vimgrep: options.vimgrep,
167+
showByteOffset: options.byteOffset,
168+
replace:
169+
options.replace !== null ? convertReplacement(options.replace) : null,
170+
passthru: options.passthru,
171+
multiline: options.multiline,
172+
kResetGroup,
173+
});
174+
175+
if (options.quiet) {
176+
return { stdout: "", stderr: "", exitCode: result.matched ? 0 : 1 };
177+
}
178+
179+
if (options.filesWithMatches) {
180+
return {
181+
stdout: result.matched ? "(standard input)\n" : "",
182+
stderr: "",
183+
exitCode: result.matched ? 0 : 1,
184+
};
185+
}
186+
187+
if (options.filesWithoutMatch) {
188+
return {
189+
stdout: result.matched ? "" : "(standard input)\n",
190+
stderr: "",
191+
exitCode: result.matched ? 1 : 0,
192+
};
193+
}
194+
195+
return {
196+
stdout: result.output,
197+
stderr: "",
198+
exitCode: result.matched ? 0 : 1,
199+
};
200+
}
201+
202+
// Default to current directory
203+
const paths = inputPaths.length === 0 ? ["."] : inputPaths;
204+
144205
// Load gitignore files
145206
let gitignore: GitignoreManager | null = null;
146207
if (!options.noIgnore) {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, it } from "vitest";
2+
import { Bash } from "../../Bash.js";
3+
4+
describe("rg searches stdin when no paths are given", () => {
5+
it("searches piped stdin instead of cwd", async () => {
6+
const env = new Bash({
7+
files: {
8+
// File exists but should NOT be searched — only stdin should be
9+
"/decoy.txt": "decoy line\n",
10+
},
11+
});
12+
const result = await env.exec(
13+
'printf "hello world\\ngoodbye\\n" | rg "hello"',
14+
);
15+
expect(result.exitCode).toBe(0);
16+
expect(result.stdout).toContain("hello world");
17+
expect(result.stdout).not.toContain("decoy");
18+
});
19+
20+
it("returns exit code 1 when stdin has no match", async () => {
21+
const env = new Bash({ files: {} });
22+
const result = await env.exec('echo "hello" | rg "xyz"');
23+
expect(result.exitCode).toBe(1);
24+
expect(result.stdout).toBe("");
25+
});
26+
27+
it("supports case-insensitive search on stdin", async () => {
28+
const env = new Bash({ files: {} });
29+
const result = await env.exec('printf "Hello World\\n" | rg -i "hello"');
30+
expect(result.exitCode).toBe(0);
31+
expect(result.stdout).toContain("Hello World");
32+
});
33+
34+
it("supports inverted match on stdin", async () => {
35+
const env = new Bash({ files: {} });
36+
const result = await env.exec('printf "aaa\\nbbb\\nccc\\n" | rg -v "bbb"');
37+
expect(result.exitCode).toBe(0);
38+
expect(result.stdout).toContain("aaa");
39+
expect(result.stdout).toContain("ccc");
40+
expect(result.stdout).not.toContain("bbb");
41+
});
42+
43+
it("supports count mode on stdin", async () => {
44+
const env = new Bash({ files: {} });
45+
const result = await env.exec('printf "foo\\nbar\\nfoo\\n" | rg -c "foo"');
46+
expect(result.exitCode).toBe(0);
47+
expect(result.stdout.trim()).toBe("2");
48+
});
49+
50+
it("searches files when explicit path is given (not stdin)", async () => {
51+
const env = new Bash({
52+
files: { "/data.txt": "target line\n" },
53+
});
54+
const result = await env.exec(
55+
'echo "stdin content" | rg "target" /data.txt',
56+
);
57+
expect(result.exitCode).toBe(0);
58+
expect(result.stdout).toContain("target line");
59+
});
60+
61+
it("does not double-consume stdin when -f - is used", async () => {
62+
const env = new Bash({
63+
files: { "/data.txt": "hello world\ngoodbye\n" },
64+
});
65+
// -f - reads patterns from stdin; rg should then search the explicit
66+
// path, not try to also search stdin as content
67+
const result = await env.exec('echo "hello" | rg -f - /data.txt');
68+
expect(result.exitCode).toBe(0);
69+
expect(result.stdout).toContain("hello world");
70+
});
71+
72+
it("supports multibyte UTF-8 patterns from piped stdin", async () => {
73+
const env = new Bash({ files: {} });
74+
const result = await env.exec("printf '한글 found\\nmiss\\n' | rg '한글'");
75+
expect(result.exitCode).toBe(0);
76+
expect(result.stdout).toContain("한글 found");
77+
expect(result.stdout).not.toContain("miss");
78+
});
79+
});

packages/just-bash/src/commands/rg/rg.utf8-stdin.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ describe("rg reads UTF-8 from stdin", () => {
88
});
99
const result = await env.exec("cat /in.txt | rg '한글'");
1010
expect(result.exitCode).toBe(0);
11-
// rg prefixes matches from stdin with a `<filename>:<line>:` source
12-
// identifier; the bytes after the last `:` are the matched line.
13-
const matched = result.stdout.split(":").slice(2).join(":");
14-
expect(matched.trim()).toBe("한글 found");
11+
expect(result.stdout).toContain("한글 found");
1512
});
1613
});

0 commit comments

Comments
 (0)