Start-Process with PowerShell.exe exhibits different behavior with embedded single quotes and double quotes
Asked Answered
P

3

3

First, in case anyone wonders why we're invoking PowerShell in this way, I ran into this behavior with a more complex command we were building, but the behavior can be exhibited using a more simple example as shown below. In practice, we are running a command under 32-bit PowerShell as admin with additional variables rendered in the string (hence why I don't simply use single-quotes for the outer portion), but that doesn't seem to factor into the behavior below.


When I invoke PowerShell through Start-Process, I get some odd behaviors if I use single quotes surrounding the -Command parameter to the PowerShell executable. For example:

Start-Process -FilePath Powershell.exe -ArgumentList "-Command 'ping google.com'"

just renders ping google.com as the output and exits. However, if I use nested double-quotes instead of the single-quotes as follows:

Start-Process -FilePath Powershell.exe -ArgumentList "-Command `"ping google.com`""

ping runs and produces the expected output:

Pinging google.com [173.194.78.113] with 32 bytes of data:

Reply from 173.194.78.113: bytes=32 time=34ms TTL=45

Reply from 173.194.78.113: bytes=32 time=33ms TTL=45

Reply from 173.194.78.113: bytes=32 time=35ms TTL=45

Reply from 173.194.78.113: bytes=32 time=32ms TTL=45

Ping statistics for 173.194.78.113:

Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),

Approximate round trip times in milli-seconds:

Minimum = 32ms, Maximum = 35ms, Average = 33ms

Why does the command string simply render as-is instead of execute if I use the single-quotes for the -Command parameter instead of double-quotes?

Philippic answered 8/4, 2020 at 20:4 Comment(0)
B
2

js2010's helpful answer is correct in that the use of Start-Process is incidental to your question and that the behavior is specific to PowerShell's CLI (powershell.exe for Windows PowerShell, and pwsh.exe for PowerShell (Core) 7+):

On Windows[1], there are two layers of evaluation to consider:

  • (a) The initial parsing of the command line into arguments.

  • (b) The subsequent evaluation of the resulting arguments following the
    -Command (-c) CLI parameter as PowerShell code
    .

    • Note: In Windows PowerShell CLI calls, -Command is implied if neither -Command nor -File are specified; in PowerShell (Core), -File is the default.

Re (a):

As most console programs on Windows do, PowerShell recognizes only " chars. (double quotes) - not also ' (single quotes) - as having syntactic function.[2]

  • That is, unless " chars. are escaped as \", they are treated as command-line argument delimiters with purely syntactic function, which are therefore removed during parsing.

    • A quick example: powershell -c "hi!" makes PowerShell strip the " and try to execute hi! as a command; by contrast, powershell -c \"hi!\" escapes and therefore retains the " as part of the command, so that PowerShell executes "hi!", i.e. a string literal that is echoed as such.

    • Note: If you're calling from cmd.exe, \" escape sequences can situationally break the invocation; for robust workarounds see this answer.

  • As implied by the above, ' chars. are not removed.

Re (b)

Whatever arguments are the result - i.e. an array of possibly "-stripped tokens - are concatenated with a single space between them, and the resulting (single) string is then interpreted as PowerShell code.


To explain this in the context of your example, with Start-Process taken out of the picture:

Note: The following applies to calling from cmd.exe or any context where no shell is involved (including Start-Process and the Windows Run (WinKey-R) dialog). By contrast, PowerShell re-quotes the command line behind the scenes to always use ", if needed.
To put it differently: the following applies to command lines as seen by PowerShell.

Single-quoted command:

# Note: This *would* work for calling ping if run from 
#       (a) PowerShell itself or (b) from a POSIX-like shell such as Bash.
#       However, via cmd.exe or any context where *no* shell is involved,
#       notably Start-Process and the Windows Run dialog, it does not.
powershell -Command 'ping google.com'
  • (a) results in PowerShell finding the following two verbatim arguments: 'ping (sic) and google.com' (sic).

  • (b) concatenates these verbatim arguments to form 'ping google.com'[2] and executes that as PowerShell code, therefore outputs the content of this string literal, ping google.com

Double-quoted command:

powershell -Command "ping google.com"
  • (a) results in PowerShell stripping the syntactic " characters, finding the following, single verbatim argument: ping google.com

  • (b) then results in this verbatim argument - ping google.com - being executed as PowerShell code, which therefore results in a command invocation, namely of the ping executable with argument google.com.


[1] On Unix-like platforms, the first layer doesn't apply, because programs being invoked only ever see an array of verbatim arguments, not a command line that they themselves must parse into arguments. Not that if you call the PowerShell CLI from a POSIX-like shell such as bash on Unix-like platforms, it is that shell that recognizes single-quotes as string delimiters, and strips them before PowerShell sees them.

[2] Surprisingly, on Windows it is ultimately up to each individual program to interpret the command line, and some do choose to also recognize ' as string delimiters (e.g., Ruby). However, many programs on Windows - including PowerShell itself - are based on the C runtime, which only recognizes ".

[3] As an aside, note that this implies that whitespace normalization is taking place: that is,
powershell -Command 'ping google.com' would equally result in 'ping google.com'.

Benedict answered 9/4, 2020 at 15:10 Comment(4)
Thanks for the thorough explanation. I didn't realize the single-quote behavior of command prompt carried over to PowerShell, or rather, that it is how command invocation treats single quoted strings in Windows itself, and affects most entrypoints - I always assumed this was only an annoying quirk of cmd.Philippic
Actually my examples are run within powershell itself. Doublequotes inside singlequotes seem to have a magic property that runs the command, while singlequotes inside doublequotes treat it as a string to echo.Squeegee
Glad to hear it was helpful, @BendertheGreatest. It's actually not Windows - it's up to each program to parse the raw command line, but many programs are built on the C runtime, which only recognizes " - please see footnote [2] that I just added.Benedict
@js2010, please see my clarification re calling from cmd.exe or from a context when no shell is involved. PowerShell muddles the picture by performing re-quoting with " behind the scenes. On non-Windows platforms, calling from a POSIX-like shell such as bash, it is that shell that recognizes single-quoting. Best to keep an additional shell (except for cmd.exe) out of the picture, to avoid confusion.Benedict
S
0

We can drop start-process from this. It seems true that embedded double and single quotes are treated differently by the powershell executable. Start-process is not relevant to this behavior. This is the way powershell behaves on the command line. I don't see a way to change it. You can also do "start-job -runas32", but it wouldn't be elevated. Another option is powershell's "-file" option instead of "-command". These examples are run from within Osx (unix) powershell core:

pwsh -c "'ping -c 1 google.com'"

ping -c 1 google.com


pwsh -c "`"ping -c 1 google.com`""

