tl;dr
Omit the embedded enclosing '...'
around --var=...
, because they will become a literal part of your argument.
The - unfortunate - need to manually \
-escape the embedded "
instances, even though PowerShell itself does not need it, is the result of a long-standing bug that was finally fixed in PowerShell (Core) 7.3.0; in 7.3.0 and up to at least 7.3.1, the fix is in effect by default, which breaks the solution below, and therefore requires $PSNativeCommandArgumentPassing = 'Legacy'
; however, it looks like the fix will become opt-in in the future, i.e. the old, broken behavior (Legacy
) will become the default again - see this answer.
Using Write-Host
to inspect the arguments isn't a valid test, because, as a PowerShell command, it isn't subject to the same rules as an external program.
- For ways to troubleshoot argument-passing to external programs, see the bottom section of this answer.
$command = "plan"
$options = @(
"--var=tags={a:\`"b\`"}" # NO embedded '...' quoting
"--out=path/to/out.tfplan"
)
# No point in using Write-Host
& { # Run in a child scope to localize the change to $PSNativeCommandArgumentPassing
# Note: Only needed if you're (also) running on PowerShell 7.3+
$PSNativeCommandArgumentPassing = 'Legacy'
& terraform $command $options
}
How to control the exact process command line on Windows / pass arguments with embedded double quotes properly on Unix:
Note: The solution above relies on PowerShell's old, broken behavior, and while it works in the case at hand, a fully robust and less conceptually confusing solution requires more explicit control over how the arguments are passed, as shown below.
A cross-edition, cross-version, cross-platform solution:
Assuming that terraform
must see --var=tags={a:\"b\"}
on its process command line on Windows, i.e. needs to see the argument as verbatim --var=tags={a:"b"}
after parsing its command line, combine --%
, the stop-parsing token, with splatting, which gives you full control over how the Windows process command line is built behind the scenes:
$command = "plan"
$options = @(
'--%'
'--var=tags={a:\"b\"}'
'--out=path/to/out.tfplan'
)
& { # Run in a child scope to localize the change to $PSNativeCommandArgumentPassing
# !! Required in v7.3.0 and up to at least v7.3.1, due to a BUG.
$PSNativeCommandArgumentPassing = 'Legacy'
& terraform $command @options
}
This creates the following process command line behind the scenes on Windows (using an example terraform
path):
C:\path\to\terraform.exe plan --var=tags={a:\"b\"} --out=path/to/out.tfplan
Note:
In PowerShell (Core) 7.3.0 and at least up to 7.3.1, --%
is broken by default, in that its proper functioning is mistakenly tied to value of the v7.3+ $PSNativeCommandArgumentPassing
preference variable; thus, (temporarily) setting $PSNativeCommandArgumentPassing = 'Legacy'
is required, as shown above - see GitHub issue #18664 for the bug report.
Even though --%
is primarily intended for Windows, it works on Unix-like platforms too, as long as you use the Microsoft C/C++ command-line syntax rules to formulate the arguments; specifically, this means:
- only use
"
characters for quoting (with syntactic function)
- use
\
only to escape "
chars.
While you can use --%
without splatting, doing so comes with severe limitations - see this answer.
A simpler, but Windows-only cross-edition, cross-version solution:
Calling via cmd /c
also gives you control over how the command line is constructed:
$command = "plan"
$options = @(
'--var=tags={a:\"b\"}'
'--out=path/to/out.tfplan'
)
cmd /c "terraform $command $options"
Note: This is often more convenient than --%
, but suboptimal, because:
- The intermediary
cmd.exe
call creates extra overhead.
%
characters may be interpreted by cmd.exe
, and, in unquoted arguments, additional metacharacters such as &
and ^
- preventing that requires extra effort.
A v7.3+ cross-platform solution:
Relying on PowerShell's corrected behavior in v7.3+ (no need for manual \
-escaping anymore) requires setting $PSNativeCommandArgumentPassing
to 'Standard'
.
- Note: If you target only Unix-like platforms, that isn't necessary.
$command = "plan"
$options = @(
'--var=tags={a:"b"}' # Note: NO \-escaping of " required anymore.
'--out=path/to/out.tfplan'
)
& { # Run in a child scope to localize the change to $PSNativeCommandArgumentPassing
# Necessary on Windows only.
$PSNativeCommandArgumentPassing = 'Standard'
& terraform $command $options
}
Note: On Windows, this creates a slightly different process command line than the solutions above; notably, --var=tags={a:\"b\"}
is enclosed in "..."
as a whole; however, well-behaved CLIs should parse this as verbatim --var=tags={a:"b"}
too, whether enclosed in "..."
or not.
C:\path\to\terraform.exe plan "--var=tags={a:\"b\"}" --out=path/to/out.tfplan