13

From my understanding, the invoke operator (&) and the Invoke-Expression cmdlet should behave similar. However, as can be seen below, this is not the case:

PS C:\Users\admin> powershell -Command "& {""([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('ZWNobyAnaGVsb
G93b3JsZCc=')))""}"
echo 'helloworld'

PS C:\Users\admin> powershell -Command "IEX ""([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('ZWNobyAnaGVs
bG93b3JsZCc=')))"""
helloworld

Here, 'ZWNobyAnaGVsbG93b3JsZCc=' is the Base64 encoded string "echo helloworld".

Can someone clarify?

Shuzheng
  • 11,288
  • 20
  • 88
  • 186
  • 2
    Because you don't supply the same input - the `IEX` arg is not wrapped in an outer scriptblock (`{}`), so it goes one step further and actually *executes* "echo helloworld" after the parser is done expanding the string – Mathias R. Jessen Apr 25 '18 at 10:21
  • @MathiasR.Jessen - Will you show how the IEX example is evaluated (each step)? What executes "echo helloworld"? – Shuzheng Apr 25 '18 at 11:09

1 Answers1

25

Invoke-Expression (whose built-in alias is iex) and &, the call operator, serve different purposes:

  • Invoke-Expression evaluates a given string as PowerShell source code, as if you had executed the string's content directly as a command.

    • As such, it is similar to eval in bash and therefore only to be used with input that is fully under the caller's control or input that the caller trusts.

    • There are often better solutions available, so Invoke-Expression should generally be avoided

  • & is used to invoke a command (& <nameOrPath> [...]) or a script block (& { ... } [...]):

    • Neither case involves evaluating a string as source code.

In the case at hand:

