Running PowerShell from another shell with Tee - powershell

I'd like to learn to execute a PowerShell command from another shell or language, e.g. Python os.system(). What I want to achieve is the following:
Execute the PowerShell command
Tee the output to both the console and a file
Return the command exit code
I think this gives an idea of what I would like to achieve, assuming to use cmd.exe as the caller environmnet:
powershell -NoProfile -command "& { cat foo.txt | Tee-Object ps-log.txt; exit $LASTEXITCODE }"
echo %errorlevel%
There are some problems here. First, I cannot use quotations in the command, e.g. :
powershell -NoProfile -command "& { cat `"foo bar.txt`" | Tee-Object ps-log.txt; exit $LASTEXITCODE }"
The cat argument seems to be passed unquoted and so cat looks for a 'bar.txt' parameter.
I think $LASTEXITCODE is expanded soon, that is before cat is executed.
& is inconvenient to use, because it does not accept a single command line string including arguments. An alternative to & is iex, however I cannot use it from cmd.exe. In fact:
powershell -NoProfile -command {iex cat foo.txt}
returns:
iex cat foo.txt

From cmd.exe, use the following (-c is short for -Command):
C:\>powershell -NoProfile -c "Get-Content \"foo bar.txt\" | Tee-Object ps-log.txt; exit -not $?"
There's no reason to use & { ... } in a string passed to -Command - just use ... instead.
Escape embedded " chars. as \" (PowerShell (Core) 7+ also accepts "").
Alternatively, as marsze's helpful answer points out, you can use '...' (single-quoting) inside the "..." string passed to -Command / -c, assuming that no string interpolation is required.
Since only PowerShell-native commands are involved in the command (on Windows, cat is simply an alias of Get-Content), $LASTEXITCODE is not set, as it only reflects the exit code of external programs. Instead, the automatic $? variable applies, which is a Boolean that indicates whether any errors were emitted by the commands in the most recently executed pipeline.
Negating this value with -not means that $true is converted to $false and $false to $true, and these values are converted to integers for the outside, with $false mapping to 0 and $true to 1.

Powershell supports single quotes, which saved me in such situations quite a lot of times. The good thing about it: They are unambiguous and easy to read. But mind that variable expansion won't work inside single-quoted strings.
powershell -NoProfile -command "cat 'foo bar.txt' | tee ps-log.txt"
Apart from that, have a look at the useful advice in mklement0's answer.

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"'

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.

Powershell 7: using ampersand (&) in string literals

I'm trying to execute the following command in PowerShell, but I have no idea how to escape the ampersand character which is part of the URL
az rest `
--method GET `
--uri ("https://graph.microsoft.com/v1.0/groups?`$count=true&`$filter=startsWith(displayName,'some+filter+text')&`$select=id,displayName") `
--headers 'Content-Type=application/json'
As the & character is used to start a new command, it breaks the url and want to execute the remainder.
Is there a way to tell powershell not to do that?
Olaf's answer provides an effective solution; let me add an explanation:
The source of the problem is a confluence of two behaviors:
When calling external programs, PowerShell performs on-demand double-quoting of each argument solely based on whether a given argument value contains spaces - otherwise, the argument is passed unquoted - irrespective of whether or not the value was originally quoted in the PowerShell command (e.g., cmd /c echo ab, cmd /c echo 'ab', and cmd /c echo "ab" all result in unquoted ab getting passed as the last token on the command line PowerShell rebuilds behind the scenes to ultimately use for execution).
The Azure az CLI is implemented as a batch file (az.cmd) and when a batch file is called, it is cmd.exe that parses the arguments given; surprisingly - and arguably inappropriately - it parses them as if the command had been submitted from inside a cmd.exe session.
As a result, if an argument is passed from PowerShell to a batch file that (a) contains no spaces, yet (b) contains cmd.exe metacharacters such as &, the call breaks.
A simple demonstration, using a cmd /c echo call as a stand-in for a call to a batch file:
# !! Breaks, because PowerShell (justifiably) passes *unquoted* a&b
# !! when it rebuilds the command line to invoke behind the scenes.
PS> cmd /c echo 'a&b'
a
'b' is not recognized as an internal or external command,
operable program or batch file.
There are three workarounds:
Use embedded "..." quoting:
# OK, but with a CAVEAT:
# Works as of PowerShell 7.2, but arguably *shouldn't*, because
# PowerShell should automatically *escape* the embedded " chars. as ""
PS> cmd /c echo '"a&b"'
"a&b"
# Ditto, using an *expandable* (interpolating) PowerShell string:
PS> cmd /c echo "`"$HOME & Family; can't put a `$ value on that.`""
"C:\Users\jdoe & Family; can't put a $ value on that." # e.g.
Use --%, the stop-parsing token - but see the bottom section of this answer for the limitations of --% and its associated pitfalls.
# OK, but with a CAVEAT:
# Requires "..." quoting, but doesn't recognize *PowerShell* variables,
# also doesn't support single-quoting and line continuation.
PS> cmd /c echo --% "a&b"
"a&b"
Call via cmd /c and pass a single string encompassing the batch-file call and all its arguments, (ultimately) using cmd.exe's syntax.
# OK (remember, cmd /c echo stands for a call to a batch file, such as az.cmd)
# Inside the single string passed to the outer cmd /c call,
# be sure to use "...", as that is the only quoting cmd.exe understands.
PS> cmd /c 'cmd /c echo "a&b"'
"a&b"
# Ditto, using an *expandable* (interpolating) PowerShell string:
PS> cmd /c "cmd /c echo `"$HOME & Family; can't put a `$ value on that.`""
"C:\Users\jdoe & Family; can't put a $ value on that." # e.g.
Taking a step back:
Now, wouldn't it be nice if you didn't have to worry about all these things?
Especially since you may not know or care if a given CLI - such as az - just so happens to be implemented as a batch file?
As a shell, PowerShell should do its best to relay arguments faithfully behind the scenes, and allow the caller to focus exclusively on satisfying only PowerShell's syntax rules:
Unfortunately, PowerShell has to date (PowerShell 7.2) generally done a very poor job in this regard, irrespective of cmd.exe's quirks - see this answer for a summary.
With respect to cmd.exe's (batch-file call) quirks, PowerShell could predictably compensate for them in a future version - but it looks like that isn't going to happen, unfortunately; see GitHub issue #15143.
I don't have access to an Azure tennant right now to test and I actually don't have experiences with the Azure CLI in general but I'd expect this to work:
az rest `
--method GET `
--uri 'https://graph.microsoft.com/v1.0/groups?$count=true&$filter=startsWith(displayName,some+filter+text)&$select=id,displayName' `
--headers 'Content-Type=application/json'
or this:
az rest --method GET --headers "Content-Type=application/json" `
--% --uri "https://graph.microsoft.com/v1.0/groups?$count=true&$filter=startsWith(displayName,some+filter+text)&$select=id,displayName"
I only added the backticks for better readability - you may remove them in your actual code.

