1

I use the following code in a CMD script file

PowerShell Add-Type -AssemblyName System.Windows.Forms;^
$Line_1 = 'Hello!';^
$Line_2 = 'How are you?';^
[System.Windows.Forms.MessageBox]::Show($Line_1)

The above will show only ($Line_1)
If ($Line_1`n$Line_2) is used, nothing will be shown.

How do I make it show both $Line_1 and $Line_2?

phuclv
  • 37,963
  • 15
  • 156
  • 475
Matthew Wai
  • 962
  • 7
  • 14

3 Answers3

3

The simplest solution is (note the \"$Line_1`n$Line_2\" part):

PowerShell -c Add-Type -AssemblyName System.Windows.Forms; ^
$Line_1 = 'Hello!'; ^
$Line_2 = 'How are you?'; ^
[System.Windows.Forms.MessageBox]::Show(\"$Line_1`n$Line_2\")

Note that I've explicitly added the -c (-Command) parameter name to signal that a PowerShell command string is being passed. While that isn't necessary in Windows PowerShell, which defaults to -Command, it is in PowerShell (Core) 7+, where -File is now the default - see the CLI documentation for Windows PowerShell and PowerShell (Core) 7+.

That is, you must use $Line_1`n$Line_2 inside "...", an expandable string, and you must \-escape the " characters so that PowerShell doesn't strip them as part of its command-line parsing (in the absence of overall double-quoting, """ works too).


Unfortunately, the parsing rules change when for /f is used in order to process PowerShell's output line by line and/or capture it in a variable:

Note: The following uses [Console]::WriteLine() so as to produce console output, just for the sake of using similar syntax to the [System.Windows.Forms.MessageBox]::Show() method call while allowing something to be captured by for /f. In real life there is no good reason to call [Console]::WriteLine().

for /f "delims=" %%l in ('

  PowerShell -c Add-Type -AssemblyName System.Windows.Forms^; ^
  $Line_1 ^= 'Hello!'^; ^
  $Line_2 ^= 'How are you?'^; ^
  [Console]::WriteLine^(\"$Line_1`n$Line_2\"^)

') do echo [%%l]
  • = , ; ( ) must additionally be escaped (outside what cmd.exe sees as a "..." string).

  • If you additionally enclose a \"...\" string in "..." to prevent whitespace normalization (see next section) you must ^-escaped the enclosing (outer) "; e.g.,
    ^"\"Marley & Me\"^"

  • The line continuations (^ at the end of command-interior lines) are actually optional inside for /f, but they were included for consistency.


Summary of quoting and escaping requirements:

  • Your multi-line technique with line continuations (^ at the end of lines) - which syntactically cannot use "..." quoting around the entire command, because cmd.exe doesn't support double-quoted multi-line strings - requires careful ^-escaping of all cmd.exe metacharacters that should be passed through to PowerShell, notably & | < > ^, and, additionally, if PowerShell is called from inside a for /f statement, , ; = ( ) - unless these characters happen to be part of a substring that cmd.exe sees as double-quoted; e.g., a & placed inside the \"...\" string - e.g. \"$Line_1`n & $Line_2\" - must not be ^-escaped (but see below re whitespace normalization).

    • As an exception, metacharacter % must always be escaped as %% (which only works in batch files, not at the command prompt - see this answer).

    • Additionally, if setlocal enabledelayedexpansion is in effect (or cmd.exe was started with /V:ON), ! must be escaped too, but inexplicably as follows:

      • As ^^! (sic) outside of what cmd.exe sees as a "..." string (where the other metacharacter require just one ^)
      • As ^! inside such a string (where the other metacharacters require no escaping).
    • When calling via for /f, line continuations are optional - that is, you may omit the ^ at the end of the command-interior lines.

  • Each statement must be ;-terminated (as you have done), because the line-continuation (^) results in no newline between the input lines, so PowerShell sees them as a single line on which multiple statements must be ;-separated.

  • Because neither cmd.exe nor PowerShell's initial command-line parsing knows about single-quoted strings ('...') and because the escaped " characters in \"...\" strings have no syntactic function during command-line parsing, such strings are broken into multiple arguments if they contain whitespace:

    • In effect, runs of multiple adjacent spaces inside such strings are normalized to a single space each. E.g., 'How are you?' and \"How are you?\" are ultimately seen as 'How are you?'and "How are you?" by PowerShell.

    • To avoid that, additionally enclose such strings in "..." :

      • Single-quoted PowerShell strings: "'How are you?'"
        • Note: This is not necessary if the entire command is enclosed in "..."
      • Double-quoted PowerShell strings: ^"\"How are you?\"^".
        • The ^-escaped enclosing " chars. ensure that cmd.exe still sees what is between the inner \"...\" as double-quoted (because it doesn't recognize \ as an escape char.), obviating the need to ^-escape cmd.exe metacharacters there.
        • Note: If the entire command is enclosed in "...", a different approach is required: use "... "^""How are you?"^"" ..." with powershell.exe (Windows PowerShell), and "... ""How are you?"" ..." with pwsh.exe (PowerShell (Core) 7+).
  • In case you want to include comments in the PowerShell code, you must use the form
    ^<# ... #^> - i.e. (escaped) inline comments - normal single-line comments (# ....) are not supported (because it would require a newline to terminate them, but there are no newlines between statements in the invocation method at hand).


How PowerShell parses the arguments passed to its CLI's -Command / -c parameter:

PowerShell gives you the choice between passing the command string

  • either: as a single argument, enclosed in overall "..."; e.g.:

    • powershell -c "Get-Date -Format 'yyyy MMM'"
  • or: multiple arguments - possibly individually "..."-quoted - which PowerShell then joins to form a single string; e.g.:

    • powershell -c Get-Date -Format "'yyyy MMM'"

In both cases, unescaped " characters are stripped from the argument(s), as they are assumed to have merely syntactic function for the sake of the command line rather than the resulting PowerShell command.

After stripping syntactic " characters and joining the resulting arguments with spaces, if applicable, Powershell interprets the resulting string as PowerShell code.

Note: This differs fundamentally from how arguments are parsed when you use the -File CLI parameter to invoke a script file and pass arguments to it - see this answer for more.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • You are right. Escaping characters is the simplest solution. Slash marks escaped me. I usually use single quotation marks. – Matthew Wai Feb 07 '21 at 11:12
  • @MatthewWai, with _single_-quoted strings (`'...'`) you won't get string interpolation, which is what is needed here. Note that `"""` instead of `\"` would have worked in this case too. – mklement0 Feb 07 '21 at 14:43
  • If multiple adjacent spaces are needed, I will use **`$Line_2 = '"Why so?"';^`** – Matthew Wai Feb 08 '21 at 02:59
  • @MatthewWai, I would reverse the quotes, for conceptual clarity: `$Line_2 = "'Why so?'"` - that way, with `"` quotes getting stripped, it is more obvious that PowerShell will see a `'...'` (verbatim) string rather than a `"..."` (interpolating) string. – mklement0 Feb 08 '21 at 03:31
  • @MatthewWai, I've updated the answer to generalize the information about passing strings with protection against whitespace normalization, and in doing so I've stumbled on the fact that - unfortunately - different parsing rules apply when PowerShell is called via `for /f` - please see my update. – mklement0 Feb 08 '21 at 19:40
2

Obviously $Line_1 + "`n" + $Line_2 and "$Line_1`n$Line_2" works normally. It's just tricky to send the command string to PowerShell from cmd with its legacy quirks because:

  • In cmd () are special characters in various places denoting a block
  • The token delimiter is not only <space> and <tab> but also ; , = <0x0B> <0x0C> and <0xFF>. This changes tokenization behavior of cmd, but the command being called may reparse the command using its rules one more time

According to the documentation PowerShell expects the command in a single string in the last parameter (which isn't quite true since the document wasn't updated correctly), so you need to quote the whole thing or escape all the delimiters. The easiest solution is to use a single line and escape the quotes in "`n" like this

PowerShell "Add-Type -AssemblyName System.Windows.Forms; $Line_1 = 'Hello!'; $Line_2 = 'How are you?'; [System.Windows.Forms.MessageBox]::Show($Line_1 + \"`n\" + $Line_2)"

If you want to put the commands in multiple lines then you can't quote the string. To put the whole thing as a single argument now you need to escape all the spaces (somehow you don't need to escape ; in this case, possibly because after the command line is passed to PowerShell, it calls GetCommandLineW and parses again the whole thing itself)

PowerShell Add-Type^ -AssemblyName^ System.Windows.Forms;^
$Line_1^ =^ 'Hello!';^
$Line_2^ =^ 'How^ are^ you?';^
[Windows.Forms.MessageBox]::Show($Line_1^ +^ \"`n\"^ +^ $Line_2)"

Alternatively you can avoid that "`n" string by getting the new line directly with [char]10

PowerShell -Command Add-Type -AssemblyName System.Windows.Forms;^
$Line_1 = 'Hello!';^
$Line_2 = 'How are you?';^
[System.Windows.Forms.MessageBox]::Show($Line_1 + [char]10 + $Line_2)

Finally a solution that works without any escaping which utilizes the EncodedCommand option of PowerShell which receives the base64 encoded string of a UTF-16 command string. You can get the encoded version by running this in PowerShell

$str = @'
>> Add-Type -AssemblyName System.Windows.Forms;
>> $Line_1 = 'Hello!';
>> $Line_2 = 'How are you?';
>> [System.Windows.Forms.MessageBox]::Show($Line_1 + "`n" + $Line_2)
>> '@
PS C:\Users> [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($str))
QQBkAGQALQBUAHkAcABlACAALQBBAHMAcwBlAG0AYgBsAHkATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwA7AAoAIAA9ACAAJwBIAGUAbABsAG8AIQAnADsACgAgAD0AIAAnAEgAbwB3ACAAYQByAGUAIAB5AG8AdQA/ACcAOwAKAFsAUwB5AHMAdABlAG0ALgBXAGkAbgBkAG8AdwBzAC4ARgBvAHIAbQBzAC4ATQBlAHMAcwBhAGcAZQBCAG8AeABdADoAOgBTAGgAbwB3ACgAIAArACAAIgAKACIAIAArACAAKQA=

After having the encoded version you can call this from cmd

PowerShell -EncodedCommand QQBkAGQALQBUAHkAcABlACAALQBBAHMAcwBlAG0AYgBsAHkATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwA7AAoAJABMAGkAbgBlAF8AMQAgAD0AIAAnAEgAZQBsAGwAbwAhACcAOwAKACQATABpAG4AZQBfADIAIAA9ACAAJwBIAG8AdwAgAGEAcgBlACAAeQBvAHUAPwAnADsACgBbAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwAuAE0AZQBzAHMAYQBnAGUAQgBvAHgAXQA6ADoAUwBoAG8AdwAoACQATABpAG4AZQBfADEAIAArACAAIgBgAG4AIgAgACsAIAAkAEwAaQBuAGUAXwAyACkA
phuclv
  • 37,963
  • 15
  • 156
  • 475
  • Nicely done; as for how PowerShell parses its command line with (implied) `-Command` / `-c` and _multiple_ arguments: it strips enclosing syntactic `"..."` quoting from each, if present, and concatenates them with a single space as a separator. The resulting string is then interpreted as a PowerShell command. You can see this in action with `powershell -nop -c "'[" "a" "]'"`, which PowerShell ends up seeing and executing as `'[ a ]'` – mklement0 Feb 07 '21 at 07:47
  • @mklement0 that was also my thought but I couldn't confirm with `powershell -help`, unlike `cmd /?` where it clearly states that the outer quotes are usually stripped off – phuclv Feb 07 '21 at 11:55
  • Yes, unfortunately the [PowerShell documentation of the `-Command` CLI parameter](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pwsh?view=powershell-7.2#-command---c) is lacking here. While it is generally improving, especially since [going open-source](https://github.com/MicrosoftDocs/PowerShell-Docs/issues), there are still cases where experimentation / reading the source code is needed. – mklement0 Feb 07 '21 at 14:53
  • P.S.: `; , =` are only special (act as argument separators) when you call _batch files_, and, similarly, you're free to use `( )` in arguments (even for batch files). I suggest correcting the claim that "PowerShell expects the command in a _single string_ in the last parameter", which isn't true (see my first comment). – mklement0 Feb 07 '21 at 22:57
  • 1
    It turns out that, even when calling something _other_ than a batch file, characters `; , = ( )` can _situationally_ be special and then require `^`-escaping, namely if the call happens _inside a `for /f` statement_. Gotta love `cmd.exe`. – mklement0 Feb 08 '21 at 19:55
0

There are multiple ways to concatenate strings which you can look up, but probably the easiest way is to use the + symbol. `n is a newline character which will place Line 2 below line 1. Note that ` is a backtick symbol (usually on same key as the tilde ~)

[System.Windows.Forms.MessageBox]::Show($Line_1 + "`n" + $Line_2)

After looking again at your post I noticed you were close with ($Line_1`n$Line_2). You're only missing some double-quotes

[System.Windows.Forms.MessageBox]::Show("$Line_1`n$Line_2")

Powershell is happy to replace variables with their values when placed inside double-quotes. You can read more about expandable strings here and here

Daniel
  • 4,792
  • 2
  • 7
  • 20
  • **```($Line_1 + "`n" + $Line_2)```** does not work. Nothing is shown. – Matthew Wai Feb 07 '21 at 04:53
  • **```("$Line_1`n$Line_2")```** does not work either. Nothing is shown. – Matthew Wai Feb 07 '21 at 04:57
  • At the end of each line I just noticed you have ^ characters. Those should not be there. Also, ending each statement with a semi-colon is optional if you are adding a line break. Also, remove powershell from the first line. – Daniel Feb 07 '21 at 04:58
  • 1
    @Daniel the OP is calling powershell from cmd so `^` must be used to escape new lines – phuclv Feb 07 '21 at 05:00
  • Ah, did not realize that. Thank you @phuclv. I don't usually see powershell script being run directly from cmd prompt. That explains somethings about the code :) – Daniel Feb 07 '21 at 05:02
  • The problem is that **`n** does not work in a CMD script. The backtick could be the culprit. – Matthew Wai Feb 07 '21 at 05:04
  • Double-quotes also do not work. Cannot figure out how to send a newline character. I can at least concat 2 strings `POWERSHELL 'this' + ' and that'` but stuck on what to do for newline. – Daniel Feb 07 '21 at 05:14
  • All joke aside, are we just doing this for fun or is there a real need to be sending powershell code directly from cmd prompt. Could you instead maybe write real powershell code inside a ps1 file and call the script file from cmd instead? – Daniel Feb 07 '21 at 05:17
  • I often use PowerShell codes in a CMD script, but I am stuck in the said problem. I want to combine CMD and PowerShell into a single script. – Matthew Wai Feb 07 '21 at 06:11
  • @MatthewWai the backtick has nothing to do with this because it's just a normal character to cmd. Only `()` and `;` are special in this case. Anyway if you often use PowerShell it's better to just use PowerShell entirely to avoid all the legacy things of cmd – phuclv Feb 07 '21 at 06:11