tl;dr:
Unless you specifically need the batch file to run in a new window, avoid Start-Process
(whose built-in aliases are start
and saps
), and invoke the batch file directly.
To avoid problems with special characters in $unsecurePassword
, do not pass it as an argument, pass it via stdin (the standard input stream), which your batch file will pass through to your python
script:
automation.ps1
:
# ...
$script = "<path>\batch_script.bat"
# Pass the password via *stdin*
$unsecurePassword | & $script 'userName'
Note: It is the $OutputEncoding
preference variable that controls what character encoding PowerShell uses for sending text to an external program's stdin. In Windows PowerShell, that variable defaults to ASCII(!) encoding, meaning that any characters outside the 7-bit ASCII-range of Unicode characters, such as accented characters, are unsupported (they turn to literal ?
); fortunately, PowerShell [Core] v6+ now defaults to UTF-8. Assign the required encoding to $OutputEncoding
as needed.
batch_script.bat
:
@echo off
SET username=%~1
REM The batch file will pass its own stdin input through to Python.
python main.py
Read on for background information.
Invoking a batch file from PowerShell:
Unless you truly need to launch a batch file in a new window, the best approach is to invoke it directly from PowerShell; that way, it runs:
in the same console window, synchronously.
with its output streams connected to PowerShell's (which allows you to capture or redirect the output).
Because your batch-file path is stored in a variable, direct invocation requires use of &
, the call operator:
# Note: The " chars. around $unsecurePassword are only needed if the variable
# value contains cmd.exe metacharacters - see next section.
& $script 'userA' `"$unsecurePassword`"
Start-Process
is usually the wrong tool for invoking console applications, batch files, and other console-based scripts; see this answer for more information.
If you do need the batch file to run in a new window (which is only an option on Windows), use Start-Process
as follows (the command will execute asynchronously, unless you also pass -Wait
):
# The string with the arguments to pass is implicitly bound
# to the -ArgumentList parameter. Use only " for embedded quoting.
Start-Process $script "userA `"$unsecurePassword`""
Note: While the (implied) -ArgumentList
(-Args
) parameter is array-valued ([string[]]
) and passing the arguments individually is arguably the cleaner approach, this generally does not work properly, due to a longstanding bug that probably won't get fixed; for instance,
Start-Process foo.exe -Args 'one', 'two (2)'
passes 3 arguments rather than 2; that is, it passes single string 'two (2)'
as two arguments - see this GitHub issue.
Therefore, it is ultimately simpler and more predictable to pass a single argument with embedded quoting to -ArgumentList
, but be sure to use only "
(not '
) for embedded quoting:
Start-Process foo.exe -Args "one `"two (2)`""
Passing arguments robustly to cmd.exe
/ batch files:
Note:
The limitations of cmd.exe
(the legacy command processor that interprets batch files) prevent fully robust solutions; notably, you cannot prevent the interpretation of tokens such as %foo%
as environment-variable references when you call a batch file from PowerShell (at least not without altering the argument to %foo^%
, which will retain the ^
).
In your specific case, since you're trying to echo
an argument unquoted, embedded double quotes ("
) in such an argument - which need to be escaped as ""
- aren't properly supported: they are passed through as ""
.
Passing an unquoted argument to cmd.exe
/ a batch file breaks, if that argument contains one of cmd.exe
's metacharacters, i.e., characters with special syntactic meaning; in this context, they are: & | < > ^ "
The solution is to enclose the argument in double quotes ("..."
), with the added need to double "
chars. that are embedded (a part of the value).
PowerShell, after performing its own parsing of the command line (notably evaluating variable references and expressions), constructs the command line that is ultimately used to invoke the external target program, behind the scenes.
However, it only automatically double-quotes an argument if it contains spaces, not if it only contains cmd.exe
metacharacters; e.g., a variable with verbatim string content two (2)
is passed double-quoted - $val = 'two 2'; .\foo.bat $val
results in command line .\foo.bat "two 2"
- whereas string content a&b
is not - $val = 'a&b'.\foo.bat $val
results in .\foo.bat a&b
- which breaks.
The solution - as shown in your question - is to enclose the variable reference in literal, embedded "
characters, because such a "pre-quoted" value instructs PowerShell to pass the value as-is:
$val = 'a&b'; .\foo.bat `"$val`"
results in .\foo.bat "a&b"
Note: .\foo.bat "`"$val`""
has the same effect; I'm taking advantage of the fact that PowerShell in argument (parsing) mode (generally) implicitly treats arguments as if they were double-quoted; in expression (parsing) mode, such as in the array-construction statement in the question (@(..., ...)
), you do need the "`"$val`""
form.
The problem with your specific batch file:
A properly "..."
-enclosed argument (with any embedded "
chars. escaped as ""
) is properly seen as a parameter (e.g., %1
) inside a batch file.
However, it is seen with the enclosing double quotes and with any doubled embedded "
chars.
If you were to pass this parameter to the target program (python
in this case) as an argument, everything would work as expected.
However, since you're passing the value via stdin using echo
, you need to strip the enclosing double quotes so that they're not passed as part of the value, which is what your batch file attempts (e.g., %~2
)
However, passing the stripped value causes the echo
command to break.
There is no good solution to this problem with echo
, short of performing cumbersome explicit ^
-escaping (^
being cmd.exe
's escape character):
$escapedUnsecurePassword = $unsecurePassword -replace '[&|<>^]' -replace '"', '""'
& $script 'userA' `"$escapedUnsecurePassword`"
That alone still isn't enough, however - your batch_script.bat
file needs a modification too:
Because the assignment itself in your SET password=%~2
command isn't protected with double quotes, it breaks with values that contain metacharacters; somewhat paradoxically, you must use the form SET "password=%~2"
in order to safely strip the embedded enclosing "
chars.:
@echo off
REM Strip the enclosing "..." from the arguments (%~<n>)
REM !! The *assignment itself* must be in "..." so that
REM !! it does not break if the value has cmd.exe metacharacters.
set "username=%~1"
set "password=%~2"
echo(%password%|python main.py
Note that that will work as intended for all metacharacters except the - of necessity doubled - embedded "
, which are passed through as ""
.
However, there is a workaround for echoing a string with metacharacters unquoted, as also demonstrated in subcoder's helpful answer:
If you define batch_script.bat
as follows:
@echo off
set "username=%~1"
REM Do NOT strip quotes from the password argument
set password=%2
REM Using a trick with set /p, echo the password unquoted.
REM Note: Put the "|" directly after the ":" to avoid trailing spaces.
<NUL set /p=%password% & echo:|python main.py
The workaround repurposes set
's /p
option, which accepts a prompt message to print when interactively prompting the user for a value and prints the message without quotes; the actual interactive prompt is suppressed via <NUL
, so that only the message is printed.
echo:
prints a newline (line break), which is necessary, because the set /p
command prints its message without a trailing newline (if you don't want to send a newline, simply omit & echo:
).
Caveat: In addition to the problem with embedded ""
applying here too, this workaround has a side effect: it trims leading whitespace; e.g., " foo "
results in output foo
(only trailing whitespace is preserved); however, given that arguments with leading whitespace are rare, this may not matter in practice.
Given how cumbersome / obscure the above approaches are, the stdin-based approach shown at the top is preferable.