@coo-quack/sensitive-canary 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +830 -0
- package/README.md +269 -43
- package/dist/lib/bash-commands.js +405 -0
- package/dist/lib/command-tables.js +462 -0
- package/dist/lib/default-config.json +570 -0
- package/dist/lib/encoding.js +123 -0
- package/dist/lib/fail-closed.js +31 -0
- package/dist/lib/inspector.js +0 -0
- package/dist/lib/rules.js +399 -0
- package/dist/lib/shapes.js +161 -0
- package/dist/lib/shell.js +436 -0
- package/dist/lib/tool-inputs.js +217 -0
- package/dist/lib/transcript.js +115 -0
- package/dist/lib/validators.js +435 -0
- package/dist/pre-tool-use-hook.js +773 -0
- package/dist/user-prompt-submit-hook.js +105 -0
- package/hooks/hooks.json +1 -1
- package/package.json +25 -11
- package/src/lib/bash-commands.ts +455 -0
- package/src/lib/command-tables.ts +518 -0
- package/src/lib/default-config.json +570 -0
- package/src/lib/encoding.ts +135 -0
- package/src/lib/fail-closed.ts +36 -0
- package/src/lib/inspector.ts +0 -0
- package/src/lib/rules.ts +482 -267
- package/src/lib/shapes.ts +175 -0
- package/src/lib/shell.ts +512 -0
- package/src/lib/tool-inputs.ts +235 -0
- package/src/lib/transcript.ts +142 -0
- package/src/lib/validators.ts +435 -0
- package/src/pre-tool-use-hook.ts +774 -198
- package/src/user-prompt-submit-hook.ts +60 -18
- package/src/__tests__/pre-tool-use-hook.test.ts +0 -779
- package/src/__tests__/user-prompt-submit-hook.test.ts +0 -297
- package/src/lib/__tests__/inspector.test.ts +0 -281
- package/src/lib/__tests__/rules.test.ts +0 -448
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// What this hook knows about how each command treats its operands.
|
|
2
|
+
//
|
|
3
|
+
// The tables are the knowledge: which commands print a file they are handed,
|
|
4
|
+
// which take a pattern first, which run another command. extractCommandRefs
|
|
5
|
+
// turns a command line into the files it may print and the environment it may
|
|
6
|
+
// expose, leaving the parsing to shell.ts.
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { ARGUMENT_ONLY_COMMANDS, asksForRecursion, classifyCommand, editsInPlace, GIT_GLOBAL_FLAGS_WITH_OPERAND, GREP_FAMILY, gitLineRangeFile, gitSubcommandPrintsFiles, isWrapperTarget, MAX_NESTING_DEPTH, mergeRefs, POSIX_SHELLS, patternSupplyingFlag, RECURSIVE_BY_DEFAULT, WRAPPER_COMMANDS, WRITE_TARGET_FLAGS, } from "./command-tables.js";
|
|
9
|
+
import { extractQuotedLiterals, extractSubstitutions, isNonCommandToken, stripHeredocBodies, tokenizeCommand, } from "./shell.js";
|
|
10
|
+
// Index of the token naming the command whose operands matter.
|
|
11
|
+
//
|
|
12
|
+
// The lead command is the first token that is not a flag, redirection or
|
|
13
|
+
// `VAR=value` assignment. Only a known wrapper (`sudo`, `env`, `timeout`, …)
|
|
14
|
+
// is peeled, by searching the rest of the segment for the first token this
|
|
15
|
+
// hook can classify (`env` and `printenv` included, so their detection behind
|
|
16
|
+
// wrapper operands works); anything else is treated as the command itself. Searching
|
|
17
|
+
// unconditionally mistook operands for commands: `echo cat secrets` resolved
|
|
18
|
+
// to `cat`, and a file that was never read was scanned and blocked. Wrappers
|
|
19
|
+
// are still not peeled by counting their flags: `sudo -u root cat f` would
|
|
20
|
+
// mistake `root` for the command, and `timeout -s KILL 5 cat f` would
|
|
21
|
+
// mistake `5`. Falling back to the lead leaves an unknown command classified
|
|
22
|
+
// as itself.
|
|
23
|
+
//
|
|
24
|
+
// The search past a wrapper carries the same hazard one step further in, which
|
|
25
|
+
// is why it stops at an ARGUMENT_ONLY_COMMANDS name: `sudo echo cat secrets`
|
|
26
|
+
// otherwise resolved to the `cat` sitting in echo's arguments. Past any other
|
|
27
|
+
// unclassifiable name the search continues, since that name may be a wrapper
|
|
28
|
+
// flag's value rather than the command.
|
|
29
|
+
function findCommandIndex(tokens) {
|
|
30
|
+
let lead = -1;
|
|
31
|
+
// A redirection may come before the command — `< secrets cat` is `cat` reading
|
|
32
|
+
// `secrets`. The operator is skipped as a non-command token, but its target is
|
|
33
|
+
// an ordinary word, so `secrets` was taken for the command name and the real
|
|
34
|
+
// one went unclassified: nothing of its operands was collected, and a spelling
|
|
35
|
+
// away from `cat < secrets`, which blocks.
|
|
36
|
+
let skipRedirectTarget = false;
|
|
37
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
38
|
+
const token = tokens[i];
|
|
39
|
+
if (token === undefined)
|
|
40
|
+
continue;
|
|
41
|
+
if (skipRedirectTarget) {
|
|
42
|
+
skipRedirectTarget = false;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (token.redirect) {
|
|
46
|
+
skipRedirectTarget = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (isNonCommandToken(token))
|
|
50
|
+
continue;
|
|
51
|
+
lead = i;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (lead === -1)
|
|
55
|
+
return 0;
|
|
56
|
+
const leadName = path.basename(tokens[lead]?.value ?? "");
|
|
57
|
+
if (!WRAPPER_COMMANDS.has(leadName))
|
|
58
|
+
return lead;
|
|
59
|
+
for (let i = lead + 1; i < tokens.length; i++) {
|
|
60
|
+
const tok = tokens[i];
|
|
61
|
+
if (tok === undefined || isNonCommandToken(tok))
|
|
62
|
+
continue;
|
|
63
|
+
const name = path.basename(tok.value);
|
|
64
|
+
if (isWrapperTarget(name) || ARGUMENT_ONLY_COMMANDS.has(name))
|
|
65
|
+
return i;
|
|
66
|
+
}
|
|
67
|
+
return lead;
|
|
68
|
+
}
|
|
69
|
+
// `env` and `printenv` print the environment unless they are being used to run
|
|
70
|
+
// another command. `sudo printenv` counts; `env FOO=1 cat f` does not. The
|
|
71
|
+
// command is located with findCommandIndex, so wrapper flags and operands
|
|
72
|
+
// (`sudo -u root printenv`, `timeout 5 env`) cannot hide it.
|
|
73
|
+
function inspectEnvironmentCommand(tokens) {
|
|
74
|
+
const nothing = { dumps: false, named: [] };
|
|
75
|
+
const start = findCommandIndex(tokens);
|
|
76
|
+
const cmdToken = tokens[start];
|
|
77
|
+
if (cmdToken === undefined)
|
|
78
|
+
return nothing;
|
|
79
|
+
const name = path.basename(cmdToken.value);
|
|
80
|
+
if (name !== "env" && name !== "printenv")
|
|
81
|
+
return nothing;
|
|
82
|
+
// `printenv` prints the whole environment unless it is given variables to
|
|
83
|
+
// print. A redirection target is not one of them: `printenv > out.txt` prints
|
|
84
|
+
// everything, and counting `out.txt` as a named variable left the environment
|
|
85
|
+
// unscanned.
|
|
86
|
+
if (name === "printenv") {
|
|
87
|
+
const rest = tokens.slice(start + 1);
|
|
88
|
+
const named = [];
|
|
89
|
+
for (let j = 0; j < rest.length; j++) {
|
|
90
|
+
const t = rest[j];
|
|
91
|
+
if (t === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
if (t.redirect) {
|
|
94
|
+
j++; // its target, or a heredoc delimiter
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (isNonCommandToken(t))
|
|
98
|
+
continue; // flags
|
|
99
|
+
named.push(t.value);
|
|
100
|
+
}
|
|
101
|
+
return named.length === 0
|
|
102
|
+
? { dumps: true, named: [] }
|
|
103
|
+
: { dumps: false, named };
|
|
104
|
+
}
|
|
105
|
+
// `env` prints the environment unless a subcommand follows its own
|
|
106
|
+
// arguments: assignments (`FOO=1`), flags, and the values of flags that
|
|
107
|
+
// take one (`-u FOO`, `-C dir`) are all env's own. The `-S` split string
|
|
108
|
+
// is the subcommand itself (`env -S "cat f"` runs cat), so it rules a dump
|
|
109
|
+
// out. With no subcommand the whole environment is printed — `env FOO=1`
|
|
110
|
+
// and `env -u FOO` included. Exception: `-i` starts from an empty
|
|
111
|
+
// environment, so only the given assignments (already scanned as command
|
|
112
|
+
// text) print.
|
|
113
|
+
const rest = tokens.slice(start + 1);
|
|
114
|
+
let ignoreEnvironment = false;
|
|
115
|
+
let hasCommand = false;
|
|
116
|
+
for (let j = 0; j < rest.length; j++) {
|
|
117
|
+
const t = rest[j];
|
|
118
|
+
if (t === undefined)
|
|
119
|
+
continue;
|
|
120
|
+
const value = t.value;
|
|
121
|
+
if (value === "-i" || value === "--ignore-environment") {
|
|
122
|
+
ignoreEnvironment = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (t.redirect) {
|
|
126
|
+
j++; // redirection operator: its target is env's own argument here
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (value === "-u" ||
|
|
130
|
+
value === "--unset" ||
|
|
131
|
+
value === "-C" ||
|
|
132
|
+
value === "--chdir") {
|
|
133
|
+
j++; // flag value
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (value === "-S" || value === "--split-string") {
|
|
137
|
+
hasCommand = true; // the split string is the subcommand
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
if (isNonCommandToken(t))
|
|
141
|
+
continue; // flags and FOO=1 assignments
|
|
142
|
+
hasCommand = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
return { dumps: !hasCommand && !ignoreEnvironment, named: [] };
|
|
146
|
+
}
|
|
147
|
+
// True when `token` introduces inline program text for `cmd`. POSIX shells only
|
|
148
|
+
// take code after -c; other interpreters also use -e and combined forms (-pe).
|
|
149
|
+
function isInlineCodeFlag(cmd, token) {
|
|
150
|
+
if (token === "-c" || token === "--command" || token === "--eval")
|
|
151
|
+
return true;
|
|
152
|
+
if (POSIX_SHELLS.has(cmd)) {
|
|
153
|
+
// A shell bundles its switches too: `bash -lc 'cat secrets'` runs the same
|
|
154
|
+
// string `bash -c` would, and only the exact spelling was recognised. The
|
|
155
|
+
// letters before the `c` have to be switches that take no value of their
|
|
156
|
+
// own, or the `c` is part of something else's value.
|
|
157
|
+
return /^-[abefhilmnpuvxCPT]*c$/.test(token);
|
|
158
|
+
}
|
|
159
|
+
if (token === "-r")
|
|
160
|
+
return true; // php -r
|
|
161
|
+
return /^-[A-Za-z]*[eE]$/.test(token);
|
|
162
|
+
}
|
|
163
|
+
// Everything a Bash command reveals that the hook can inspect before it runs:
|
|
164
|
+
// the files whose contents it may print, and the environment it may expose.
|
|
165
|
+
export function extractCommandRefs(command, depth = 0) {
|
|
166
|
+
const refs = {
|
|
167
|
+
paths: [],
|
|
168
|
+
envVars: [],
|
|
169
|
+
dumpsEnvironment: false,
|
|
170
|
+
searchesWorkingDirectory: false,
|
|
171
|
+
};
|
|
172
|
+
if (depth > MAX_NESTING_DEPTH)
|
|
173
|
+
return refs;
|
|
174
|
+
// Heredoc bodies are text, not commands; strip them before any other pass.
|
|
175
|
+
const text = stripHeredocBodies(command);
|
|
176
|
+
// `echo $(cat secrets)` reads secrets just as `cat secrets` does.
|
|
177
|
+
for (const inner of extractSubstitutions(text)) {
|
|
178
|
+
mergeRefs(refs, extractCommandRefs(inner, depth + 1));
|
|
179
|
+
}
|
|
180
|
+
for (const tokens of tokenizeCommand(text)) {
|
|
181
|
+
const environment = inspectEnvironmentCommand(tokens);
|
|
182
|
+
if (environment.dumps)
|
|
183
|
+
refs.dumpsEnvironment = true;
|
|
184
|
+
refs.envVars.push(...environment.named);
|
|
185
|
+
mergeRefs(refs, collectSegmentRefs(tokens, depth));
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
paths: [...new Set(refs.paths)],
|
|
189
|
+
envVars: [...new Set(refs.envVars)],
|
|
190
|
+
dumpsEnvironment: refs.dumpsEnvironment,
|
|
191
|
+
searchesWorkingDirectory: refs.searchesWorkingDirectory,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// File paths one segment of a command line may print, plus anything found inside
|
|
195
|
+
// inline program text it carries.
|
|
196
|
+
function collectSegmentRefs(tokens, depth) {
|
|
197
|
+
const refs = {
|
|
198
|
+
paths: [],
|
|
199
|
+
envVars: [],
|
|
200
|
+
dumpsEnvironment: false,
|
|
201
|
+
searchesWorkingDirectory: false,
|
|
202
|
+
};
|
|
203
|
+
const start = findCommandIndex(tokens);
|
|
204
|
+
const cmdToken = tokens[start];
|
|
205
|
+
if (cmdToken === undefined)
|
|
206
|
+
return refs;
|
|
207
|
+
const cmd = path.basename(cmdToken.value);
|
|
208
|
+
const operands = tokens.slice(start + 1);
|
|
209
|
+
// `< secrets cat` puts the redirection before the command, so its target is
|
|
210
|
+
// not among the operands and the loop below never sees it. The command still
|
|
211
|
+
// reads it.
|
|
212
|
+
//
|
|
213
|
+
// `$(<secrets)` has no command at all: bash reads the file and substitutes its
|
|
214
|
+
// contents. There the whole token list is in front of the "command", which is
|
|
215
|
+
// the redirection operator itself.
|
|
216
|
+
const beforeCommand = cmdToken.redirect ? tokens.length : start;
|
|
217
|
+
for (let i = 0; i < beforeCommand; i++) {
|
|
218
|
+
if (tokens[i]?.redirect !== true)
|
|
219
|
+
continue;
|
|
220
|
+
if (tokens[i]?.value !== "<")
|
|
221
|
+
continue;
|
|
222
|
+
const target = tokens[i + 1];
|
|
223
|
+
if (target === undefined || target.redirect)
|
|
224
|
+
continue;
|
|
225
|
+
if (!classifyCommand(cmd).printsNoFileContents) {
|
|
226
|
+
refs.paths.push(target.value);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// In-place editing sends the result back to the file, so nothing reaches
|
|
230
|
+
// stdout and nothing is read into the conversation.
|
|
231
|
+
if (editsInPlace(cmd, operands)) {
|
|
232
|
+
return refs;
|
|
233
|
+
}
|
|
234
|
+
// `eval 'cat secrets'` is a command line in a single word, the same shape
|
|
235
|
+
// `env -S` carries. Stepping past `eval` finds that word as the command name,
|
|
236
|
+
// which classifies as nothing at all.
|
|
237
|
+
if (cmd === "eval") {
|
|
238
|
+
for (const operand of operands) {
|
|
239
|
+
if (operand.redirect)
|
|
240
|
+
continue;
|
|
241
|
+
mergeRefs(refs, extractCommandRefs(operand.value, depth + 1));
|
|
242
|
+
refs.paths.push(...extractQuotedLiterals(operand.value));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// `env -S "cmd args"` splits the string into the command it runs, so scan
|
|
246
|
+
// inside it the way inline code is scanned.
|
|
247
|
+
if (cmd === "env") {
|
|
248
|
+
for (let k = 0; k < operands.length; k++) {
|
|
249
|
+
const t = operands[k]?.value;
|
|
250
|
+
if (t !== "-S" && t !== "--split-string")
|
|
251
|
+
continue;
|
|
252
|
+
const script = operands[k + 1]?.value;
|
|
253
|
+
if (script === undefined)
|
|
254
|
+
continue;
|
|
255
|
+
mergeRefs(refs, extractCommandRefs(script, depth + 1));
|
|
256
|
+
k++;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const behaviour = classifyCommand(cmd);
|
|
260
|
+
let skipNext = false;
|
|
261
|
+
let collectNext = false;
|
|
262
|
+
let codeNext = false;
|
|
263
|
+
let inlineCodeSeen = false;
|
|
264
|
+
let patternSkipped = false;
|
|
265
|
+
let gitSubcommandSeen = false;
|
|
266
|
+
let gitReadsFiles = false;
|
|
267
|
+
let optionsEnded = false;
|
|
268
|
+
let lineRangeNext = false;
|
|
269
|
+
for (const tok of operands) {
|
|
270
|
+
if (skipNext) {
|
|
271
|
+
skipNext = false;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (collectNext) {
|
|
275
|
+
collectNext = false;
|
|
276
|
+
refs.paths.push(tok.value);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (codeNext) {
|
|
280
|
+
codeNext = false;
|
|
281
|
+
// The expression came from -e/-c, so a later operand is a file, not the
|
|
282
|
+
// script `perl file` would have run. That is `inlineCodeSeen` below: no
|
|
283
|
+
// command takes inline code *and* a leading pattern, an assumption the
|
|
284
|
+
// tests pin, so there is no pattern here to mark as supplied.
|
|
285
|
+
if (behaviour.inlineCodeReadsOperands)
|
|
286
|
+
inlineCodeSeen = true;
|
|
287
|
+
mergeRefs(refs, extractCommandRefs(tok.value, depth + 1));
|
|
288
|
+
refs.paths.push(...extractQuotedLiterals(tok.value));
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (tok.redirect) {
|
|
292
|
+
if (tok.value === "<") {
|
|
293
|
+
// stdin is fed from the next token, unless nothing of it is printed
|
|
294
|
+
collectNext = !behaviour.printsNoFileContents;
|
|
295
|
+
skipNext = behaviour.printsNoFileContents;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
// A heredoc or herestring delimiter, or an output target: never read.
|
|
299
|
+
skipNext = true;
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// `--` ends option parsing: every token after it is an operand, whatever it
|
|
304
|
+
// is spelled like. `grep -- -aws secrets` searches for `-aws` in `secrets`,
|
|
305
|
+
// so the file is a file — read as a flag, `-aws` left the pattern
|
|
306
|
+
// unaccounted for and `secrets` was consumed in its place.
|
|
307
|
+
if (!optionsEnded && tok.value === "--") {
|
|
308
|
+
optionsEnded = true;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (!optionsEnded &&
|
|
312
|
+
behaviour.takesInlineCode &&
|
|
313
|
+
isInlineCodeFlag(cmd, tok.value)) {
|
|
314
|
+
codeNext = true;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
// `git log -L1,10:f` prints the lines of `f` themselves, and the file is
|
|
318
|
+
// written inside the range spec after the last `:`. The flag branch below
|
|
319
|
+
// consumes any `-`-shaped token, so this has to come before it, and the
|
|
320
|
+
// operand branch would never see the file anyway.
|
|
321
|
+
if (behaviour.isGit &&
|
|
322
|
+
(tok.value.startsWith("-L") || tok.value.startsWith("--line-range"))) {
|
|
323
|
+
const inFlag = gitLineRangeFile(tok.value);
|
|
324
|
+
if (inFlag !== null)
|
|
325
|
+
refs.paths.push(inFlag);
|
|
326
|
+
else
|
|
327
|
+
lineRangeNext = true;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (lineRangeNext) {
|
|
331
|
+
lineRangeNext = false;
|
|
332
|
+
const separate = gitLineRangeFile(tok.value);
|
|
333
|
+
if (separate !== null)
|
|
334
|
+
refs.paths.push(separate);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (!optionsEnded && tok.value.startsWith("-")) {
|
|
338
|
+
// A global git flag with a separate value consumes the next token too:
|
|
339
|
+
// in `git -C repo show f`, `repo` is not the subcommand.
|
|
340
|
+
if (behaviour.isGit &&
|
|
341
|
+
!gitSubcommandSeen &&
|
|
342
|
+
GIT_GLOBAL_FLAGS_WITH_OPERAND.has(tok.value)) {
|
|
343
|
+
skipNext = true;
|
|
344
|
+
}
|
|
345
|
+
// The value of an output flag is written, not read.
|
|
346
|
+
if (WRITE_TARGET_FLAGS[cmd]?.has(tok.value))
|
|
347
|
+
skipNext = true;
|
|
348
|
+
// The pattern arrived as a flag, so no operand stands in for it.
|
|
349
|
+
if (behaviour.firstOperandIsPatternOrScript) {
|
|
350
|
+
const supply = patternSupplyingFlag(tok.value);
|
|
351
|
+
if (supply !== null) {
|
|
352
|
+
patternSkipped = true;
|
|
353
|
+
// Only a flag still waiting for its value consumes the next token. A
|
|
354
|
+
// value already attached — `--regexp=aws`, or `-eaws` — is part of
|
|
355
|
+
// this one.
|
|
356
|
+
if (supply === "separate")
|
|
357
|
+
skipNext = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (behaviour.isGit) {
|
|
363
|
+
if (!gitSubcommandSeen) {
|
|
364
|
+
gitSubcommandSeen = true;
|
|
365
|
+
gitReadsFiles = gitSubcommandPrintsFiles(tok.value, operands);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (gitReadsFiles)
|
|
369
|
+
refs.paths.push(tok.value);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
// `if=<file>` names an input only for `dd`; other commands taking an
|
|
373
|
+
// `if=` argument are not reading the file it names.
|
|
374
|
+
if (behaviour.isDd) {
|
|
375
|
+
const ddInput = /^if=(.+)$/.exec(tok.value);
|
|
376
|
+
if (ddInput?.[1]) {
|
|
377
|
+
refs.paths.push(ddInput[1]);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (behaviour.firstOperandIsPatternOrScript && !patternSkipped) {
|
|
382
|
+
patternSkipped = true; // the pattern, expression or script name
|
|
383
|
+
// An awk or sed program can name a file inside itself, the way inline code
|
|
384
|
+
// does: `awk 'BEGIN{ while ((getline l < "secrets") > 0) print l }'` reads
|
|
385
|
+
// one without ever naming it as an operand.
|
|
386
|
+
refs.paths.push(...extractQuotedLiterals(tok.value));
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (behaviour.printsOperands ||
|
|
390
|
+
behaviour.firstOperandIsPatternOrScript ||
|
|
391
|
+
inlineCodeSeen) {
|
|
392
|
+
refs.paths.push(tok.value);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// A searcher handed no file searches where it is run. `rg PATTERN` and
|
|
396
|
+
// `grep -r PATTERN` are the ordinary forms and name nothing, so the loop above
|
|
397
|
+
// collects the pattern and stops with no path — and the tree they print from
|
|
398
|
+
// is the working directory. The caller is what knows which directory that is.
|
|
399
|
+
if (refs.paths.length === 0 &&
|
|
400
|
+
(RECURSIVE_BY_DEFAULT.has(cmd) ||
|
|
401
|
+
(GREP_FAMILY.has(cmd) && asksForRecursion(operands)))) {
|
|
402
|
+
refs.searchesWorkingDirectory = true;
|
|
403
|
+
}
|
|
404
|
+
return refs;
|
|
405
|
+
}
|