The core of your command is the following expression, which returns the string
"echo 'helloworld'" (its content doesn't include the enclosing " - this is simply the representation of the resulting string as a PowerShell string literal):

[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('ZWNobyAnaGVsbG93b3JsZCc='))

Also note that, due to how the command line is parsed, the ""..."" surrounding the core expression in your original commands are effectively ignored, which explains why the expression is executed rather than being treated as the content of a string.[1]

Therefore, your two commands amount to:

  • & { "echo 'helloworld'" }

    • & executes the statement inside the script block, which happens to be a string, and a string by itself - if it isn't assigned to a variable or redirected elsewhere - is simply output as-is.
      In this case, the command is effectively the same as just executing "echo 'helloworld'" by itself (including the enclosing ", which you can think of as
      echo "echo 'helloworld'"), so echo 'helloworld' prints to the console.

    • Note that echo is a built-in alias for the Write-Output cmdlet, whose explicit use is rarely necessary: Return values from commands or expressions are implicitly output, if they are not captured in some form, as in this case, where executing a string by itself as a statement simply outputs the string. (You can try this by submitting just 'hi' at the prompt, for instance).

  • iex "echo 'helloworld'"

    • This makes iex (Invoke-Expression) evaluate the string's content as source code, which therefore executes echo 'helloworld', which prints helloworld to the console.

[1] Optional reading: PowerShell quoting woes when calling external programs

Note:

  • Handling of quoting with respect to external programs or when calling from an external programs is not part of the official documentation, as far as I can tell (as of this writing, neither about_Parsing nor about_Quoting_Rules nor about_Special_Characters mentions it - I've opened this issue on GitHub to address that).

  • There are flaws in the existing handling, but they cannot be fixed without breaking backward compatibility.

  • When calling from PowerShell, the best approach is to use a script block, which bypasses the quoting problems - see below.

Even though you correctly embedded " by escaping them as "" inside the overall "..." string from a PowerShell-internal perspective, additional escaping of " with \ is needed in order to pass them through to an external program, even if that external program is another instance of PowerShell called via powershell.exe.

Take this simplified example:

powershell.exe -command " ""hi"" "  # !! BROKEN
powershell.exe -command ' "hi" '    # !! BROKEN
  • PowerShell-internally, " ""hi"" " and ' "hi" ' evaluate to a string with literal contents  "hi" , which, when executed, prints hi.

  • Regrettably, PowerShell passes this string to powershell.exe as " "hi" " - note how the "" turned into plain " and the enclosing single quotes were replaced with double quotes - which effectively results in  hi  after parsing by the new instance (because " "hi" " is parsed as the concatenation of substrings " ", hi, and " "), so PowerShell ends up trying to execute a (presumably nonexistent) command named hi.

By contrast, if you manage to pass the embedded as " as \" (sic) - after meeting PowerShell's own escaping needs - the command works as intended. Therefore, as stated, you need to combine PowerShell-internal escaping with for-the-CLI escaping in order to pass an embedded ", so that:

  • inside overall "...", each embedded " must be escaped as \"" (sic) or \`" (sic)
  • inside overall '...', \" can be used as-is.
powershell.exe -command " \""hi\"" " # OK
powershell.exe -command " \`"hi\`" " # OK
powershell.exe -command ' \"hi\" '   # OK

Alternatively, use a script block instead of a command string, which bypasses the quoting headaches:

powershell.exe -command { "hi" } # OK, but only works when calling from PS

Note that the script-block technique only works when calling from PowerShell, not from cmd.exe.


cmd.exe has its own quoting requirements: Notably, cmd.exe only supports "" for embedding double quotes (not also `"); thus, among the solutions above, only
powershell.exe -command " \""hi\"" " works from cmd.exe (a batch file) without additional escaping.

The down-side of \"", however, is that runs of interior whitespace between \""...\"" are collapsed to a single space each. To avoid that, use \"...\", but cmd.exe then sees substrings between the \" instances as unquoted, which would cause the command to break if that substring contained metacharacters such as | or &; e.g., powershell.exe -command " \"a|b\" "; to fix that you must individually ^-escape the following characters: & | < > ^

powershell.exe -command ' "hi" ' is similarly brittle, because cmd.exe doesn't recognize ' as a string delimiter, so any metacharacters outside embedded "..." are again interpreted by cmd.exe itself; e.g., powershell.exe -command ' "hi" | Measure-Object '

Finally, using just "" from cmd.exe for embedding " sometimes works, but not reliably; e.g., powershell.exe -command " 'Nat ""King"" Cole' " prints Nat "King Cole (the closing " is missing).
This appears to have been fixed in PowerShell Core.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thank you for an excellent answer. I'm very interested in what you wrote: `"Also note that, due to how the command line is parsed, the ""..."" surrounding the core expression in your original commands are effectively ignored..."`. Can you provide a link to documentation? I've read that `""` means a single `"` when used inside `"..."`. Therefore, I'd expect that the core-expression is quoted, i.e., seen as a string. – Shuzheng Apr 26 '18 at 12:46
  • 1
    @user111854 *I've read that `""` means a single `"` when used inside `"..."`.* That is true. But then you pass that string to `PowerShell.exe` by command line. And command line parser treat `"` as special character, thus it need to be escaped to be literal part of command. – user4003407 Apr 26 '18 at 12:56
  • @user111854: PetSerAl is correct, but please see my update for a more detailed explanation. – mklement0 Apr 26 '18 at 13:38
  • Last question, why use `\""` and not `\"\"`? The \ only escapes the first `"`, so the second still has special meaning? – Shuzheng Apr 27 '18 at 09:06
  • @user111854: The `""` is required for _PowerShell_ to recognize an `"` embedded in `"..."`, whereas the `\ ` - which has _no_ special meaning to PowerShell internally - is required for _command-line_ construction. After PowerShell's internal parsing, it is `\"` (_one_ `"` only) that `powershell.exe` sees on its command line. By contrast, `\"\"` would cause a syntax error, because by PowerShell rules the `"` chars. aren't escaped. – mklement0 Apr 27 '18 at 11:53
  • @mklement0 - But, you say that PowerShell parses `\"` as `"` for command-line construction? Then `\"\"` would be parsed as `""`, which is exactly what we want? Or do you mean that `\""` is parsed as `\"`, which is equivalent to `""` when constructing a command-line, i.e., `powershell.exe -C "\""...\"""` is executed as `powershell.exe -C """..."""` (if that worked properly)? – Shuzheng Apr 28 '18 at 10:44
  • @user111854: Yes, `\""` ends up as `\"` when `powershell.exe` sees it on its command line, and only on the command line does it expect that form of escaping. Yes, it is the equivalent of `powershell.exe -C """..."""` _if_ that worked; it actually _mostly_ works when you call _from `cmd.exe`_, but not in _all_ cases. I've also revised the answer a bit. – mklement0 Apr 28 '18 at 12:15
  • 1
    Good idea, @Blaisem - thanks; answer updated. – mklement0 Aug 19 '23 at 19:03