I want to forward %*
to a powershell script from a Batch script, which already sort of works. The only problem is that I can't forward arguments with spaces cleanly.
Example.bat..
@echo off
set cwd=%~dp0
set argv=%*
powershell -NoProfile -ExecutionPolicy Bypass -Command "& '%cwd%Example.ps1' %argv%"
Example.ps1..
Param(
$Message
)
If (!$Message)
{
$Message = "Hello!!!"
}
Write-Host $Message
I can do ./Example.bat Oh
just fine, which prints Oh
. However, ./Example.bat "Oh no"
only prints Oh
as well. ./Example "'Oh no'"
works but is not clean, as then the usage of the scripts isn't identical anymore, which means any use of it becomes tightly coupled, which I don't want.
Fabrice SANGA's helpful answer explains the need to escape the "
chars. embedded in the value of %*
well and offers a solution that will typically work, using \"
escaping.
However, it isn't robust, because it breaks cmd.exe
's own parsing with "..."
-enclosed arguments that contain cmd.exe
metacharacters such as &
or |
, e.g. "you & I"
Update: The linked answer has since been updated with an alternative solution that attempts to provide a robust solution, but the one below is simpler.
A robust solution requires a different - obscure - form of escaping embedded "
chars., namely as "^""
[sic] rather than as \"
powershell.exe
, the Windows PowerShell CLI; with pwsh.exe
, the PowerShell (Core) 7 CLI, use ""
Here's a robust, streamlined solution that also uses this form of escaping to enclose the target *.ps1
path, so as to also handle paths with embedded '
chars. correctly.
Also note the use of %~dpn0.ps1
, which obviates the need to repeat the base file name, Example
:
@echo off & setlocal
:: Escape any " chars. embedded in %* as "^""
set argv=%*
if defined argv set argv=%argv:"="^""%
powershell -NoProfile -ExecutionPolicy Bypass -Command "& "^""%~dpn0.ps1"^"" %argv%"
setlocal
is used to ensure that the variables defined in the batch file are scoped to that file; by default, they would be defined session-globally.
Tip of the hat to João Mac's answer for the reminder of the if defined somevar
technique as a variable emptiness/non-existence test, which is simpler than if not %somevar%.==.
Note: More work is needed if you additionally need to guard against `
and $
chars. in the script path and / or arguments getting interpreted by PowerShell (as the escape character and as the start of a variable reference such as $HOME
or a subexpression such as $(1 + 2)
, respectively):
@echo off & setlocal
:: Escape any " chars. embedded in %* as "^"",
:: ` as ``, and $ as `$
set argv=%*
if defined argv set argv=%argv:"="^""%
if defined argv set argv=%argv:`=``%
if defined argv set argv=%argv:$=`$%
:: Do the same to the script file path,
:: but defer enclosing the path in "^""..."^"" until later.
set "ps1path=%~dpn0.ps1"
set "ps1path=%ps1path:`=``%"
set "ps1path=%ps1path:$=`$%"
powershell -NoProfile -ExecutionPolicy Bypass -Command "& "^""%ps1path%"^"" %argv%"
However, you should then consider using the -File
CLI parameter instead, which greatly simplifies the approach - see the next section.
Taking a step back:
If your intent is solely to call a *.ps1
file with arguments, it is better to use the -File
(-f
) CLI parameter rather than -Command
(-c
)
Doing so has two advantages:
It obviates the need for escaping and therefore greatly simplifies your batch file (see below).
It protects your script-file path and any arguments from potentially unwanted interpretation by PowerShell: e.g., with -File
, passing verbatim $HOME
as an argument is passed through as-is, whereas with -Command
it would be expanded as PowerShell's automatic $HOME
variable.
@echo off & setlocal
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dpn0.ps1" %*
For detailed guidance on when to use -File
vs. -Command
, see this answer.