3

I want to specify a multi-line PowerShell command in a cmd.exe .bat script. I clearly do not yet have the right line continuation and/or quoting. I also tried using a backtick as the line continuation character with similar failure. How can I enter this correctly?

PS C:\src\t> cat .\pd.bat
powershell -NoProfile -Command "Get-ChildItem -Path '../' -Filter '*.doc' | ^
    Select-Object -First 1 | ^
    ForEach-Object { notepad `"$_.FullName`" }"
PS C:\src\t> .\pd.bat

C:\src\t>powershell -NoProfile -Command "Get-ChildItem -Path '../' -Filter '*.doc' | ^
^ : The term '^' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if
a path was included, verify that the path is correct and try again.
At line:1 char:45
+ Get-ChildItem -Path '../' -Filter '*.doc' | ^
+                                             ~
    + CategoryInfo          : ObjectNotFound: (^:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException
lit
  • 14,456
  • 10
  • 65
  • 119
  • 2
    Why use cmd.exe in the first place? Just type the command you want directly from the PowerShell prompt. – Bill_Stewart Jul 20 '17 at 20:49
  • 1
    Especially where the only advantage `cmd.exe` has over PowerShell is memory footprint (several orders of magnitude smaller). If you are going to launch powershell, just use powershell. – David C. Rankin Jul 20 '17 at 21:44
  • 2
    I want to learn what mechanisms are required to put PowerShell into .bat scripts because some people will never let go of the cmd shell. This also goes toward letting people use whatever they are comfortable with. If they see that PowerShell is more powerful and readable, perhaps they will start using more PowerShell commands. This is a long process, not an overnight change. – lit Jul 20 '17 at 23:28

4 Answers4

8

Short of using an intermediate temporary file you can use selective ^-escaping:

powershell -NoProfile -Command Get-ChildItem -Path '../' -Filter '*.doc' ^| ^
Select-Object -First 1 ^| ^
ForEach-Object { notepad "\"$($_.FullName)\"" }

Note that notepad "$_.FullName" wouldn't work in PowerShell, because you need an enclosing $(...) to reference properties inside "...". I've corrected the issue above, but note that you don't need the double quotes at all in this case.

^ must be used:

  • to escape | to prevent cmd.exe from interpreting it up front.

    • Generally, you have to ^-escape all of cmd.exe's metacharacters if you want to pass them through to PowerShell: & | < > %

    • " instances that you want PowerShell to see literally (for them to then have syntactic meaning when interpreted as PowerShell source code) are a special case: you must \-escape them (!) and "..."-enclose the string you want to be recognized as a single argument by PowerShell, so as to preserve embedded whitespace accurately.

    • Note: You can not use "..." quoting around the entire -Command argument, because cmd.exe doesn't support multi-line string literals (^ inside "..." is interpreted literally), and even per-line "..." quoting doesn't work as intended,

  • to escape line endings to tell cmd.exe that the command continues on the next line.

    • Note that the ^ must be the very last character on the line.
    • Also note that a line-ending ^ removes the line break, so the resulting command is a single-line command.

To include actual line breaks in your command - which are required for passing commands to a language such as Python with python -c, as eryksun points out - use ^<newline><newline>, as PetSerAl recommends:

powershell.exe -noprofile -command "\"line 1^

line 2\""

The above yields (based on PowerShell simply echoing (outputting) a quoted string literal submitted as a command):

line 1
line 2

Note: The "\" (and matching \"") is not only required to ultimately pass the string with embedded whitespace preserved correctly here, but also to present a balanced set of " on that line to cmd.exe (irrespective of \-escaping, which cmd.exe doesn't recognize) - without it, the newline-escaping ^ at the end of that line wouldn't be recognized.

PetSerAl also points out that if you're passing what PowerShell should regard a string literal, you can also pass what PowerShell ends up seeing as a single-quoted string ('...'):

powershell.exe -noprofile -command ^"'line 1^

line 2'^"

Here, the ^-escaping of the " instances are for the benefit of cmd.exe so it doesn't mistake the newline-escaping ^ as being inside a double-quoted string.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • I found that it was working without the subexpression in PowerShell because I was not quoting it. `{ notepad $_.FullName }` works, while `{ notepad "$_.FullName" }` fails. Using the subexpression inside the quotes produces the desired result. This seems odd. – lit Jul 20 '17 at 23:22
  • @lit: It may be surprising, but that's how it works in PowerShell - see [this answer](https://stackoverflow.com/a/40445998/45375) of mine. – mklement0 Jul 20 '17 at 23:37
  • I will bookmark this post to use it as duplicate closures. – Gerhard Oct 17 '22 at 12:00
2

I second Bill_Stewart's comment to just do it in the native PowerShell console; no need for cmd.

Should you need to do it in cmd, use quote marks for each line before using ^. And remove the backtick from within the ForEach-Object block.

powershell -NoProfile -Command "Get-ChildItem -Path '../' -Filter '*.doc' |" ^
"Select-Object -First 1 ^| ^
"ForEach-Object { notepad "$_.FullName" }
G42
  • 9,791
  • 2
  • 19
  • 34
1

I usually do the opposite and collapse my command down to 1 line when possible. You can do this by terminating commands with ; just like javascript or c#. In your case, I don't even see a need to use a terminator. You can just roll this into one line:

powershell -NoProfile -Command "Get-ChildItem -Path '../' -Filter '*.doc' |     Select-Object -First 1 | ForEach-Object { notepad `"$_.FullName`" }"

another option would be to save your PowerShell to an external file and call it:

powershell.exe -File MyPSScript.ps1

Ty Savercool
  • 1,132
  • 5
  • 10
1

The pd.bat content must be the following:

powershell -NoProfile -Command Get-ChildItem -Path '../' -Filter '*.doc' ^| ^
    Select-Object -First 1 ^| ^
    ForEach-Object { notepad $_.FullName }
Andrei Odegov
  • 2,925
  • 2
  • 15
  • 21