Skip to content

Commit 4626ccf

Browse files
scratchyoneclaude
andcommitted
feat(web): Windows commands highlight in their own languages
Windows agents also run PowerShell directly, without a pwsh wrapper, and those commands were colored with the shell grammar. A command now uses the PowerShell grammar when a statement starts with a capitalized Verb-Noun cmdlet (`Get-Content`), `$env:`, a `$name = value` assignment, or the `&` call operator, none of which parse as POSIX shell. Nested highlighting also finds Python run through the Windows `py` launcher (`py -3 -c '…'`) and interpreters named by a Windows path (`C:\Python312\python.exe -c '…'`). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1 parent e51a919 commit 4626ccf

4 files changed

Lines changed: 50 additions & 4 deletions

File tree

‎apps/web/src/lib/embeddedScripts.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ describe("embeddedScripts", () => {
5454
it.each([
5555
[`python3 -c 'print(1)'`, "python", "print(1)"],
5656
[`python3 -X dev -c 'print(1)'`, "python", "print(1)"],
57+
[`py -3 -c 'print(1)'`, "python", "print(1)"],
58+
[`C:\\Python312\\python.exe -c 'print(1)'`, "python", "print(1)"],
59+
[`"C:\\Program Files\\Python312\\python.exe" -c 'print(1)'`, "python", "print(1)"],
5760
[`node --eval "process.exit(0)"`, "javascript", "process.exit(0)"],
5861
[`bun -e 'console.log(Bun.version)'`, "typescript", "console.log(Bun.version)"],
5962
[`deno eval 'console.log(1)'`, "typescript", "console.log(1)"],

‎apps/web/src/lib/embeddedScripts.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,14 @@ function forEachCommand(
262262
}
263263

264264
function programName(word: Word): string {
265-
return (word.value.split(/[\\/]/u).at(-1) ?? "").toLowerCase().replace(/\.exe$/u, "");
265+
// Bash reads `C:\Python312\python.exe` unquoted as escapes; Windows meant a path.
266+
const path = /\\/u.test(word.text) ? word.text.replace(/^["']|["']$/gu, "") : word.value;
267+
return (path.split(/[\\/]/u).at(-1) ?? "").toLowerCase().replace(/\.exe$/u, "");
266268
}
267269

268270
function interpreterFor(name: string): Interpreter | undefined {
269-
if (/^python(?:\d+(?:\.\d+)?)?$|^pypy3?$/u.test(name)) return PYTHON;
271+
// `py` is the Windows Python launcher, as in `py -3 -c '…'`.
272+
if (/^python(?:\d+(?:\.\d+)?)?$|^pypy3?$|^py$/u.test(name)) return PYTHON;
270273
return Object.hasOwn(INTERPRETERS, name) ? INTERPRETERS[name] : undefined;
271274
}
272275

‎packages/client-runtime/src/work-log/commandLabel.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,16 @@ describe("commandHighlightLanguage", () => {
583583
["git status; pwsh -c 'Get-Date'", "shellscript"],
584584
["cat <<'EOF' > notes.txt\npwsh\nEOF", "shellscript"],
585585
["", "shellscript"],
586+
// Windows agents also run PowerShell directly, without a pwsh wrapper.
587+
["Get-Content package.json | Select-String version", "powershell"],
588+
["$env:CI='1'; npm test", "powershell"],
589+
["$tmp = Join-Path $env:TEMP repo; git clone example", "powershell"],
590+
['"=== CHECK FILE ==="; Get-Content file.txt', "powershell"],
591+
["& 'C:\\Python312\\python.exe' -c 'print(1)'", "powershell"],
592+
["update-alternatives --list java", "shellscript"],
593+
["echo Get-Content; git status", "shellscript"],
594+
["FOO=1 npm test", "shellscript"],
595+
["echo $HOME && ls", "shellscript"],
586596
])("%s", (command, language) => {
587597
expect(commandHighlightLanguage(command)).toBe(language);
588598
});

‎packages/client-runtime/src/work-log/commandLabel.ts‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,13 +1410,43 @@ function withoutShellWrapper(command: string): string {
14101410

14111411
const POWERSHELL_PROGRAMS = new Set(["powershell", "pwsh"]);
14121412

1413-
/** The grammar to highlight a command with: PowerShell when pwsh or powershell runs it. */
1413+
// Common approved PowerShell verbs. Cmdlets are capitalized Verb-Noun, which
1414+
// keeps lowercase Linux tools such as `update-alternatives` out.
1415+
const POWERSHELL_CMDLET =
1416+
/^(?:Add|Clear|Compare|Compress|ConvertFrom|ConvertTo|Copy|Expand|Export|ForEach|Format|Get|Group|Import|Invoke|Join|Measure|Move|New|Out|Pop|Push|Read|Remove|Rename|Resolve|Select|Set|Sort|Split|Start|Stop|Test|Wait|Where|Write)-[A-Z][A-Za-z]*$/u;
1417+
1418+
/**
1419+
* The grammar to highlight a command with: PowerShell when pwsh or powershell
1420+
* runs it, or when it is written in PowerShell, as Windows agents run it.
1421+
*/
14141422
export function commandHighlightLanguage(command: string): "powershell" | "shellscript" {
14151423
const program = /^(?:&\s*)?(?:"([^"]*)"|'([^']*)'|(\S+))/u.exec(command.trim());
14161424
const name = (program?.[1] ?? program?.[2] ?? program?.[3] ?? "")
14171425
.split(/[\\/]/u)
14181426
.at(-1)
14191427
?.toLowerCase()
14201428
.replace(/\.exe$/u, "");
1421-
return name && POWERSHELL_PROGRAMS.has(name) ? "powershell" : "shellscript";
1429+
if (name && POWERSHELL_PROGRAMS.has(name)) return "powershell";
1430+
return isPowerShellScript(command) ? "powershell" : "shellscript";
1431+
}
1432+
1433+
/** Whether any statement starts like PowerShell and never like POSIX shell. */
1434+
function isPowerShellScript(command: string): boolean {
1435+
let rest: string | null = command.trim();
1436+
// A leading call operator; POSIX shell cannot start a command with `&`.
1437+
if (/^&\s*\S/u.test(rest)) return true;
1438+
for (let segment = 0; rest && segment < MAX_COMMAND_SEGMENTS; segment += 1) {
1439+
const { firstCommand, remainingCommand } = splitFirstShellCommand(rest);
1440+
const statement = firstCommand.trim();
1441+
if (
1442+
// `$env:NAME` or `$name = value`; neither parses as POSIX shell.
1443+
/^\$(?:env|global|local|script):/iu.test(statement) ||
1444+
/^\$[A-Za-z_]\w*\s+=/u.test(statement) ||
1445+
POWERSHELL_CMDLET.test(statement.match(/^\S+/u)?.[0] ?? "")
1446+
) {
1447+
return true;
1448+
}
1449+
rest = remainingCommand;
1450+
}
1451+
return false;
14221452
}

0 commit comments

Comments
 (0)