Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions ParallelKit.Harness.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2683,6 +2683,24 @@ if ($compiled) {
Test-Assert ((ConvertTo-PkArgumentString $null) -eq '') "and takes `$null itself"
Test-Assert ((ConvertTo-PkArgumentString 'two words') -eq '"two words"' -and (ConvertTo-PkArgumentString @(1, 'x y')) -eq '1 "x y"') "it still takes one string, and non-strings"

# cmd.exe's special characters. A .bat/.cmd program runs through cmd.exe,
# which acts on & | < > ^ outside quotes and splits batch arguments at , ;
# so an element holding one is quoted even without a space. 'a&ver' used to
# reach a batch file as 'a' and run ver.
$special = @('a&b', 'a|b', 'a<b', 'a>b', 'a^b', '(x)', 'a,b', 'a;b')
Test-Assert ((ConvertTo-PkArgumentString $special) -eq '"a&b" "a|b" "a<b" "a>b" "a^b" "(x)" "a,b" "a;b"') "an element with a cmd.exe special character is quoted"
Test-Assert ((Get-BatchArgvText $special) -eq '[a&b][a|b][a<b][a>b][a^b][(x)][a,b][a;b]') "and an ordinary program still gets it exactly"
Test-Assert ((ConvertTo-PkArgumentString @('a=b', '100%', 'a!b')) -eq 'a=b 100% a!b') "= % and ! are left alone"
$batFile = Join-Path $env:TEMP ("pk-args-" + [guid]::NewGuid().ToString("N") + ".cmd")
# set "NAME=value" keeps & | ^ literal, so this prints what cmd handed over.
[System.IO.File]::WriteAllText($batFile, "@echo off`r`nset `"PKARG1=%~1`"`r`nset `"PKARG2=%~2`"`r`nset PKARG`r`n")
foreach ($value in @('a&ver', 'a|b', 'a^b', 'a,b', 'a;b', '(x)')) {
$batText = $null
try { $batText = (Invoke-PkProcessWithTimeout -FilePath $batFile -Arguments @($value) -TimeoutMs 10000).Stdout.Trim() } catch { $batText = [string]$_ }
Test-Assert ($batText -ceq ('PKARG1=' + $value)) ("a .cmd program gets '" + $value + "' as one argument, and nothing else runs (" + $batText + ")")
}
try { [System.IO.File]::Delete($batFile) } catch { } # temp file only; best effort

# A string is still exactly as written.
Test-Assert ((Get-BatchArgvText '"C:\My Files\a.png" out.jxl') -eq $spacedText) "a string is still passed exactly as written"
# The documented change: list elements are values, not pieces of a command line.
Expand Down
16 changes: 13 additions & 3 deletions ParallelKit.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -694,8 +694,14 @@ function ConvertTo-PkArgumentString {
# An argument with a space, tab, newline or quote is wrapped in quotes, its
# quotes become \", and backslashes are doubled where they come before a quote.
# These are the C runtime's rules, which most Windows programs use; cmd.exe /c
# parses its own way. The kit's -Arguments and Arguments values do this
# themselves when given a list.
# parses its own way. An argument with one of & | < > ^ ( ) , ; is quoted
# too, so a .bat or .cmd program, which runs through cmd.exe, gets it as one
# argument instead of cmd acting on it. Not covered for those programs: cmd
# expands %NAME% inside quotes, splits batch arguments at = (left unquoted,
# since some programs read NAME=value themselves), and counts a \" as a
# quote, so an argument containing a quote can end up partly unquoted. The
# kit's -Arguments and Arguments values do this themselves when given a
# list.
# .PARAMETER Arguments
# The arguments, in order. A $null one is left out; an empty string is passed
# as "".
Expand All @@ -720,7 +726,11 @@ function ConvertTo-PkArgumentString {
foreach ($item in $Arguments) {
if ($null -eq $item) { continue }
$arg = [string]$item
if ($arg.Length -gt 0 -and $arg.IndexOfAny([char[]]@(' ', "`t", "`n", "`v", '"')) -lt 0) {
# The C runtime only needs quotes around whitespace and quotes. The rest
# are cmd.exe's: it runs a .bat/.cmd program's command line, acting on
# & | < > ^ ( ) outside quotes and splitting batch arguments at , ;
# Quoting them is harmless to other programs.
if ($arg.Length -gt 0 -and $arg.IndexOfAny([char[]]@(' ', "`t", "`n", "`v", '"', '&', '|', '<', '>', '^', '(', ')', ',', ';')) -lt 0) {
$parts.Add($arg)
continue
}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ $batch = Invoke-PkRun { Invoke-PkProcessBatch @batchParams }

Long parameter lists in these examples use splatting (`@batchParams`) instead of trailing backticks: it's easier to read, and a stray space after a backtick can't break it.

Each command is a hashtable with `FilePath`, `Arguments` and optionally `WorkingDirectory`, plus any keys of your own. `Arguments` is either one string, passed to the program exactly as written, or a list, where each element is one argument: `Arguments = @($in, $out)` works with spaces in the paths. A list is quoted the way `ConvertTo-PkArgumentString @(...)` quotes it, so the program gets exactly that list back, spaces, quotes and trailing backslashes included, and `$null` elements are left out. Quotes inside an element are passed on as quote characters, so don't pre-quote list elements. It follows the rules most Windows programs use; `cmd.exe /c` parses its own way, so give it one string. Programs run hidden, with their output captured.
Each command is a hashtable with `FilePath`, `Arguments` and optionally `WorkingDirectory`, plus any keys of your own. `Arguments` is either one string, passed to the program exactly as written, or a list, where each element is one argument: `Arguments = @($in, $out)` works with spaces in the paths. A list is quoted the way `ConvertTo-PkArgumentString @(...)` quotes it, so the program gets exactly that list back, spaces, quotes and trailing backslashes included, and `$null` elements are left out. Quotes inside an element are passed on as quote characters, so don't pre-quote list elements. It follows the rules most Windows programs use; `cmd.exe /c` parses its own way, so give it one string. A `.bat` or `.cmd` program runs through `cmd.exe` too. A list element holding `&`, `|`, `<`, `>`, `^`, `(`, `)`, `,` or `;` is quoted, so such a program gets it as one argument. What quoting can't cover there: `cmd.exe` expands `%NAME%` in an argument, quoted or not, when a variable by that name exists; it splits batch arguments at `=`, which isn't quoted because some programs read `NAME=value` from the command line themselves; and an element containing a `"` can leave the characters after it unquoted as far as `cmd.exe` is concerned. Programs run hidden, with their output captured.

Programs run in your current location (`Get-Location`) unless a command says otherwise, and a relative `WorkingDirectory`, or a `FilePath` with a folder in it like `.\tools\cjxl.exe`, is relative to that location too. Drives PowerShell knows, like `~` or one made with `New-PSDrive`, work in both. A bare name like `cjxl.exe` is found the way Windows finds programs, mostly on `PATH`. A `.bat` or `.cmd` program can't run in a network-share (`\\server\share`) folder: `cmd.exe` falls back to `C:\Windows` there, so give such commands a local `WorkingDirectory`.

Expand Down
Loading