Escape Powershell Variables Starting with - (dash) in a Cmdlet or program call?

Viewed 1348

So, I've just been pulling my hair out and yelling for the past 30 minutes, because I've learned that (apparently?) PowerShell will silently strip out the first - in any variable, regardless of if its escaped, if that variable starts with a - and is being used in a path. Here's my script, which is a simple wrapper around FFMPEG to merge two audio tracks into one and clip a video:

function IsNull($objectToCheck) {
    if ($objectToCheck -eq $null) {
        return $true
    }

    if ($objectToCheck -is [String] -and $objectToCheck -eq [String]::Empty) {
        return $true
    }

    if ($objectToCheck -is [DBNull] -or $objectToCheck -is [System.Management.Automation.Language.NullString]) {
        return $true
    }

    return $false
}

$_input = [Management.Automation.WildcardPattern]::Escape($args[0])
$time = $args[1]
$duration = $args[2]

if ($time -match ":") {
    $_matches = ($time | Select-String ":" -AllMatches).Matches
    $seconds = $time.Substring($_matches[$_matches.Count - 1].Index + 1, 2) -as [int]
    $finalSeconds = $seconds - 1
    $timeFlag2 = "-ss 1"
    $time = $time.Substring(0, $_matches[$_matches.Count - 1].Index) + (":{0:d2}" -f $finalSeconds)
    # there's totally a bug in here where we don't handle :00 seconds, plz ignore
}
else {
    $timeFlag2 = ""
}

if (-not (IsNull($duration))) {
    $d = "-t " + $duration
}
else {
    $d = ""
}

Write-Verbose "C:\ffmpeg.exe -ss $time -i $_input $timeFlag2 -filter_complex '[0:a:0]volume=0.6[l];[0:a:1]volume=1.2[k];[k][l]amerge=inputs=2[a]' -map '0:v:0' -map '[a]' -c:v copy -c:a libmp3lame -q:a 3 -ac 2 $d 'output-muxxed.mp4'" -Verbose

C:\ffmpeg.exe -ss $time -i $_input $timeFlag2 -filter_complex '[0:a:0]volume=0.6[l];[0:a:1]volume=1.2[k];[k][l]amerge=inputs=2[a]' -map '0:v:0' -map '[a]' -c:v copy -c:a libmp3lame -q:a 3 -ac 2 $d 'output-muxxed.mp4'

All I want to do is: take in a file, take in a timestamp to start clipping from, and then a duration. I do a small bit of math so I can pass -ss before and after the -i call to fix a weird green frame bug when working with ShadowPlay.

This one is currently broken and will give the error: Unrecognized option ss 1. K, so I figured the variable wasn't getting escaped properly. So I updated the last command to add quotes around it, and still no dice. So then I decided to do: $timeFlag2 = "`-ss 1", still absolutely no luck. During all of this, the command Write-Verbose is producing is:

VERBOSE: C:\ffmpeg.exe -ss 00:02:01 -i .\input.mp4 -ss 1 -filter_complex '[0:a:0]volume=0.6[l];[0:a:1]volume=1.2[k];[k][l]amerge=inputs=2[a]' -map '0:v:0' -map '[a]' -c:v copy -c:a libmp3lame -q:a 3 -ac 2  -t 40 'output-muxxed.mp4'

Which is 100% correct and works when copy-pasted to the terminal. After doing a shit ton of searching, I decided I'd try one last thing and I updated the $timeFlag2 and $d assignment calls to be:

$timeFlag2 = " -ss 1"
$d = " -t " + $duration

And the world worked... All errors were gone. Removing the spaces reintroduces them.

So, my question is what in the world is going on here? Why doesn't `, the escape operator, escape -'s? Why does the space escape it? Why does this appear to be properly escaped when passed to a string for Write-Verbose, but not when passed to a Path cmdlet or whatever the hell that's called?

I'm a loooonnnnnggg-time bash user, but have been using PS for like 6 weeks, so I'm sure there's tons wrong here and that I'm missing, so all advice/help/critiques are highly appreciated!

Thanks in advance!

