@howaboua/pi-codex-conversion 1.0.7 → 1.0.9-dev.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/README.md +37 -0
- package/package.json +7 -2
- package/src/adapter/runtime-shell.ts +13 -0
- package/src/index.ts +2 -1
- package/src/prompt/build-system-prompt.ts +6 -1
- package/src/shell/bash.ts +261 -0
- package/src/shell/parse-command.ts +616 -0
- package/src/shell/parse.ts +370 -93
- package/src/shell/summary.ts +19 -51
- package/src/shell/tokenize.ts +28 -5
- package/src/tools/exec-session-manager.ts +61 -5
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { extractBashCommand, parseShellLcPlainCommands } from "./bash.ts";
|
|
2
|
+
import {
|
|
3
|
+
isAbsoluteLike,
|
|
4
|
+
joinCommandTokens,
|
|
5
|
+
joinPaths,
|
|
6
|
+
normalizeTokens,
|
|
7
|
+
shellSplit,
|
|
8
|
+
shortDisplayPath,
|
|
9
|
+
splitOnConnectors,
|
|
10
|
+
} from "./tokenize.ts";
|
|
11
|
+
|
|
12
|
+
export type ParsedShellCommand =
|
|
13
|
+
| { kind: "read"; command: string; name: string; path: string }
|
|
14
|
+
| { kind: "list"; command: string; path?: string }
|
|
15
|
+
| { kind: "search"; command: string; query?: string; path?: string }
|
|
16
|
+
| { kind: "unknown"; command: string };
|
|
17
|
+
|
|
18
|
+
export function parseCommandString(command: string): ParsedShellCommand[] {
|
|
19
|
+
return parseCommandTokens(shellSplit(command));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseCommandTokens(command: string[]): ParsedShellCommand[] {
|
|
23
|
+
const parsed = parseCommandImpl(command);
|
|
24
|
+
const deduped: ParsedShellCommand[] = [];
|
|
25
|
+
for (const part of parsed) {
|
|
26
|
+
const previous = deduped[deduped.length - 1];
|
|
27
|
+
if (previous && JSON.stringify(previous) === JSON.stringify(part)) continue;
|
|
28
|
+
deduped.push(part);
|
|
29
|
+
}
|
|
30
|
+
if (deduped.some((part) => part.kind === "unknown")) {
|
|
31
|
+
return [singleUnknownForCommand(command)];
|
|
32
|
+
}
|
|
33
|
+
return deduped;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseCommandImpl(command: string[]): ParsedShellCommand[] {
|
|
37
|
+
const shellCommands = parseShellLcCommands(command);
|
|
38
|
+
if (shellCommands) return shellCommands;
|
|
39
|
+
|
|
40
|
+
const powerShellScript = extractPowerShellCommand(command);
|
|
41
|
+
if (powerShellScript) {
|
|
42
|
+
return [{ kind: "unknown", command: powerShellScript[1] }];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const normalized = normalizeTokens(command);
|
|
46
|
+
const parts = containsConnectors(normalized) ? splitOnConnectors(normalized) : [normalized];
|
|
47
|
+
const effectiveParts = parts.length > 1 ? parts.filter((part) => !isSmallFormattingCommand(part)) : parts;
|
|
48
|
+
if (effectiveParts.length === 0) {
|
|
49
|
+
return [{ kind: "unknown", command: joinCommandTokens(command) }];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const commands: ParsedShellCommand[] = [];
|
|
53
|
+
let cwd: string | undefined;
|
|
54
|
+
for (const tokens of effectiveParts) {
|
|
55
|
+
if (tokens[0] === "cd") {
|
|
56
|
+
const target = cdTarget(tokens.slice(1));
|
|
57
|
+
if (target) cwd = cwd ? joinPaths(cwd, target) : target;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const parsed = summarizeMainTokens(tokens);
|
|
62
|
+
if (parsed.kind === "read" && cwd) {
|
|
63
|
+
commands.push({ ...parsed, path: joinPaths(cwd, parsed.path) });
|
|
64
|
+
} else if (parsed.kind === "list" && cwd && !parsed.path) {
|
|
65
|
+
commands.push({ ...parsed, path: shortDisplayPath(cwd) });
|
|
66
|
+
} else {
|
|
67
|
+
commands.push(parsed);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let simplified = commands;
|
|
72
|
+
while (true) {
|
|
73
|
+
const next = simplifyOnce(simplified);
|
|
74
|
+
if (!next) break;
|
|
75
|
+
simplified = next;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return simplified;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function singleUnknownForCommand(command: string[]): ParsedShellCommand {
|
|
82
|
+
const shell = extractShellCommand(command);
|
|
83
|
+
if (shell) return { kind: "unknown", command: shell[1] };
|
|
84
|
+
return { kind: "unknown", command: joinCommandTokens(command) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractShellCommand(command: string[]): [shell: string, script: string] | undefined {
|
|
88
|
+
return extractBashCommand(command) ?? extractPowerShellCommand(command);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function extractPowerShellCommand(command: string[]): [shell: string, script: string] | undefined {
|
|
92
|
+
if (command.length < 3) return undefined;
|
|
93
|
+
const shell = command[0];
|
|
94
|
+
const shellName = shell.replace(/\\/g, "/").split("/").pop()?.toLowerCase();
|
|
95
|
+
if (shellName !== "powershell" && shellName !== "powershell.exe" && shellName !== "pwsh" && shellName !== "pwsh.exe") {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
for (let index = 1; index + 1 < command.length; index++) {
|
|
99
|
+
const flag = command[index]?.toLowerCase();
|
|
100
|
+
if (flag !== "-nologo" && flag !== "-noprofile" && flag !== "-command" && flag !== "-c") {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
if (flag === "-command" || flag === "-c") {
|
|
104
|
+
return [shell, command[index + 1]!];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseShellLcCommands(original: string[]): ParsedShellCommand[] | undefined {
|
|
111
|
+
const bash = extractBashCommand(original);
|
|
112
|
+
if (!bash) return undefined;
|
|
113
|
+
const [, script] = bash;
|
|
114
|
+
const allCommands = parseShellLcPlainCommands(original);
|
|
115
|
+
if (!allCommands || allCommands.length === 0) {
|
|
116
|
+
return [{ kind: "unknown", command: script }];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const scriptTokens = shellSplit(script);
|
|
120
|
+
const hadMultipleCommands = allCommands.length > 1;
|
|
121
|
+
const filteredCommands = dropSmallFormattingCommands(allCommands);
|
|
122
|
+
if (filteredCommands.length === 0) {
|
|
123
|
+
return [{ kind: "unknown", command: script }];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let commands: ParsedShellCommand[] = [];
|
|
127
|
+
let cwd: string | undefined;
|
|
128
|
+
for (const tokens of filteredCommands) {
|
|
129
|
+
if (tokens[0] === "cd") {
|
|
130
|
+
const target = cdTarget(tokens.slice(1));
|
|
131
|
+
if (target) cwd = cwd ? joinPaths(cwd, target) : target;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const parsed = summarizeMainTokens(tokens);
|
|
136
|
+
if (parsed.kind === "read" && cwd) {
|
|
137
|
+
commands.push({ ...parsed, path: joinPaths(cwd, parsed.path) });
|
|
138
|
+
} else {
|
|
139
|
+
commands.push(parsed);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (commands.length > 1) {
|
|
144
|
+
commands = commands.filter((command) => !(command.kind === "unknown" && command.command === "true"));
|
|
145
|
+
while (true) {
|
|
146
|
+
const next = simplifyOnce(commands);
|
|
147
|
+
if (!next) break;
|
|
148
|
+
commands = next;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (commands.length === 1) {
|
|
153
|
+
const hadConnectors = hadMultipleCommands || scriptTokens.some((token) => token === "|" || token === "&&" || token === "||" || token === ";");
|
|
154
|
+
commands = commands.map((command) => {
|
|
155
|
+
if (command.kind === "read") {
|
|
156
|
+
if (hadConnectors) {
|
|
157
|
+
const hasPipe = scriptTokens.includes("|");
|
|
158
|
+
const hasSedN = scriptTokens.some((token, index) => token === "sed" && scriptTokens[index + 1] === "-n");
|
|
159
|
+
if (hasPipe && hasSedN) {
|
|
160
|
+
return { ...command, command: script };
|
|
161
|
+
}
|
|
162
|
+
return command;
|
|
163
|
+
}
|
|
164
|
+
return { ...command, command: joinCommandTokens(scriptTokens) };
|
|
165
|
+
}
|
|
166
|
+
if (command.kind === "list") {
|
|
167
|
+
return hadConnectors ? command : { ...command, command: joinCommandTokens(scriptTokens) };
|
|
168
|
+
}
|
|
169
|
+
if (command.kind === "search") {
|
|
170
|
+
return hadConnectors ? command : { ...command, command: joinCommandTokens(scriptTokens) };
|
|
171
|
+
}
|
|
172
|
+
return command;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return commands;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function containsConnectors(tokens: string[]): boolean {
|
|
180
|
+
return tokens.some((token) => token === "&&" || token === "||" || token === "|" || token === ";");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function simplifyOnce(commands: ParsedShellCommand[]): ParsedShellCommand[] | undefined {
|
|
184
|
+
if (commands.length <= 1) return undefined;
|
|
185
|
+
|
|
186
|
+
if (commands[0]?.kind === "unknown") {
|
|
187
|
+
const tokens = shellSplit(commands[0].command);
|
|
188
|
+
if (tokens[0] === "echo") return commands.slice(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const cdIndex = commands.findIndex((command) => command.kind === "unknown" && shellSplit(command.command)[0] === "cd");
|
|
192
|
+
if (cdIndex !== -1 && commands.length > cdIndex + 1) {
|
|
193
|
+
return [...commands.slice(0, cdIndex), ...commands.slice(cdIndex + 1)];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const trueIndex = commands.findIndex((command) => command.kind === "unknown" && command.command === "true");
|
|
197
|
+
if (trueIndex !== -1) {
|
|
198
|
+
return [...commands.slice(0, trueIndex), ...commands.slice(trueIndex + 1)];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const nlIndex = commands.findIndex((command) => {
|
|
202
|
+
if (command.kind !== "unknown") return false;
|
|
203
|
+
const tokens = shellSplit(command.command);
|
|
204
|
+
return tokens[0] === "nl" && tokens.slice(1).every((token) => token.startsWith("-"));
|
|
205
|
+
});
|
|
206
|
+
if (nlIndex !== -1) {
|
|
207
|
+
return [...commands.slice(0, nlIndex), ...commands.slice(nlIndex + 1)];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function isSmallFormattingCommand(tokens: string[]): boolean {
|
|
214
|
+
if (tokens.length === 0) return false;
|
|
215
|
+
const command = tokens[0];
|
|
216
|
+
if (command === "wc" || command === "tr" || command === "cut" || command === "sort" || command === "uniq" || command === "tee" || command === "column" || command === "yes" || command === "printf") {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (command === "xargs") return !isMutatingXargsCommand(tokens);
|
|
220
|
+
if (command === "awk") return awkDataFileOperand(tokens.slice(1)) === undefined;
|
|
221
|
+
if (command === "head") {
|
|
222
|
+
if (tokens.length === 1) return true;
|
|
223
|
+
if (tokens.length === 2) return tokens[1]!.startsWith("-");
|
|
224
|
+
if (tokens.length === 3 && (tokens[1] === "-n" || tokens[1] === "-c") && /^[0-9]+$/.test(tokens[2]!)) return true;
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (command === "tail") {
|
|
228
|
+
if (tokens.length === 1) return true;
|
|
229
|
+
if (tokens.length === 2) return tokens[1]!.startsWith("-");
|
|
230
|
+
if (tokens.length === 3 && (tokens[1] === "-n" || tokens[1] === "-c")) {
|
|
231
|
+
const value = tokens[2]!.startsWith("+") ? tokens[2]!.slice(1) : tokens[2]!;
|
|
232
|
+
return value.length > 0 && /^[0-9]+$/.test(value);
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (command === "sed") return sedReadPath(tokens.slice(1)) === undefined;
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function dropSmallFormattingCommands(commands: string[][]): string[][] {
|
|
241
|
+
return commands.filter((command) => !isSmallFormattingCommand(command));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function summarizeMainTokens(mainCommand: string[]): ParsedShellCommand {
|
|
245
|
+
const [head, ...tail] = mainCommand;
|
|
246
|
+
if (!head) return { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
247
|
+
|
|
248
|
+
if (head === "ls" || head === "eza" || head === "exa") {
|
|
249
|
+
const flagsWithValues =
|
|
250
|
+
head === "ls"
|
|
251
|
+
? ["-I", "-w", "--block-size", "--format", "--time-style", "--color", "--quoting-style"]
|
|
252
|
+
: ["-I", "--ignore-glob", "--color", "--sort", "--time-style", "--time"];
|
|
253
|
+
const path = firstNonFlagOperand(tail, flagsWithValues);
|
|
254
|
+
return { kind: "list", command: joinCommandTokens(mainCommand), path: path ? shortDisplayPath(path) : undefined };
|
|
255
|
+
}
|
|
256
|
+
if (head === "tree") {
|
|
257
|
+
const path = firstNonFlagOperand(tail, ["-L", "-P", "-I", "--charset", "--filelimit", "--sort"]);
|
|
258
|
+
return { kind: "list", command: joinCommandTokens(mainCommand), path: path ? shortDisplayPath(path) : undefined };
|
|
259
|
+
}
|
|
260
|
+
if (head === "du") {
|
|
261
|
+
const path = firstNonFlagOperand(tail, ["-d", "--max-depth", "-B", "--block-size", "--exclude", "--time-style"]);
|
|
262
|
+
return { kind: "list", command: joinCommandTokens(mainCommand), path: path ? shortDisplayPath(path) : undefined };
|
|
263
|
+
}
|
|
264
|
+
if (head === "rg" || head === "rga" || head === "ripgrep-all") {
|
|
265
|
+
const args = trimAtConnector(tail);
|
|
266
|
+
const hasFilesFlag = args.includes("--files");
|
|
267
|
+
const candidates = skipFlagValues(args, ["-g", "--glob", "--iglob", "-t", "--type", "--type-add", "--type-not", "-m", "--max-count", "-A", "-B", "-C", "--context", "--max-depth"]);
|
|
268
|
+
const nonFlags = candidates.filter((token) => !token.startsWith("-"));
|
|
269
|
+
if (hasFilesFlag) {
|
|
270
|
+
return { kind: "list", command: joinCommandTokens(mainCommand), path: nonFlags[0] ? shortDisplayPath(nonFlags[0]) : undefined };
|
|
271
|
+
}
|
|
272
|
+
return { kind: "search", command: joinCommandTokens(mainCommand), query: nonFlags[0], path: nonFlags[1] ? shortDisplayPath(nonFlags[1]) : undefined };
|
|
273
|
+
}
|
|
274
|
+
if (head === "git") {
|
|
275
|
+
const [subcommand, ...subtail] = tail;
|
|
276
|
+
if (subcommand === "grep") return parseGrepLike(mainCommand, subtail);
|
|
277
|
+
if (subcommand === "ls-files") {
|
|
278
|
+
const path = firstNonFlagOperand(subtail, ["--exclude", "--exclude-from", "--pathspec-from-file"]);
|
|
279
|
+
return { kind: "list", command: joinCommandTokens(mainCommand), path: path ? shortDisplayPath(path) : undefined };
|
|
280
|
+
}
|
|
281
|
+
return { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
282
|
+
}
|
|
283
|
+
if (head === "fd") {
|
|
284
|
+
const [query, path] = parseFdQueryAndPath(tail);
|
|
285
|
+
return query ? { kind: "search", command: joinCommandTokens(mainCommand), query, path } : { kind: "list", command: joinCommandTokens(mainCommand), path };
|
|
286
|
+
}
|
|
287
|
+
if (head === "find") {
|
|
288
|
+
const [query, path] = parseFindQueryAndPath(tail);
|
|
289
|
+
return query ? { kind: "search", command: joinCommandTokens(mainCommand), query, path } : { kind: "list", command: joinCommandTokens(mainCommand), path };
|
|
290
|
+
}
|
|
291
|
+
if (head === "grep" || head === "egrep" || head === "fgrep") return parseGrepLike(mainCommand, tail);
|
|
292
|
+
if (head === "ag" || head === "ack" || head === "pt") {
|
|
293
|
+
const args = trimAtConnector(tail);
|
|
294
|
+
const candidates = skipFlagValues(args, ["-G", "-g", "--file-search-regex", "--ignore-dir", "--ignore-file", "--path-to-ignore"]);
|
|
295
|
+
const nonFlags = candidates.filter((token) => !token.startsWith("-"));
|
|
296
|
+
return { kind: "search", command: joinCommandTokens(mainCommand), query: nonFlags[0], path: nonFlags[1] ? shortDisplayPath(nonFlags[1]) : undefined };
|
|
297
|
+
}
|
|
298
|
+
if (head === "cat") {
|
|
299
|
+
const path = singleNonFlagOperand(tail, []);
|
|
300
|
+
return path ? readCommand(mainCommand, path) : { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
301
|
+
}
|
|
302
|
+
if (head === "bat" || head === "batcat") {
|
|
303
|
+
const path = singleNonFlagOperand(tail, ["--theme", "--language", "--style", "--terminal-width", "--tabs", "--line-range", "--map-syntax"]);
|
|
304
|
+
return path ? readCommand(mainCommand, path) : { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
305
|
+
}
|
|
306
|
+
if (head === "less") {
|
|
307
|
+
const path = singleNonFlagOperand(tail, ["-p", "-P", "-x", "-y", "-z", "-j", "--pattern", "--prompt", "--tabs", "--shift", "--jump-target"]);
|
|
308
|
+
return path ? readCommand(mainCommand, path) : { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
309
|
+
}
|
|
310
|
+
if (head === "more") {
|
|
311
|
+
const path = singleNonFlagOperand(tail, []);
|
|
312
|
+
return path ? readCommand(mainCommand, path) : { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
313
|
+
}
|
|
314
|
+
if (head === "head") {
|
|
315
|
+
const path = readPathFromHeadTail(tail, "head");
|
|
316
|
+
return path ? readCommand(mainCommand, path) : { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
317
|
+
}
|
|
318
|
+
if (head === "tail") {
|
|
319
|
+
const path = readPathFromHeadTail(tail, "tail");
|
|
320
|
+
return path ? readCommand(mainCommand, path) : { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
321
|
+
}
|
|
322
|
+
if (head === "awk") {
|
|
323
|
+
const path = awkDataFileOperand(tail);
|
|
324
|
+
return path ? readCommand(mainCommand, path) : { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
325
|
+
}
|
|
326
|
+
if (head === "nl") {
|
|
327
|
+
const candidates = skipFlagValues(tail, ["-s", "-w", "-v", "-i", "-b"]);
|
|
328
|
+
const path = candidates.find((token) => !token.startsWith("-"));
|
|
329
|
+
return path ? readCommand(mainCommand, path) : { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
330
|
+
}
|
|
331
|
+
if (head === "sed") {
|
|
332
|
+
const path = sedReadPath(tail);
|
|
333
|
+
return path ? readCommand(mainCommand, path) : { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
334
|
+
}
|
|
335
|
+
if (isPythonCommand(head)) {
|
|
336
|
+
return pythonWalksFiles(tail) ? { kind: "list", command: joinCommandTokens(mainCommand) } : { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
337
|
+
}
|
|
338
|
+
return { kind: "unknown", command: joinCommandTokens(mainCommand) };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function parseGrepLike(mainCommand: string[], args: string[]): ParsedShellCommand {
|
|
342
|
+
const trimmed = trimAtConnector(args);
|
|
343
|
+
const operands: string[] = [];
|
|
344
|
+
let pattern: string | undefined;
|
|
345
|
+
let afterDoubleDash = false;
|
|
346
|
+
for (let index = 0; index < trimmed.length; index++) {
|
|
347
|
+
const arg = trimmed[index]!;
|
|
348
|
+
if (afterDoubleDash) {
|
|
349
|
+
operands.push(arg);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (arg === "--") {
|
|
353
|
+
afterDoubleDash = true;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (arg === "-e" || arg === "--regexp") {
|
|
357
|
+
if (!pattern) pattern = trimmed[index + 1];
|
|
358
|
+
index += 1;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (arg === "-f" || arg === "--file") {
|
|
362
|
+
if (!pattern) pattern = trimmed[index + 1];
|
|
363
|
+
index += 1;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (arg === "-m" || arg === "--max-count" || arg === "-C" || arg === "--context" || arg === "-A" || arg === "--after-context" || arg === "-B" || arg === "--before-context") {
|
|
367
|
+
index += 1;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (arg.startsWith("-")) continue;
|
|
371
|
+
operands.push(arg);
|
|
372
|
+
}
|
|
373
|
+
const hasPattern = pattern !== undefined;
|
|
374
|
+
const query = pattern ?? operands[0];
|
|
375
|
+
const pathIndex = hasPattern ? 0 : 1;
|
|
376
|
+
return { kind: "search", command: joinCommandTokens(mainCommand), query, path: operands[pathIndex] ? shortDisplayPath(operands[pathIndex]!) : undefined };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function readCommand(mainCommand: string[], path: string): ParsedShellCommand {
|
|
380
|
+
return { kind: "read", command: joinCommandTokens(mainCommand), name: shortDisplayPath(path), path };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function trimAtConnector(tokens: string[]): string[] {
|
|
384
|
+
const index = tokens.findIndex((token) => token === "|" || token === "&&" || token === "||" || token === ";");
|
|
385
|
+
return index === -1 ? [...tokens] : tokens.slice(0, index);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function skipFlagValues(args: string[], flagsWithValues: string[]): string[] {
|
|
389
|
+
const out: string[] = [];
|
|
390
|
+
let skipNext = false;
|
|
391
|
+
for (let index = 0; index < args.length; index++) {
|
|
392
|
+
const token = args[index]!;
|
|
393
|
+
if (skipNext) {
|
|
394
|
+
skipNext = false;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (token === "--") {
|
|
398
|
+
out.push(...args.slice(index + 1));
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
if (token.startsWith("--") && token.includes("=")) continue;
|
|
402
|
+
if (flagsWithValues.includes(token)) {
|
|
403
|
+
if (index + 1 < args.length) skipNext = true;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
out.push(token);
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function positionalOperands(args: string[], flagsWithValues: string[]): string[] {
|
|
412
|
+
const out: string[] = [];
|
|
413
|
+
let afterDoubleDash = false;
|
|
414
|
+
let skipNext = false;
|
|
415
|
+
for (let index = 0; index < args.length; index++) {
|
|
416
|
+
const arg = args[index]!;
|
|
417
|
+
if (skipNext) {
|
|
418
|
+
skipNext = false;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (afterDoubleDash) {
|
|
422
|
+
out.push(arg);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (arg === "--") {
|
|
426
|
+
afterDoubleDash = true;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (arg.startsWith("--") && arg.includes("=")) continue;
|
|
430
|
+
if (flagsWithValues.includes(arg)) {
|
|
431
|
+
if (index + 1 < args.length) skipNext = true;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (arg.startsWith("-")) continue;
|
|
435
|
+
out.push(arg);
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function firstNonFlagOperand(args: string[], flagsWithValues: string[]): string | undefined {
|
|
441
|
+
return positionalOperands(args, flagsWithValues)[0];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function singleNonFlagOperand(args: string[], flagsWithValues: string[]): string | undefined {
|
|
445
|
+
const operands = positionalOperands(args, flagsWithValues);
|
|
446
|
+
return operands.length === 1 ? operands[0] : undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function awkDataFileOperand(args: string[]): string | undefined {
|
|
450
|
+
if (args.length === 0) return undefined;
|
|
451
|
+
const trimmed = trimAtConnector(args);
|
|
452
|
+
const hasScriptFile = trimmed.some((arg) => arg === "-f" || arg === "--file");
|
|
453
|
+
const candidates = skipFlagValues(trimmed, ["-F", "-v", "-f", "--field-separator", "--assign", "--file"]);
|
|
454
|
+
const nonFlags = candidates.filter((arg) => !arg.startsWith("-"));
|
|
455
|
+
if (hasScriptFile) return nonFlags[0];
|
|
456
|
+
return nonFlags.length >= 2 ? nonFlags[1] : undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function pythonWalksFiles(args: string[]): boolean {
|
|
460
|
+
const trimmed = trimAtConnector(args);
|
|
461
|
+
for (let index = 0; index < trimmed.length; index++) {
|
|
462
|
+
if (trimmed[index] !== "-c") continue;
|
|
463
|
+
const script = trimmed[index + 1];
|
|
464
|
+
if (!script) continue;
|
|
465
|
+
return script.includes("os.walk") || script.includes("os.listdir") || script.includes("os.scandir") || script.includes("glob.glob") || script.includes("glob.iglob") || script.includes("pathlib.Path") || script.includes(".rglob(");
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function isPythonCommand(command: string): boolean {
|
|
471
|
+
return command === "python" || command === "python2" || command === "python3" || command.startsWith("python2.") || command.startsWith("python3.");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function cdTarget(args: string[]): string | undefined {
|
|
475
|
+
if (args.length === 0) return undefined;
|
|
476
|
+
let target: string | undefined;
|
|
477
|
+
for (let index = 0; index < args.length; index++) {
|
|
478
|
+
const arg = args[index]!;
|
|
479
|
+
if (arg === "--") return args[index + 1];
|
|
480
|
+
if (arg === "-L" || arg === "-P") continue;
|
|
481
|
+
if (arg.startsWith("-")) continue;
|
|
482
|
+
target = arg;
|
|
483
|
+
}
|
|
484
|
+
return target;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function isPathish(value: string): boolean {
|
|
488
|
+
return value === "." || value === ".." || value.startsWith("./") || value.startsWith("../") || value.includes("/") || value.includes("\\");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function parseFdQueryAndPath(args: string[]): [string | undefined, string | undefined] {
|
|
492
|
+
const trimmed = trimAtConnector(args);
|
|
493
|
+
const candidates = skipFlagValues(trimmed, ["-t", "--type", "-e", "--extension", "-E", "--exclude", "--search-path"]);
|
|
494
|
+
const nonFlags = candidates.filter((token) => !token.startsWith("-"));
|
|
495
|
+
if (nonFlags.length === 1) {
|
|
496
|
+
return isPathish(nonFlags[0]!) ? [undefined, shortDisplayPath(nonFlags[0]!)] : [nonFlags[0], undefined];
|
|
497
|
+
}
|
|
498
|
+
if (nonFlags.length >= 2) return [nonFlags[0], shortDisplayPath(nonFlags[1]!)];
|
|
499
|
+
return [undefined, undefined];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function parseFindQueryAndPath(args: string[]): [string | undefined, string | undefined] {
|
|
503
|
+
const trimmed = trimAtConnector(args);
|
|
504
|
+
let path: string | undefined;
|
|
505
|
+
for (const arg of trimmed) {
|
|
506
|
+
if (!arg.startsWith("-") && arg !== "!" && arg !== "(" && arg !== ")") {
|
|
507
|
+
path = shortDisplayPath(arg);
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
let query: string | undefined;
|
|
512
|
+
for (let index = 0; index < trimmed.length; index++) {
|
|
513
|
+
const arg = trimmed[index]!;
|
|
514
|
+
if (arg === "-name" || arg === "-iname" || arg === "-path" || arg === "-regex") {
|
|
515
|
+
query = trimmed[index + 1];
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return [query, path];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function readPathFromHeadTail(args: string[], tool: "head" | "tail"): string | undefined {
|
|
523
|
+
if (args.length === 1 && !args[0]!.startsWith("-")) return args[0];
|
|
524
|
+
if (tool === "head") {
|
|
525
|
+
const hasValidN = args[0] === "-n" ? /^[0-9]+$/.test(args[1] ?? "") : (args[0]?.startsWith("-n") ?? false) && /^[0-9]+$/.test(args[0]!.slice(2));
|
|
526
|
+
if (hasValidN) {
|
|
527
|
+
const candidates: string[] = [];
|
|
528
|
+
for (let index = 0; index < args.length; index++) {
|
|
529
|
+
if (index === 0 && args[index] === "-n" && /^[0-9]+$/.test(args[index + 1] ?? "")) {
|
|
530
|
+
index += 1;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
candidates.push(args[index]!);
|
|
534
|
+
}
|
|
535
|
+
return candidates.find((candidate) => !candidate.startsWith("-"));
|
|
536
|
+
}
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
const hasValidN = args[0] === "-n"
|
|
540
|
+
? /^\+?[0-9]+$/.test(args[1] ?? "")
|
|
541
|
+
: (args[0]?.startsWith("-n") ?? false) && /^\+?[0-9]+$/.test(args[0]!.slice(2));
|
|
542
|
+
if (hasValidN) {
|
|
543
|
+
const candidates: string[] = [];
|
|
544
|
+
for (let index = 0; index < args.length; index++) {
|
|
545
|
+
if (index === 0 && args[index] === "-n" && /^\+?[0-9]+$/.test(args[index + 1] ?? "")) {
|
|
546
|
+
index += 1;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
candidates.push(args[index]!);
|
|
550
|
+
}
|
|
551
|
+
return candidates.find((candidate) => !candidate.startsWith("-"));
|
|
552
|
+
}
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function isValidSedRange(value: string | undefined): boolean {
|
|
557
|
+
if (!value || !value.endsWith("p")) return false;
|
|
558
|
+
const core = value.slice(0, -1);
|
|
559
|
+
const parts = core.split(",");
|
|
560
|
+
return parts.length >= 1 && parts.length <= 2 && parts.every((part) => part.length > 0 && /^[0-9]+$/.test(part));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function sedReadPath(args: string[]): string | undefined {
|
|
564
|
+
const trimmed = trimAtConnector(args);
|
|
565
|
+
if (!trimmed.includes("-n")) return undefined;
|
|
566
|
+
let hasRangeScript = false;
|
|
567
|
+
for (let index = 0; index < trimmed.length; index++) {
|
|
568
|
+
const token = trimmed[index]!;
|
|
569
|
+
if ((token === "-e" || token === "--expression") && isValidSedRange(trimmed[index + 1])) {
|
|
570
|
+
hasRangeScript = true;
|
|
571
|
+
}
|
|
572
|
+
if (!token.startsWith("-") && isValidSedRange(token)) {
|
|
573
|
+
hasRangeScript = true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (!hasRangeScript) return undefined;
|
|
577
|
+
const candidates = skipFlagValues(trimmed, ["-e", "-f", "--expression", "--file"]);
|
|
578
|
+
const nonFlags = candidates.filter((token) => !token.startsWith("-"));
|
|
579
|
+
if (nonFlags.length === 0) return undefined;
|
|
580
|
+
if (isValidSedRange(nonFlags[0])) return nonFlags[1];
|
|
581
|
+
return nonFlags[0];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function isMutatingXargsCommand(tokens: string[]): boolean {
|
|
585
|
+
return xargsSubcommand(tokens)?.length ? xargsIsMutatingSubcommand(xargsSubcommand(tokens)!) : false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function xargsSubcommand(tokens: string[]): string[] | undefined {
|
|
589
|
+
if (tokens[0] !== "xargs") return undefined;
|
|
590
|
+
let index = 1;
|
|
591
|
+
while (index < tokens.length) {
|
|
592
|
+
const token = tokens[index]!;
|
|
593
|
+
if (token === "--") return tokens.slice(index + 1);
|
|
594
|
+
if (!token.startsWith("-")) return tokens.slice(index);
|
|
595
|
+
const takesValue = token === "-E" || token === "-e" || token === "-I" || token === "-L" || token === "-n" || token === "-P" || token === "-s";
|
|
596
|
+
index += takesValue && token.length === 2 ? 2 : 1;
|
|
597
|
+
}
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function xargsIsMutatingSubcommand(tokens: string[]): boolean {
|
|
602
|
+
const [head, ...tail] = tokens;
|
|
603
|
+
if (!head) return false;
|
|
604
|
+
if (head === "perl" || head === "ruby") return xargsHasInPlaceFlag(tail);
|
|
605
|
+
if (head === "sed") return xargsHasInPlaceFlag(tail) || tail.includes("--in-place");
|
|
606
|
+
if (head === "rg") return tail.includes("--replace");
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function xargsHasInPlaceFlag(tokens: string[]): boolean {
|
|
611
|
+
return tokens.some((token) => token === "-i" || token.startsWith("-i") || token === "-pi" || token.startsWith("-pi"));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function isAbsolutePathLike(path: string): boolean {
|
|
615
|
+
return isAbsoluteLike(path);
|
|
616
|
+
}
|