3

Why does PowerShell treat quoted parameters differently when you invoke the script directly (in a PowerShell console or ISE) or when you invoke it via another PowerShell instance?

Here is the script (TestQuotes.ps1):

param
(
    [string]
    $Config = $null
)

"Config = $Config"

Here are the results:

PS D:\Scripts> .\TestQuotes.ps1 -Config "A B C"
Config = A B C
PS D:\Scripts> PowerShell .\TestQuotes.ps1 -Config "A B C"
Config = A
PS D:\Scripts> .\TestQuotes.ps1 -Config 'A B C'
Config = A B C
PS D:\Scripts> PowerShell .\TestQuotes.ps1 -Config 'A B C'
Config = A

Any ideas?

Alek Davis
  • 10,628
  • 2
  • 41
  • 53
  • the ones that call powershell.exe are NOT doing the same thing as the ones that start a script with parameters. take a look at `powershell.exe /?` for some ideas. it's mostly the way you are handing strings to the exe file. – Lee_Dailey Mar 16 '19 at 22:10

2 Answers2

8

According to the PowerShell.exe Command-Line Help, the first argument for the powershell executable is -Command:

PowerShell[.exe]
       [-Command { - | <script-block> [-args <arg-array>]
                     | <string> [<CommandParameters>] } ]
       [-EncodedCommand <Base64EncodedCommand>]
       [-ExecutionPolicy <ExecutionPolicy>]
       [-File <FilePath> [<Args>]]
       [-InputFormat {Text | XML}]
       [-Mta]
       [-NoExit]
       [-NoLogo]
       [-NonInteractive]
       [-NoProfile]
       [-OutputFormat {Text | XML}]
       [-PSConsoleFile <FilePath> | -Version <PowerShell version>]
       [-Sta]
       [-WindowStyle <style>]

PowerShell[.exe] -Help | -? | /?

Any text after -Command is sent as a single command line to PowerShell.

...

When the value of -Command is a string, Command must be the last parameter specified because any characters typed after the command are interpreted as the command arguments.

What child PowerShell instance actually receives is easy to check with echoargs:

PS > echoargs .\TestQuotes.ps1 -Config "A B C"
Arg 0 is <.\TestQuotes.ps1>
Arg 1 is <-Config>
Arg 2 is <A B C>

Which is further parsed by the child instance to:

'.\TestQuotes.ps1' '-Config' 'A' 'B' 'C'

And this is where you get your "wrong" result: Config = A

If you specify -File argument, you'll get the desired result:

PS >  PowerShell -File .\TestQuotes.ps1 -Config 'A B C'
Config = A B C

PS >  PowerShell -Command .\TestQuotes.ps1 -Config 'A B C'
Config = A
beatcracker
  • 6,714
  • 1
  • 18
  • 41
5

tl;dr

If you're calling another PowerShell instance from PowerShell, use a script block ({ ... }) to get predictable behavior:

Windows PowerShell:

# To pass arguments to the script block, append -args ...
powershell.exe { .\TestQuotes.ps1 -Config "A B C" }

PowerShell Core:

# To pass arguments to the script block, append -args ...
pwsh { .\TestQuotes.ps1 -Config "A B C" }

This will make the quoting of arguments work as expected - and it will even return objects with near-type fidelity from the invocation, because serialization similar to that used for PowerShell remoting is employed.

Note, however, that this is not an option when calling from outside of PowerShell, such as from cmd.exe or bash.

Read on for an explanation of the behavior you saw in the absence of a script block.


The PowerShell CLI (calling powershell.exe (Windows PowerShell) / pwsh.exe (PowerShell Core) supports only one parameter that accepts a positional argument (i.e., a value not preceded by a parameter name such as -Command).

  • In Windows PowerShell, that (implied) parameter is -Command.

  • In PowerShell Core, it is -File.

    • The default had to be changed to support use of the CLI in Unix shebang lines.

Any arguments after the first positional argument, if any, are considered:

  • in Windows PowerShell: part of the snippet of PowerShell source code passed to the (implied)
    -Command parameter.

  • in PowerShell Core: individual arguments to pass as literals to the script file specified in the first positional argument (the implied -File argument).


Arguments passed to -Command - whether implicitly or explicitly - undergo two rounds of parsing by PowerShell, which can be tricky:

  • In the first round, the "..." (double quotes) enclosing individual arguments are stripped.

    • If you're calling from PowerShell, this even applies to arguments that were originally '...'-enclosed (single-quoted), because, behind the scenes, PowerShell re-quotes such arguments to use "..." when calling external programs (which includes the PowerShell CLI itself).
  • In the second round, the stripped arguments are joined with spaces to form a single string that is then interpreted as PowerShell source code.


Applied to your invocation, this means that both PowerShell .\TestQuotes.ps1 -Config "A B C" and PowerShell .\TestQuotes.ps1 -Config 'A B C' resulted in PowerShell ultimately parsing and executing the following code:

.\TestQuotes.ps1 -Config A B C

That is, due to the 2 rounds of parsing, the original quoting was lost, resulting in three distinct arguments getting passed, which explains your symptom.


If you had to make your command work without a script block, you have two options:

  • Use -File, which only applies one round of parsing:

      powershell.exe -File .\TestQuotes.ps1 -Config "A B C"
    
    • That is, aside from stripping the enclosing "...", the resulting arguments are treated as literals - which, however, is typically what you want.
  • With (implied) -Command, apply an extra layer of quoting:

      powershell.exe -Command .\TestQuotes.ps1 -Config "'A B C'"
    

The outer "..." are stripped during command-line parsing, leaving the inner, single-quoted 'A B C' string as part of the code to be executed.
If you wanted to use " for the inner quoting as well (not necessary here), you'd have to use "\"A B C\"" from outside PowerShell and "\`"A B C\`""[1] from inside PowerShell - that is, PowerShell requires " chars. to be escaped as \" in arguments passed to its CLI, whereas inside PowerShell, `" (or "") must be used.


[1] The \ escaping in addition to ` shouldn't be necessary, but unfortunately is due to a long-standing bug up to at least PowerShell 7.2.x, which may get fixed in 7.3 - see this answer

mklement0
  • 382,024
  • 64
  • 607
  • 775