I need to pass a password with special characters from powershell script automation.ps1
to batch script batch_script.bat
which pipes it to main.py
. Piping from batch_script.bat
to main.py
works fine, that is authentication succeeds. However, when I run the entire procedure described above, authentication fails, but echo
ing the password shows the correct password string.
My guess is that there are issues with special characters. What is a safe way to pass these strings?
Background
I want to automate the daily download from some external source via a Python script main.py
. This process requires a password. So I wrote a batch_script.bat
which pipes the password to the Python script when prompted for it. However, I don't want to store the password as plain text in the batch script, so I encrypted the password and wrote another layer automation.ps1
which decrypts the password and passes it as plain text to batch_script.bat
.
automation.ps1
# get password
$securePassword = Get-Content "<file_path>" | ConvertTo-SecureString
$credentials = New-Object System.Management.Automation.PsCredential("<user_name>",$securePassword)
$unsecurePassword = ($credentials).GetNetworkCredential().Password
# call script
$script = "<path>\batch_script.bat"
$args = @("`"<user name>`"","`"$unsecurePassword`"")
start-process $script $args
batch_script.bat
(I am aware that in this example I discard the passed username, just wanted to preserve the fact that I pass multiple arguments in case there is any relevance to it)
@echo off
SET username=%~1
SET password=%~2
echo(%password%|python main.py
The PowerShell script is a collection of commands saved in a .ps1 extension file. PowerShell executes those commands in sequence. This tutorial will introduce different methods to pass arguments to a PowerShell script. We can define arguments using the param statement. It also allows us to use the default values.
It's usually simpler and easier to run the batch file as administrator/elevated, and Powershell will run with the same access rights as the batch file. Was this post helpful?
In your PowerShell script, add $a as a parameter to the batch file: In you batch file reference the parameter as %1. blat -s "Daily Report %1" -i "Our Team" -to [email protected],[email protected] -body "Please see attached." -priority 1 -attach D:\Script\Daily_Report.xlsx
The PowerShell script is a collection of commands saved in a .ps1 extension file. PowerShell executes those commands in sequence. This tutorial will introduce different methods to pass arguments to a PowerShell script.
With following, all special characters should be handled very well. If any character required to be escaped, check this
$pass could be any string but check for special characters of powershell
$pass="%^&<>|'\``,;=(\)![]\/";
# Wait till it ends with -Wait when using -NoNewWindow.
# It may be comprehensible to use `" instead of "" to denote we are enclosing string in quotes.
(thanks @mklement0 for elaboration).
start-process -Wait -NoNewWindow .\script.cmd "`"$pass`""
script.cmd
setlocal
rem Remove Double quotes
set "arg=%~1"
rem Test result with base64 encoding
echo|set/p="%arg%"|openssl base64
rem echo is used with set/p to prevent trailing new line.
echo|set/p="%arg%"|python main.py
rem Test with following, argument is in double quotes
rem script "%^&<>|'\`,;=(\)![]\/"
rem Expected result
rem %^&<>|'\`,;=(\)![]\/
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.
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)`""
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.
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.
You pass arguments to batch files in powershell using the -argumentlist
switch of start
/saps
. For you you could use:
saps "$script" -argumentlist $args
But I would suggest first breaking $args
up as it may not work since to pass arguments, you usually want to pass the arguments one at a time like:
saps "$script" -argumentlist "1","2","3"
Passing $args
will work most of the time, but there are some cases where where it won't work. Most of the time you are fine though
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With