-
Notifications
You must be signed in to change notification settings - Fork 35
fix: CDK app fails to launch if paths contain spaces #645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rix0rrr
wants to merge
18
commits into
main
Choose a base branch
from
huijbers/fix-exec-spawn-cross-platform
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
370a0cc
fix: use arrays instead of strings with exec/spawn for cross-platform…
rix0rrr 3b0cf8c
fix: remove backwards compatibility functions
rix0rrr 9f54fb8
fix: use execFileSync instead of spawnSync for smaller API delta
rix0rrr edabc74
fix: update tests to work with array-based exec/spawn
rix0rrr b076fa9
fix: use promisified execFile instead of custom function
rix0rrr 549b717
I'm not happy with AI
rix0rrr 27c245c
Missed a spot
rix0rrr eb21994
We can now use `shell: false`.
rix0rrr d372d4c
WIP
rix0rrr 5ccc67d
Merge remote-tracking branch 'origin/main' into huijbers/fix-exec-spa…
rix0rrr 9caecf9
Merge remote-tracking branch 'origin/main' into huijbers/fix-exec-spa…
rix0rrr 1a2545b
WIP
rix0rrr b490692
WIP
rix0rrr 4e73ada
WIP
rix0rrr 0355da8
Fix tests by adding shell: true
rix0rrr b151fa7
Update docs
rix0rrr 573f015
Two more places
rix0rrr 72bf9c4
Exports
rix0rrr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
266 changes: 266 additions & 0 deletions
266
packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/command-line.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
import { ToolkitError } from '../../toolkit/toolkit-error'; | ||
|
||
type ShellSyntax = 'posix' | 'windows'; | ||
|
||
/** | ||
* Class to help with parsing and formatting command-lines | ||
* | ||
* What syntax we recognizing is an attribute of the `parse` and `toString()` operations, | ||
* NOT of the command line itself. Defaults to the current platform. | ||
*/ | ||
export class CommandLine { | ||
/** | ||
* Parse a command line into components. | ||
* | ||
* On Windows, emulates the behavior of `CommandLineToArgvW`. On Linux, emulates the behavior of a POSIX shell. | ||
* | ||
* (See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw) | ||
*/ | ||
public static parse(cmdLine: string, syntax?: ShellSyntax) { | ||
const argv = isWindows(syntax) ? parseCommandLineWindows(cmdLine) : parseCommandLinePosix(cmdLine); | ||
return new CommandLine(argv); | ||
} | ||
|
||
constructor(public readonly argv: string[]) { | ||
} | ||
|
||
/** | ||
* Render the command line as a string, taking care only to quote whitespace and quotes | ||
* | ||
* Any other special characters are left in exactly as-is. | ||
*/ | ||
public toStringGrouped(syntax?: ShellSyntax) { | ||
if (isWindows(syntax)) { | ||
return formatCommandLineWindows(this.argv, /^\\S+$/); | ||
} else { | ||
return formatCommandLinePosix(this.argv, /^\\S+$/); | ||
} | ||
} | ||
|
||
/** | ||
* Render the command line as a string, escaping characters that would be interpreted by the shell | ||
* | ||
* The command will be a command invocation with literal parameters, nothing else. | ||
*/ | ||
public toStringInert(syntax?: ShellSyntax) { | ||
if (isWindows(syntax)) { | ||
return formatCommandLineWindows(this.argv, /^[a-zA-Z0-9._\-+=/:]+$/); | ||
} else { | ||
return formatCommandLinePosix(this.argv, /^[a-zA-Z0-9._\-+=/:]+$/); | ||
} | ||
} | ||
|
||
public toString() { | ||
return this.toStringGrouped(); | ||
} | ||
} | ||
|
||
/** | ||
* Parse command line on Windows | ||
* | ||
* @see https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments?view=msvc-170 | ||
*/ | ||
function parseCommandLineWindows(commandLine: string): string[] { | ||
const ret: string[] = []; | ||
let current = ''; | ||
let quoted = false; | ||
let backSlashcount = 0; | ||
|
||
for (let i = 0; i < commandLine.length; i++) { | ||
const c = commandLine[i]; | ||
|
||
if (c === '\\') { | ||
backSlashcount += 1; | ||
continue; | ||
} | ||
|
||
// We also allow quoting " by doubling it up. | ||
if (c === '"' && i + 1 < commandLine.length && commandLine[i + 1] === '"') { | ||
current += '"'; | ||
i += 1; | ||
continue; | ||
} | ||
|
||
// Only type of quote is ", and backslashes only behave specially before a " | ||
if (c === '"') { | ||
if (backSlashcount % 2 === 0) { | ||
current += '\\'.repeat(backSlashcount / 2); | ||
quoted = !quoted; | ||
} else { | ||
current += '\\'.repeat(Math.floor(backSlashcount / 2)) + '"'; | ||
} | ||
backSlashcount = 0; | ||
|
||
continue; | ||
} | ||
|
||
if (backSlashcount > 0) { | ||
current += '\\'.repeat(backSlashcount); | ||
backSlashcount = 0; | ||
} | ||
|
||
if (quoted) { | ||
current += c; | ||
continue; | ||
} | ||
|
||
if (isWhitespace(c)) { | ||
if (current) { | ||
ret.push(current); | ||
} | ||
current = ''; | ||
continue; | ||
} | ||
|
||
current += c; | ||
} | ||
|
||
if (current) { | ||
ret.push(current); | ||
} | ||
|
||
return ret; | ||
} | ||
|
||
function isWhitespace(char: string): boolean { | ||
return char === ' ' || char === '\t'; | ||
} | ||
|
||
function isWindows(x?: ShellSyntax) { | ||
return x ? x === 'windows' : process.platform === 'win32'; | ||
} | ||
|
||
function parseCommandLinePosix(commandLine: string): string[] { | ||
const result: string[] = []; | ||
let current = ''; | ||
let inDoubleQuote = false; | ||
let inSingleQuote = false; | ||
let escapeNext = false; | ||
|
||
for (let i = 0; i < commandLine.length; i++) { | ||
const char = commandLine[i]; | ||
|
||
// Handle escape character | ||
if (escapeNext) { | ||
// In double quotes, only certain characters are escaped | ||
if (inDoubleQuote && !'\\"$`'.includes(char)) { | ||
current += '\\'; | ||
} | ||
current += char; | ||
escapeNext = false; | ||
continue; | ||
} | ||
|
||
if (char === '\\' && !inSingleQuote) { | ||
escapeNext = true; | ||
continue; | ||
} | ||
|
||
// Handle quotes | ||
if (char === '"' && !inSingleQuote) { | ||
inDoubleQuote = !inDoubleQuote; | ||
continue; | ||
} | ||
|
||
if (char === "'" && !inDoubleQuote) { | ||
inSingleQuote = !inSingleQuote; | ||
continue; | ||
} | ||
|
||
// Handle whitespace | ||
if (!inDoubleQuote && !inSingleQuote && /\s/.test(char)) { | ||
if (current) { | ||
result.push(current); | ||
current = ''; | ||
} | ||
continue; | ||
} | ||
|
||
current += char; | ||
} | ||
|
||
// Add the last argument if there is one | ||
if (current) { | ||
result.push(current); | ||
} | ||
|
||
// Check for unclosed quotes | ||
if (inDoubleQuote || inSingleQuote) { | ||
throw new ToolkitError('Unclosed quotes in command line'); | ||
} | ||
|
||
// Check for trailing backslash | ||
if (escapeNext) { | ||
throw new ToolkitError('Trailing backslash in command line'); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
/** | ||
* Format a command line in a sensible way | ||
*/ | ||
function formatCommandLinePosix(argv: string[], componentIsSafe: RegExp): string { | ||
return argv.map(arg => { | ||
// Empty string needs quotes | ||
if (arg === '') { | ||
return '\'\''; | ||
} | ||
|
||
// If argument contains no problematic characters, return it as-is | ||
if (componentIsSafe.test(arg)) { | ||
return arg; | ||
} | ||
|
||
const escaped = Array.from(arg).map(char => char === '\'' || char === '\\' ? `\\${char}` : char); | ||
return `'${escaped}'`; | ||
}).join(' '); | ||
} | ||
|
||
/** | ||
* Format a command line in a sensible way | ||
*/ | ||
function formatCommandLineWindows(argv: string[], componentIsSafe: RegExp): string { | ||
return argv.map(arg => { | ||
// Empty string needs quotes | ||
if (arg === '') { | ||
return '""'; | ||
} | ||
|
||
// If argument contains no problematic characters, return it as-is | ||
if (componentIsSafe.test(arg)) { | ||
return arg; | ||
} | ||
|
||
let escaped = '"'; | ||
let backslashCount = 0; | ||
|
||
for (let i = 0; i < arg.length; i++) { | ||
const char = arg[i]; | ||
|
||
if (char === '\\') { | ||
// Count consecutive backslashes | ||
backslashCount++; | ||
} else if (char === '"') { | ||
// Double the backslashes before a quote and escape the quote | ||
escaped += '\\'.repeat(backslashCount * 2 + 1) + '"'; | ||
backslashCount = 0; | ||
} else { | ||
// Add accumulated backslashes if any | ||
if (backslashCount > 0) { | ||
escaped += '\\'.repeat(backslashCount); | ||
backslashCount = 0; | ||
} | ||
escaped += char; | ||
} | ||
} | ||
|
||
// Handle trailing backslashes before the closing quote | ||
if (backslashCount > 0) { | ||
escaped += '\\'.repeat(backslashCount * 2); | ||
} | ||
|
||
escaped += '"'; | ||
return escaped; | ||
}).join(' '); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we can make this assumption. What about Windows users using
bash
?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know, but
subprocess.spawn(..., { shell: true })
will always usecmd.exe
on Windows.If you want bash you'll need to explicitly do
subprocess.spawn("bash -c \"...\"", { ... })
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well actually it does respect
%COMSPEC%
, so let us also respect that.