Multiline powershell function inside batch script

I want to run .bat-script which calls some powershell function inside it. Function is not so small, so I want to split it. But I cannot do it, escape symbols doesn`t help ( ` ,^).
Script example:
set file=%1
set function="$file=$Env:file; ^
$hash = CertUtil -hashfile $file SHA256 | Select -Index 1"
powershell -command %function%
You can leave the quote at the end of each line like so:
set file=%1
set function="$file=$Env:file; "^
"$hash = CertUtil -hashfile $file SHA256 | Select -Index 1; "^
"example break line further...."
powershell -command %function%
The ^ works as multiline character but it also escapes the first character, so also a quote would be escaped.
Do not mix batchfile syntax with PowerShell. As #Stephan mentioned $function= won't work in batch file. You need to use set function= instead. Let's say I want to execute the following:
Get-Process
Get-ChildItem
Then the code should look like this:
set function=Get-Process; ^
Get-ChildItem;
And you start PowerShell with:
powershell -noexit -command %function%
-noexit added so that you can verify that the code was successfully executed.
Also keep in mind that what you pass to PowerShell is batch multiline and in PowerShell it's visible as one line so you have to remember about semicolon (which you actually do but I'm leaving this comment here for future readers).
There's also another option how to pass variable from batch script to PowerShell. You can do it like this:
set name=explorer
set function=get-process $args[0]; ^
get-childitem
powershell -noexit -command "& {%function% }" %name%
Explanation:
$args[0] represents first argument passed to the scriptblock. To pass that argument, add %name% after the scriptblock while starting powershell. Also, as pointed out in this answer (credits to #Aacini for pointing this out in comments), you have to add & operator and keep your scriptblock inside curly brackets { }.
Sidenote: to be honest, I'd avoid running scripts like this. Much simpler way would be to just save the file as .ps1 and run this in your batch file:
powershell -noexit -file .\script.ps1

Executing powershell command under cmd and INFO: Could not find files for the given pattern(s)

I am trying to execute the following powershell commands from CMD, for example:
powershell Get-WmiObject Win32_PnPSignedDriver
powershell Get-WmiObject Win32_PnPSignedDriver > test.txt
which both work correctly.
But when I do a query, for example:
powershell (Get-WmiObject Win32_PnPSignedDriver | where {$_.location -like "*PCI bus 0, device 22, function 0*"}).DeviceName
I am getting this message the cause of which I can not pin down:
INFO: Could not find files for the given pattern(s)
This seems to work for me (I was seeing the same error as you originally):
powershell -command "(Get-WmiObject Win32_PnPSignedDriver | where {$_.location -like '*PCI bus 0, device 22, function 0*'}).DeviceName"
To complement the existing answers with general guidelines for passing commands to powershell.exe from cmd.exe (a Command Prompt):
Enclose the entire PowerShell command in "..." (double quotes).
This protects its contents from unwanted up-front interpretation by cmd.exe - as happened with | in this case, as explained in aschipfl's answer.
However, cmd.exe-style environment-variable references (e.g., %USERNAME%) are still expanded.
For quoting that is embedded in the PowerShell command:
Use '...' (single quotes) where feasible; this is what Mark Wragg's helpful answer does, but it is only an option if the quoted string is meant to be a literal (doesn't contain variable references or subexpressions).
If you do need embedded "...", escape it as \"...\"
Note that while inside PowerShell it is the ` (backtick) that serves as the escape character, when passing strings from the outside PowerShell requires \.
Simplified examples (run from cmd.exe):
Note that passing a command without a parameter name implies the -Command parameter; run powershell -? to see the command-line syntax.
You may also want to use -NoProfile so that the user profile isn't loaded every time.
Commands that don't need embedded quoting - simply double-quote:
powershell -noprofile "get-date"
powershell -noprofile "(get-date).Date"
powershell -noprofile "get-date | get-member"
Commands with embedded quoting - use '...' or \"...\":
powershell -noprofile "(get-date).Year -like '*17'"
powershell -noprofile "$v=17; (get-date).Year -like \"*$v\""
Commands that incorporate the value of a cmd.exe environment variable:
:: # Nothing special to do: cmd.exe expands the reference irrespective of quoting.
set "v=17"
powershell -noprofile "(get-date).Year -like '*%v%'"
:: # More robust variant that passes the value as an argument to a script block.
set "v=17"
powershell -noprofile ". { param($v) (get-date).Year -like \"*$v\" }" "%v%"
Optional reading: calling powershell from POSIX-like shells such as bash:
The above rules apply analogously, except that you'll typically use '...' (single quotes) to enclose the entire PowerShell command, which categorically prevents up-front interpretation by the shell.
Using "..." is an option if up-front expansions of shell-variable references and command substitutions are explicitly desired, but the potential for confusion is great, because both POSIX-like shells and PowerShell use sigil $ to refer to variables - it may not be obvious what is expanded when.
POSIX-like shells categorically do not support embedding ' instances in side '...' strings, which necessitates a somewhat awkward workaround (see below).
Simplified examples (run from a POSIX-like shell such as bash):
Commands that don't need embedded quoting - simply single-quote:
powershell -noprofile 'get-date'
powershell -noprofile '(get-date).Date'
powershell -noprofile 'get-date | get-member'
Commands with embedded quoting - replace embedded ' instances with '\'' (sic) and use embedded " as-is:
powershell -noprofile '(get-date).Year -like '\''*17'\'''
powershell -noprofile '$v=17; (get-date).Year -like "*$v"'
Commands that incorporate the value of a shell/environment variable:
# By using a double-quoted string, the shell expands $v up front.
v=17
powershell -noprofile "(get-date).Year -like '*$v'"
# It gets trickier if you want to reference PS variables too.
# Note how the PS variable's $ is \-escaped so that the shell doesn't interpret it.
v=$HOME
powershell -noprofile "\$HOME -eq '$v'"
# More robust variant that passes the value as an argument to a script block.
v=17
powershell -noprofile '. { param($v) (get-date).Year -like "*$v" }' "$v"
I am pretty sure that the pipe character | is the problem here, because cmd tries to process it.
Simply escape it by preceding with ^:
powershell (Get-WmiObject Win32_PnPSignedDriver ^| where {$_.location -like "*PCI bus 0, device 22, function 0*"}).DeviceName
If this code is used within a parenthesised block of code in cmd, you may need to escape the closing ) as well (the opening ( can be escaped too, but there is no need):
powershell ^(Get-WmiObject Win32_PnPSignedDriver ^| where {$_.location -like "*PCI bus 0, device 22, function 0*"}^).DeviceName