Home > front end >  How can use a PowerShell script to run the Terraform CLI and pass a variable of type map?
How can use a PowerShell script to run the Terraform CLI and pass a variable of type map?

Time:12-23

The variable in the Terraform file (infrastructure.tf) is declared like this:

variable "tags" {
  type = map(string)
}

This is the PowerShell code that executes the terraform command-line program with the plan command:

$command = "plan"
$options = @(
    "'--var=tags={a:\`"b\`"}'"
    "--out=path/to/out.tfplan"
)

Write-Host terraform,$command,$options

& terraform $command $options

The Write-Host command's output is:

terraform plan '--var=tags={a:\"b\"}' --out=path/to/out.tfplan

If I copy paste that into an interactive PowerShell 7.2 (pwsh) session then it works. But the & terraform command fails with this error:

Error: Too many command line arguments

To specify a working directory for the plan, use the global -chdir flag.

I know the Terraform documentation for the plan command warns against using PowerShell to run the command, due to quoting issues (emphasis mine):

PowerShell on Windows cannot correctly pass literal quotes to external programs, so we do not recommend using Terraform with PowerShell when you are on Windows. Use Windows Command Prompt instead.

Unfortunately they don't specify if they mean Windows PowerShell 5.1 included in Windows (powershell.exe), or also PowerShell 7.1 running on Windows (pwsh). But with pwsh it is clearly possible to run the plan command in an interactive PowerShell session (I am using macOS, but some others use Windows) and pass literal quotes to an external program. It just seems that running the same command from a .ps1 file does not work.

Since our whole dev/ops tooling is PowerShell-based, I'd like to know: is this possible?

Because if it is not, then we will have to work around this limitation.

Edit: Some things I've tried:

  • Use splatting for $options (e.g. @options)
  • Add the stop-parsing token (e.g. --%) as the first item in $options (that token is actually "only intended for use on Windows platforms")
  • Many variations of single/double quotes
  • Remove the \ (I am not actually sure why this seems required, the [map] literal syntax is either {"x"="y"} or {x:"y"}), but without the \ copy-pasting the printed command line in an interactive PowerShell also does not work.

CodePudding user response:

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
  • Related