1 Answers

Alright, got this figured out, especially from an ex-BASHer's PoV. So, BASH is REAL loose about argument escaping and how spaces work, a common idiom in bash is to construct commands as: mv "$src" "$dest", just in case $src or $dest have spaces in them. zsh and modern ilk handle this, but BASH doesn't. This is how I was used to writing shell scripts.

This is not how Windows PowerShell works (obviously). PS does a really good job of escaping its variables and command parameters entirely behind the scenes. What was happening to me here was that when I set the var: $timeFlag2 = "-ss 1" and passed that, PS was literally passing: "-ss 1" to the underlying command, regardless of how I escaped it or what I did. This isn't immediately obvious with the errors you get back, unless you sort of understand what errors command escaping can result in (Invalid flag with name: "$THIS_IS_CLEARLY_A_VALID_FLAG"), and what not.

The right way to do this is to split the flags up into two vars: $timePiece1 = "-ss" and $timePiece2 = "1" and then call the command using both. Even better is splatting, which is similar to BASH's space expansion, but MUCH smarter. Here's what the main fix ended up being:

$FFMPEG_PARAMS = @(
  "-ss",
  $time,
  "-i",
  $_input,
  $time2Flag,
  $time2Val,
  "-filter_complex",
  "[0:a:0]volume=0.6[l];[0:a:1]volume=1.2[k];[k][l]amerge=inputs=2[a]",
  "-map",
  "0:v:0",
  "-map",
  "[a]",
  "-c:v",
  "copy",
  "-c:a",
  "libmp3lame",
  "-q:a",
  "3",
  "-ac",
  "2",
  $d,
  $duration,
  "output-muxxed.mp4"
)

C:\ffmpeg.exe @FFMPEG_PARAMS

Breaking the params up and using splatting solved it! Here's the entire full script, for reference:

function IsNull($objectToCheck) {
    if ($objectToCheck -eq $null) {
        return $true
    }

    if ($objectToCheck -is [String] -and $objectToCheck -eq [String]::Empty) {
        return $true
    }

    if ($objectToCheck -is [DBNull] -or $objectToCheck -is [System.Management.Automation.Language.NullString]) {
        return $true
    }

    return $false
}

$_input = [Management.Automation.WildcardPattern]::Escape($args[0])
$time = $args[1]
$duration = $args[2]
if ($time -match ":") {
    $_matches = ($time | Select-String ":" -AllMatches).Matches
    $seconds = $time.Substring($_matches[$_matches.Count - 1].Index + 1, 2) -as [int]
    $finalSeconds = $seconds - 1
    $time2Flag = "-ss"
    $time2Val = "1"
    $time = $time.Substring(0, $_matches[$_matches.Count - 1].Index) + (":{0:d2}" -f $finalSeconds)
}
else {
    $time2Flag = ""
    $time2Val = ""
}

if (-not (IsNull($duration))) {
    $d = "-t"
}
else {
    $d = ""
}

$FFMPEG_PARAMS = @(
  "-ss",
  $time,
  "-i",
  $_input,
  $time2Flag,
  $time2Val,
  "-filter_complex",
  "[0:a:0]volume=0.6[l];[0:a:1]volume=1.2[k];[k][l]amerge=inputs=2[a]",
  "-map",
  "0:v:0",
  "-map",
  "[a]",
  "-c:v",
  "copy",
  "-c:a",
  "libmp3lame",
  "-q:a",
  "3",
  "-ac",
  "2",
  $d,
  $duration,
  "output-muxxed.mp4"
)
Write-Verbose "C:\ffmpeg.exe -ss $time -i $_input $time2Flag $time2Val -filter_complex '[0:a:0]volume=0.6[l];[0:a:1]volume=1.2[k];[k][l]amerge=inputs=2[a]' -map '0:v:0' -map '[a]' -c:v copy -c:a libmp3lame -q:a 3 -ac 2 $d $duration 'output-muxxed.mp4'" -Verbose

C:\ffmpeg.exe @FFMPEG_PARAMS

I've learned a valuable PS lesson today.

Related