Unexpected token '}' when passing string to PowerShell script - powershell

I have a script that accepts two strings, each are delimited by '|-|'. The second string can contain any character other than '.
To call the script I do:
PowerShell .\script.ps1 -arg1 'Hello|-|World' -arg2 'random|-|dwua0928]43d}'
It raises an error on the character }. How can I resolve this?
Edit:
It also has an issue with the - from the delimiters and raises the same error:
At line:1 char:54
+ .\Script.ps1 -arg2 <redacted>|-|<redacted> ...
+ ~
Missing expression after unary operator '-'.
At line:1 char:53
+ .\Script.ps1 -arg1 <redacted>|-|<redacted> ...
+ ~
Expressions are only allowed as the first element of a pipeline.
It does that at every instance of |-|. It does it with the brackets if I change the delimiter to /-/:
At line:1 char:168
+ ... <redacted>/-/<redacted>!OW}:Lj<redacted> ...
+ ~
Unexpected token '}' in expression or statement.
At line:1 char:181
+ ... /-/<redacted>cO0}e]k<redacted> ...
+ ~
Unexpected token '}' in expression or statement.
The beginning of the script is this:
Param
(
[switch] $enable,
[string] $arg1,
[string] $arg2
)

There's generally no need to call the PowerShell CLI (powershell.exe for Windows PowerShell, pwsh for PowerShell (Core) 7+) from inside PowerShell.
Assuming you're running Windows PowerShell (or, to generalize, the same edition of PowerShell), you can invoke your script directly:
.\script.ps1 -arg1 'Hello|-|World' -arg2 'random|-|dwua0928]43d}'
If you still want to call the CLI - which is expensive, because it involves creating a child process, and also results in a loss of type fidelity - use a script block ({ ... }), but note that this works only from inside PowerShell (though it works across both PowerShell editions, i.e., you can call pwsh.exe from Windows PowerShell, and powershell.exe from PowerShell (Core) 7+)
powershell { .\script.ps1 -arg1 'Hello|-|World' -arg2 'random|-|dwua0928]43d}' }
If you were to call from outside PowerShell, it is best to use the -File CLI parameter rather than the (implied in Windows PowerShell) -Command parameter:
powershell -File .\script.ps1 -arg1 "Hello|-|World" -arg2 "random|-|dwua0928]43d}"
While this works from inside PowerShell too, the disadvantage compared to the { ... } approach is that the output is only ever text, whereas the { ... } approach is - within limits - capable of preserving the original data types - see this answer for more information.
In order to make a -Command-based command line work, enclose the PowerShell code in "..." as a whole, in which case the embedded '...' strings are passed as such:
powershell -Command ".\script.ps1 -arg1 'Hello|-|World\' -arg2 'random|-|dwua0928]43d}'"
For a comprehensive overview of the PowerShell CLI, see this post.
As for what you tried:
You called powershell.exe with neither -File (-f) nor -Command (-c), which defaults to -Command (note that, by contrast, pwsh, the PowerShell (Core) 7+ CLI, defaults to -File).
When -Command is used, PowerShell's command-line processing first strips all subsequent arguments of any syntactic (i.e., unescaped) " characters, namely from "..."-enclosed arguments, joins the resulting tokens with spaces, and then interprets the resulting string as PowerShell code.
In your case, the fact that you used '...' quoting around the arguments is irrelevant:
PowerShell invariably translates the original quoting into "..." quoting when calling external programs (as they can only be expected to "..." quoting, not also '...' quoting)
However, it only uses "..." quoting if needed, which is solely based on whether the argument at hand contains spaces.
Neither of your arguments, verbatim Hello|-|World and random|-|dwua0928]43d}, contained spaces, so they were placed as-is on the command line for powershell.exe (though note that even if they did contain spaces and had been enclosed in "..." as a result, PowerShell's command-line processing would have stripped the " chars.)
The net result was that the powershell.exe child process tried to execute the following PowerShell code, which predictably breaks with the errors shown in your question:
# !! Note the absence of quoting around the -arg1 and -arg2 values.
.\script.ps1 -arg1 Hello|-|World -arg2 random|-|dwua0928]43d}
You can easily provoke the error you saw as follows from a PowerShell session:
# -> Error "Missing expression after unary operator '-'."
Write-Output Hello|-|World