PING google.com (172.217.10.110): 56 data bytes
64 bytes from 172.217.10.110: icmp_seq=0 ttl=52 time=20.020 ms

--- google.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 20.020/20.020/20.020/0.000 ms


pwsh -c '"ping -c 1 google.com"'        
PING google.com (172.217.6.206): 56 data bytes
64 bytes from 172.217.6.206: icmp_seq=0 ttl=52 time=22.786 ms

--- google.com ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 22.786/22.786/22.786/0.000 ms

EDIT:

trying this without a shell, so using Task Scheduler (Schedule service), and powershell as the program with these arguments. Only a single set of doublequotes around the commandline seem to work, or no quotes at all. In one case the backquote gets added to the hostname. With -command you can also add > log 2>&1 to capture the output and errors to a text file. Hmm pwd;get-date only logs get-date. ($(pwd | fl; get-date | fl) > log or $(pwd; get-date) | set-content log)

-noexit -Command "'ping google.com'"

ping google.com


-noexit -Command "`"ping google.com`""

Ping request could not find host google.com`. Please check the name and try again.


-noexit -Command '"ping google.com"'

ping google.com


-noexit -Command "ping google.com"

Pinging google.com [2607:f8b0:4006:80c::200e] with 32 bytes of data:
Reply from 2607:f8b0:4006:80c::200e: time=13ms
Reply from 2607:f8b0:4006:80c::200e: time=15ms
Reply from 2607:f8b0:4006:80c::200e: time=14ms
Reply from 2607:f8b0:4006:80c::200e: time=12ms

