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

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

Related

On WSL2 how do I get a powershell command's output to print to stdout

In WSL2 I want to run a powershell command and get the output of the command to print to stdout on my WSL2.
I have tried many variants but it seems I always just get the command given back and not its output:
$ pwsh.exe -Command {Get-Date}
Get-Date
$ pwsh.exe -Command '{Write-Output "hello"}'
Write-Output "hello"
You should not use curly braces here :
pwsh.exe -Command 'Get-Date'
Philippe's helpful answer provides the crucial pointer:
When calling pwsh, the PowerShell (Core) CLI, from outside PowerShell:
Do not use { ... } to enclose your command(s) - that only works from inside PowerShell.
Inside PowerShell, { ... } creates a script block literal, which is a reusable piece of PowerShell code that can be invoked on demand.
If you pass { ... } to pwsh from inside PowerShell, special processing happens behind the scenes, which notably preserves type fidelity in the output as much as possible - see this answer for details.
It should be noted that calling the PowerShell CLI from PowerShell is rarely necessary, except, for instance, on Windows, if you want to call the respective other PowerShell edition's CLI.
Instead, pass your command(s) as-is, inside a string.
If you pass a string to -Command from outside PowerShell with { ... } embedded, what happens is that PowerShell creates a script block and - in the absence of invoking it, with & or . - its string representation is output, which is the block's verbatim content (excluding { and }) - that is what you saw.
As an aside: There are many examples out there that use "& { ... }" as a -Command argument; while that technically works, it is unnecessary, and passing just "..." (or '...', from POSIX-compatible shells) is sufficient.
Thus:
# Simple case: no quoting necessary
$ pwsh.exe -Command Get-Date
# Command string that requires quoting:
$ pwsh.exe -Command 'Write-Output "hello"'

Unexpected token '}' when passing string to PowerShell script

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

Execute a PowerShell script within RunAs in a script

I have a need to run a PowerShell script as another user (the users will actually be doing the auth) based on detected environment. The script leverages a smartcard for login. The problem I have is when the PowerShell.exe instance launches from the runas, it simply prints my command and doesn't actually run it.
Note: The filepaths have spaces that get escaped with double-`, just not shown here hence the escaped ``' in the actual command. Have also used ```".
Example:
Start-Process runas.exe "/netonly /smartcard /user:domain\$env:USERNAME ""powershell.exe -noexit -Command `'&$filePath -arg1 -arg2`' "" "
or
Start-Process runas.exe "/netonly /smartcard /user:domain\$env:USERNAME ""powershell.exe -noexit -File `'$filePath -arg1 -arg2`' "" "
File path points to the same .ps1 file. The dir is similar to: C:\Users\Username\My Documents\script.ps1
When this runs I simply get a script window that doesn't actually run, it just prints the File name. I have also tried using -File but that simply crashes (regardless of -noexit). The runas bit works fine, I get my smartcard prompt etc its just the actual PowerShell launch that I am struggling with.
These work just fine calling them directly from PowerShell Cli but when in a .ps1 it just won't work.
Any help would be greatly appreciate.
There's generally no reason to use Start-Process to run a console application such as runas.exe - unless you explicitly want the application to run in a new window, asynchronously (by default) - see this answer for more information.
Eliminating Start-Process simplifies the quoting:
runas.exe /netonly /smartcard /user:domain\$env:USERNAME "powershell.exe -noexit -File \`"$filePath\`" -arg1 -arg2"
Note the - unfortunate - need to manually \-escape the embedded " chars. (`"), which is required up to at least v7.1 - see this answer.
As for what you tried:
On Windows, passing a '...'-enclosed string to the PowerShell CLI's -Command (-c) parameter from outside PowerShell (such as via runas.exe) causes it to be interpreted as a verbatim PowerShell string, printing its content as-is (except for whitespace normalization).
You can verify this by running the following from cmd.exe, for instance:
C:\>powershell -c 'Get-Date; whatever I type here is used as-is - almost'
Get-Date; whatever I type here is used as-is - almost
Note how the multiple spaces before the word almost were normalized to a single one.
The reason is that on Windows only double-quoting ("...") has syntactic function on the command line - ' characters are interpreted verbatim and therefore passed through.
Therefore, when the -Command (-c) argument sees its argument(s) -- after command-line parsing - and then interprets the resulting space-joined, possibly double-quote-stripped arguments as PowerShell source code, a span of '...' is interpreted as a verbatim PowerShell string, as usual (see the conceptual about_Quoting_Rules help topic, which discusses the types of PowerShell string literals).
In concrete terms:
'Get-Date; whatever I type here is used as-is - almost' is parsed by the PowerShell CLI as the following arguments(!):
'Get-Date;, whatever, I, type, here, is, used, as-is, -, almost' - note how any information about the amount of whitespace that originally separated these argument is lost in the process.
These arguments are then joined with a single space to form the string that PowerShell then interprets as PowerShell source code, which yields:
'Get-Date; whatever I type here is used as-is - almost'
This amounts to a verbatim (single-quoted) PowerShell string literal, whose content is then output as-is.

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).