Related

How to solve problem with quotes in arguments on calling a PowerShell script with -File option?

I want to call a small PowerShell (v5.1) script from Windows 10 Pro command line with arguments.
param($ReqURL, $JSONString)
Write-Host $ReqURL # for debug
Write-Host $JSONString # for debug
Invoke-RestMethod -Uri $ReqURL -Method Patch -ContentType "application/json" -Body $JSONString
Calling the script in a PowerShell console works fine:
.\http-patch.ps1 -ReqURL myRESTApi -JSONString '{"lastname": "Bunny"}'
Calling the script from the Windows Command Prompt (cmd.exe) works when I escape the double quotes, like this:
PowerShell .\http-patch.ps1 -ReqURL myRESTApi -JSONString '{\"lastname\": \"Bunny\"}'
But when I use the -File option, it fails because the $JSONString inside the script is equal to '{"lastname"::
PowerShell -File "C:\work\LV Demo\http-patch.ps1" -ReqURL myRESTApi -JSONString '{\"lastname\": \"Bunny\"}'
I assume here are some problems with quotes, but I can't find the right way.
When you use the -File parameter of powershell.exe, the Windows PowerShell CLI, only double quotes (") are recognized as having syntactic function when calling from outside PowerShell, such as from cmd.exe:
Therefore, switch from ' to " (the embedded " chars. require escaping as \" (sic) either way):
:: From cmd.exe
PowerShell -File "C:\work\LV Demo\http-patch.ps1" -JSONString "{\"lastname\": \"Bunny\"}" -ReqURL myRESTApi
By contrast, when you use -Command, '...' strings are recognized, after unescaped " chars. - the only ones with syntactic function during initial command-line parsing - have been stripped, because the resulting tokens are then interpreted as PowerShell code.
For guidance on when to use -Command vs. -File and the differences in resulting parsing, see this answer.
From inside PowerShell, there's rarely a need to to call another PowerShell instance, given that .ps1 files can be invoked directly, in-process.
In cases where you do need to call the CLI from inside PowerShell - say you need to call the CLI of the other PowerShell edition, PowerShell (Core)'s pwsh.exe, from powershell.exe or vice versa - the best choice is to use a script block ({ ... }) (which works only when calling from inside PowerShell), because that:
allows you to focus solely on PowerShell's own quoting and escaping requirements.
supports receiving typed output (objects other than strings), behind-the-scenes XML-based serialization, albeit with limited type fidelity - see this answer.
# From PowerShell
# Note how the " chars. now need NO escaping.
# (If you were to use a "..." string, you'd escape them as `" or "")
PowerShell {
& C:\work\LV Demo\http-patch.ps1" -JSONString '{"lastname": "Bunny"}' -ReqURL myRESTApi
}
While you can call via individual arguments, analogous to how you must call from outside PowerShell, you'll not only lose the benefit of typed output, but you'll also run in a longstanding bug up to PowerShell 7.2.x that requires manual escaping of embedded " chars. as \ when calling external programs - see this answer - as evidenced by one of your own attempts (here, using '...' is perfectly fine, because PowerShell recognizes it):
# !! Note the unexpected need to \-escape the embedded " chars.
PowerShell -File .\http-patch.ps1 -JSONString '{\"lastname\": \"Bunny\"}' -ReqURL myRESTApi
# Variant with double quotes.
# !! Note the *double* escaping: first with ` - for the sake of PowerShell itself -
# !! then with \ due to the bug.
PowerShell -File .\http-patch.ps1 -JSONString "{\`"lastname\`": \`"Bunny\`"}" -ReqURL myRESTApi

I can't run the code when I use powershell.exe -command

Cold you help me with following problem?
How to run the
"{0:n10}" -f 21.21
code in
powershell.exe -command "& {}"
argument?
There's no reason to use "& { ... }" in order to invoke code passed to PowerShell's CLI via the -Command (-c) parameter - just use "..." directly.
Older versions of the CLI documentation erroneously suggested that & { ... } is required, but this has since been corrected.
Unescaped " characters on the command line are stripped before the resulting argument(s) following -Command (-c) are interpreted as PowerShell code. " characters that need to be preserved as part of the PowerShell command to execute therefore need escaping.
From PowerShell's perspective, using \" works, in both editions.
Additionally, to prevent inadvertent whitespace normalization (folding of multiple spaces into one), the entire command to pass to -Command (-c) should be passed inside a single, "..." string overall:
Therefore (note that this assumes calling from outside PowerShell):
powershell.exe -Command " \"{0:n10}\" -f 21.21"
However, when calling from cmd.exe, specifically, \" escaping may break cmd.exe's syntax, in which case a different form of escaping of embedded " chars. is required - the following works robustly:
"^"" (sic) with powershell.exe, the Windows PowerShell CLI:
powershell.exe -Command " "^""{0:n10}"^"" -f 21.21"
"" with pwsh.exe, the PowerShell (Core) 7+ CLI:
pwsh.exe -Command " ""{0:n10}"" -f 21.21"
See this answer for details.

How to pass a string array to powershell through vbscript?

I'm trying to pass a string array to powershell through vbscript.
I got the params in the file.ps1 script:
param (
[string[]]$target
)
I'm trying to call file.ps1 through vbscript.
I want $target = #('c:\dev','d:\lib') to be passed in.
I currently have in my vbscript :-
target1 = """c:\dev"", ""d:\lib"""
Set objShell = CreateObject("Wscript.Shell")
objShell.run("powershell.exe -noexit -ExecutionPolicy Unrestricted -file .\file.ps1 -target1 """ & target & """")
which returns:
c:\dev, d:\lib and which isn't the powershell string array format "c:\dev", "d:\lib"
Update - Thanks mklement0 and alex-dl for your answers!
alex-dl's helpful answer contains an effective solution, as long as the array's elements don't need quoting, but let me try to break it down conceptually in more detail:
The -File parameter of PowerShell's CLI, powershell.exe (pwsh in PowerShell (Core) 7+) fundamentally does not support passing arrays to PowerShell code, because all arguments are interpreted strictly as whitespace-separated, with verbatim content.
E.g., "c:\dev", "d:\lib" on the command line is parsed as two arguments:
c:\dev, (note the trailing ,, syntactic " quotes stripped)
d:\lib
You must use the -Command (-c) option in order to pass arrays.
-Command fundamentally changes how the arguments are parsed:
All arguments are stripped of syntactic (non \"-escaped) " quotes.
The resulting tokens are space-concatenated to form a single string.
The resulting string is then interpreted as PowerShell code, i.e. as if you had submitted it from inside a PowerShell session.
This therefore enables all of PowerShell's features, such as passing arrays with ,, '...' (single-quoting), ..., and notably also means that references to PowerShell variables (e.g. $HOME) are recognized (unlike with -File).
See this answer for more guidance on when to use -File vs. -Command (-c).
Therefore, the best approach in your case is:
target = "'c:\dev', 'd:\lib'" ' Note the embedded '...'-quoting
Set objShell = CreateObject("Wscript.Shell")
' Note the use of -c (-command) instead of -file
objShell.Run("powershell.exe -noexit -ExecutionPolicy Unrestricted -c .\file.ps1 -target " & target)
Using embedded '...'-quoting simplifies passing the array elements in a way that PowerShell sees them as individually quoted (with the values at hand, this isn't strictly necessary, but it may be in other cases).
Doing this with embedded "..."-quoting gets unwieldy, because each embedded " must then be escaped as \"" (sic):
"" to escape the " inside the VBScript string,
and \ to ensure that \" is ultimately passed on the command line, which PowerShell requires for escaping " on the command line[1] (whereas PowerShell-internally, it is `" or (alternatively, inside a double-quoted string) "").
target = "\""c:\dev\"", \""d:\lib\"""
Set objShell = CreateObject("Wscript.Shell")
objShell.Run("powershell.exe -noexit -ExecutionPolicy Unrestricted -c .\file.ps1 -target " & target)
[1] While in Windows PowerShell you can situationally get away with "" instead of \", it can break, especially if the overall -c argument is enclosed in "...".
This problem has been fixed in PowerShell (Core) 7+, where you can now use "" and \" interchangeably.
E.g., when invoked from cmd.exe,
powershell -c " ""ab c"".length " breaks, whereas
pwsh -c " ""ab c"".length " works.
You could use the following vbscript:
target1 = """c:\dev, d:\lib"""
Set objShell = CreateObject("Wscript.Shell")
objShell.run("powershell.exe -Command .\file.ps1 -target " & target1)
And this powershell script:
param (
[string[]]$target
)
foreach ($t in $target) {
write-output $t
}
Read-Host -Prompt "Press Enter to exit"
Without -Command the argument is always interpreted as a single string.

Passing newline character to PowerShell via Cmd

I'm trying to run a PowerShell script from Windows cmd.exe. The input to the PowerShell script is a string, which contains newline characters using PowerShell backtick escaping - i.e:
`r`n
For demonstration purposes, the input string is then written to the console, and also dumped to a file.
The issue I have is that when the script is run from cmd.exe using the syntax
powershell.exe script.ps1 "TEST`r`nTEST"
The newline characters in the string are not treated as newline, and are included literally in both the console output and the output text file.
TEST`r`nTEST
However, if I run this from a PowerShell environment, I get the expected result (i.e. the newline characters are parsed correctly, and a newline is inserted in the appropriate location).
TEST
TEST
Similarly, if I pass in \r\n instead of the escaped newline characters through Windows cmd.exe, and do a .replace in the PowerShell script
$date = $data.replace("\r\n","`r`n")
I get the expected output:
TEST
TEST
Is anyone able to shed some light on why this happens?
The test script is as follows:
param([string]$data) # data to send
Write-Host $data
[IO.File]::WriteAllText('d:\temp.txt', $data)
return 0
And the file is called from the command line as:
powershell.exe script.ps1 "TEST`r`nTEST"
The script is running on Windows Server 2012 R2, using PowerShell v4.0
tl;dr
Use -Command and pass the entire PowerShell command as a single string; e.g.:
C:\> powershell -NoProfile -Command "script.ps1 \"TEST`r`nTEST\""
TEST
TEST
Note how the internal " instances are escaped as \", which PowerShell requires when called from the outside (alternatively, for full robustness, use "^"" (sic) in Windows PowerShell and "" in PowerShell (Core) v6+).
In your specific case,
powershell -NoProfile -Command script.ps1 "TEST`r`nTEST" would have worked too, but generally that only works as intended if the string has no embedded spaces.
Given that -Command is the default up to PSv5.1, your command - as currently posted - should work as-is.
As of PowerShell v5.1, arguments passed to powershell.exe from the outside:
ARE subject to interpretation by PowerShell, including string interpolation, by default and when you use -Command (i.e., specifying neither -File nor -Command currently defaults to -Command).
Caveat: The default behavior will change in v6: -File will be the default then - see the relevant change on GitHub.
are NOT subject to interpretation if you use -File to invoke a script - (after potential interpretation by cmd.exe) PowerShell treats all arguments as literal strings.
Caveat: This behavior is currently being discussed with respect to v6, given that it is overtly problematic in at least one case: trying to pass Boolean values.
Optional reading: Why you should pass the entire PowerShell command as a single argument when using -Command:
When you use -Command with multiple arguments, PowerShell essentially assembles them into a single command line behind the scenes before executing it.
Any "..."-quoting around the individual arguments is lost in the process, which can have unexpected results; e.g.:
C:\> powershell -NoProfile -Command "& { $args.count }" "line 1`r`nline 2"
3 # !! "line 1`r`nline 2" was broken into 3 arguments
Given that the outer "..." quoting was removed in the process of parsing the command line, the actual command line that PowerShell ended up executing was:
C:\ PS> & { $args.Count } line 1`r`nline 2
3
To illustrate why, let's look at an equivalent command that uses explicit quoting:
C:\ PS> & { $args.Count } "line" "1`r`nline" "2"
In other words: After the enclosing " were removed, the resulting token was broken into multiple arguments by spaces, as usual.
The parameter will need to be reinterpreted as a PowerShell string. Will this get you down the road?
The reason your -replace did not work is that the original string actually contains a backtick. It needs to be escaped in the search string.
C:\src\t>type p1.ps1
Param([string]$s)
Write-Host $s
$p = Invoke-Expression `"$s`"
Write-Host $p
$p2 = $s -replace "``r``n","`r`n"
Write-Host $p2
C:\src\t>powershell -noprofile -file .\p1.ps1 "TEST`r`nTEST"
TEST`r`nTEST
TEST
TEST
TEST
TEST
Carriage return and Linefeed are bytes with values 13 and 10, you can't write them, you can't see them.
As a convenience, when writing PowerShell code, the language will let you write:
"`r`n"
in a double quoted string, and when processing PowerShell source code (and at no other time), it will read those and replace them with bytes value 13 and 10.
It is this line of code in the PowerShell tokenizer which does it.
There is nothing special about backtick-n to the cmd.exe interpreter, and nothing special about having it in a string - you can put it there in a single quoted string
'`n'
or replacing it in a string - except that you have to note when the replacement happens. e.g. in your comment:
For example, if you pass in 'r'n and then replace 'r'n with 'r'n, the 'r'n is still output literally
Because your code
-replace "`r`n"
becomes
-replace "[char]13[char]10"
and your string passed in from outside contains
`r`n
and they don't match. Backtick-n in a string isn't magic, strings are not all interpreted by the PowerShell engine as PowerShell code, nor are parameters, or anything. And it's only in that context - when you write your -replace code, that is when the swap for actual newline characters happens.

How to download a file to $env:temp directory using powershell from cmd console?

I can download a file to a constant location from cmd console using powershell like this
C:\Users\Weijun>powershell -command "(new-object System.Net.WebClient).DownloadFile('https://example.com/example.zip', 'D:\example.zip')"
but i need to download it to system temp directory,using the powershell variable $env:temp,the following doesn't work.(variable not expanded in single-quote)
powershell -command "(new-object System.Net.WebClient).DownloadFile('https://example.com/example.zip', '$env:temp\example.zip')"
how to pass variable as parameter in method invoke?
UPDATE: it is a typo for $env:temp:,I amend it.
tl;dr:
From outside of PowerShell (cmd.exe - Command Prompt or batch file), use escaped double-quoted strings - \"...\" - inside the overall command string - "..." - passed to powershell -command for strings that need interpolation (e.g., expansion of variable references such as $env:temp):
powershell -command "(new-object System.Net.WebClient).DownloadFile('https://example.com/example.zip', \"$env:temp\example.zip\")"
You not only need to fix the incidental problem that Nkosi points out - removing the extraneous : at the end of $env:temp: - but you must generally either not quote the path at all (argument syntax), or use escaped double quotes (expression syntax or values with shell metacharacters) - single-quoted strings don't work, because PowerShell treats their content as a literal and won't expand the environment-variable reference.[1]
For a description of PowerShell's two fundamental parsing modes - argument syntax vs. expression syntax - see Get-Help about_Parsing.
Simplified examples:
Without quoting (argument syntax):
c:\>powershell.exe -noprofile -command "write-output $env:temp\example.zip"
C:\Users\jdoe\AppData\Local\Temp\example.zip
With double-quoting (expression syntax or if the value contains shell metacharacters such as , or |):
c:\>powershell.exe -noprofile -command "write-output \"$env:temp\example.zip\""
C:\Users\jdoe\AppData\Local\Temp\example.zip
Note: When called from the outside, PowerShell requires embedded " chars. to be escaped as \" - unlike inside PowerShell, where `" must be used.
Applied to your command: with (escaped) double-quoting, which is needed, because you're using .NET method syntax, which is parsed with expression syntax:
powershell -command "(new-object System.Net.WebClient).DownloadFile('https://example.com/example.zip', \"$env:temp\example.zip\")"
[1] If you run the same command from PowerShell, you won't see the problem, because the command as a whole is enclosed in "...", which means that the current PowerShell session will expand $env:temp - before it is passed to powershell.exe.
However, when called from cmd.exe, this interpolation is NOT applied - hence the need to either omit quoting or use embedded (escaped) double-quoting.
I would use Invoke-WebRequest with -OutFile:
powershell -command "Invoke-WebRequest -Uri 'http://example.com/file-to-get.txt' -OutFile $env:temp\file-to-get.txt"
NB this requires PowerShell 3.0 (if I remember correctly).