Ping statistics for 2607:f8b0:4006:80c::200e:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 12ms, Maximum = 15ms, Average = 13ms


-noexit -Command 'ping google.com'

ping google.com


-noexit -Command ping google.com

Pinging google.com [2607:f8b0:4006:80c::200e] with 32 bytes of data:
Reply from 2607:f8b0:4006:80c::200e: time=13ms
Reply from 2607:f8b0:4006:80c::200e: time=13ms
Reply from 2607:f8b0:4006:80c::200e: time=13ms
Reply from 2607:f8b0:4006:80c::200e: time=13ms

Ping statistics for 2607:f8b0:4006:80c::200e:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 13ms, Maximum = 13ms, Average = 13ms
Squeegee answered 8/4, 2020 at 21:21 Comment(2)
So, read my first paragraph. This is a simplified example but in reality we need to run 32-bit PowerShell elevated for this, so we are invoking the SysWOW64 variant of PowerShell, using the -Verb RunAs argument of Start-Process. Otherwise I agree that using Start-Process would be redundant.Philippic
To address your update, I've already established that there is a difference with the single quotes vs. the double quotes through the powershell entrypoint. But I'm looking for a more concrete answer other than "that's just the way it is". The behavior seems wrong considering that other than the few nuances between double quoted strings and single quoted strings in PowerShell, both are strings so I would expect either variant to execute regardless of whether it's a string or string literal.Philippic
D
-1

The help for powershell.exe mentions nothing about supporting single-quotes... why would you be using them?

-Command
    Executes the specified commands (and any parameters) as though they were
    typed at the Windows PowerShell command prompt, and then exits, unless
    NoExit is specified. The value of Command can be "-", a string. or a
    script block.

    If the value of Command is "-", the command text is read from standard
    input.

    If the value of Command is a script block, the script block must be enclosed
    in braces ({}). You can specify a script block only when running PowerShell.exe
    in Windows PowerShell. The results of the script block are returned to the
    parent shell as deserialized XML objects, not live objects.

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

    To write a string that runs a Windows PowerShell command, use the format:
        "& {<command>}"
    where the quotation marks indicate a string and the invoke operator (&)
    causes the command to be executed.
Discombobulate answered 8/4, 2020 at 20:11 Comment(4)
It doesn't say it doesn't support them either: The value of Command can be "-", a string. or a script block. Both single and double quotes produce strings, and as long as I'm not expecting variables to be rendered inside the single-quotes at execution time, or characters to be escaped, there shouldn't be a behavioral difference here as documented (obviously in practice there is something different which is what I'm asking about).Philippic
yeah I can't answer that for you. But due to the fact that single quotes are always interpreted as literals, that seems logical that it would be a natural exclusionary case. If you don't like back-ticking, I'm pretty sure doubling the double-quotes works too "" ""Discombobulate
I agree that single quotes are interpreted as literal strings.... but if -Command is expected to execute the string, I would expect the string to render as a literal string and then execute. It doesn't seem like a natural behavior to me.Philippic
I've always been baffled by PowerShell's quirks with executing 3rd party binaries from command-line. &{}, '', .\, there seems to be some very inconsistent behavior or at least, no clear guidance for passing parameters to other executables--especially when they require some level of quotations themselves...Discombobulate

© 2022 - 2024 — McMap. All rights reserved.