"echo on" in powershell or how do I make Powershell output the command lines of all the commands, INCLUDING the native ones invoked by the script? - powershell

My question may seem duplicate of PowerShell "echo on", but it is not.
I am not interested in capturing the command output, but in the command line itself of every command executed by the script, including the native commands.
This is what "echo on" in cmd does and this is what I am looking for. Set-PSDebug -Trace 1 does not do it and neither passing the -Verbose flag.
So far I have not see a way except outputing them myself, which is a huge pain in itself.
So, can Powershell do what "echo on" does in cmd?
EDIT 1
Not ideal, but I would accept an answer suggesting to use a wrapper function which would receive a command (native or powershell) with parameters and run the command while faithfully logging the respective command line. Of course, the wrapper function code should be part of the answer.
EDIT 2
The following trivial example demonstrates why Set-PSDebug -Trace 1 does not do it:
tasklist `
/fi "status eq running" | Select-Object -First 4
Please, observe:
C:\> cat C:\temp\1.ps1
tasklist `
/fi "status eq running" | Select-Object -First 4
C:\> Set-PSDebug -Trace 1
C:\> C:\temp\1.ps1
DEBUG: 1+ >>>> C:\temp\1.ps1
DEBUG: 1+ >>>> tasklist `
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
csrss.exe 756 Console 1 2,816 K
C:\>
EDIT 3
For comparison, observe an equivalent script in cmd with echo on:
C:\>type c:\temp\1.cmd
#echo on
tasklist ^
/fi "status eq running" |findstr/n ^^|findstr "^[1-4]:"
C:\>c:\temp\1.cmd
C:\>tasklist /fi "status eq running" | findstr/n ^ | findstr "^[1-4]:"
1:
2:Image Name PID Session Name Session# Mem Usage
3:========================= ======== ================ =========== ============
4:csrss.exe 756 Console 1 2,328 K
C:\>
EDIT 4
start-transcript does not do it either:
C:\WINDOWS\system32> cat c:\temp\1.ps1
tasklist `
/fi "status eq running" | Select-Object -First 4 | Out-Default
C:\WINDOWS\system32> Start-Transcript
Transcript started, output file is ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
C:\WINDOWS\system32> c:\temp\1.ps1
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
csrss.exe 756 Console 1 2,936 K
C:\WINDOWS\system32> Stop-Transcript
Transcript stopped, output file is ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
C:\WINDOWS\system32> cat ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
**********************
Windows PowerShell transcript start
Start time: 20190611143800
Username: xyz\me
RunAs User: xyz\me
Configuration Name:
Machine: L-PF0TBKV7 (Microsoft Windows NT 10.0.16299.0)
Host Application: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Process ID: 25508
PSVersion: 5.1.16299.1004
PSEdition: Desktop
PSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1.16299.1004
BuildVersion: 10.0.16299.1004
CLRVersion: 4.0.30319.42000
WSManStackVersion: 3.0
PSRemotingProtocolVersion: 2.3
SerializationVersion: 1.1.0.1
**********************
Transcript started, output file is ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
C:\WINDOWS\system32
>
PS>c:\temp\1.ps1
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
csrss.exe 756 Console 1 2,936 K
C:\WINDOWS\system32
>
PS>Stop-Transcript
**********************
Windows PowerShell transcript end
End time: 20190611143810
**********************
C:\WINDOWS\system32>
As you can see it does not contain the command line.

Firstly, the reason you're dissatisfied with the built-in options is because you're going against the grain; your requirement is like asking how to put sacks of gravel in the back of a Porsche. Powershell comes with Verbose and Debug output streams and a fantastic debugger.
If you have any ability to influence coding standards, look at splatting as an alternative to backtick-continuations.
If you can count on versions of Windows that are not years past EoL, consider Get-ScheduledTask | Where-Object State -eq 'Ready' instead of tasklist.
That said, yes, what you want is possible. Here's a script that will echo across line continuations:
# Echo.ps1
function Disable-Echo
{
param
(
[Parameter(Mandatory)]
[string]$Path
)
$Path = ($Path | Resolve-Path -ErrorAction Stop).Path
Get-PSBreakpoint -Script $Path | Remove-PSBreakpoint
}
function Enable-Echo
{
param
(
[Parameter(Mandatory)]
[string]$Path
)
$Path = ($Path | Resolve-Path -ErrorAction Stop).Path
Disable-Echo $Path
$Ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$null)
$Statements = $Ast.BeginBlock, $Ast.ProcessBlock, $Ast.EndBlock |
Select-Object -ExpandProperty Statements |
Write-Output |
Where-Object {$_.Extent}
foreach ($Statement in $Statements)
{
$Action = {
$Text = $Statement.Extent.Text
$Text = $Text -replace '`\r?\n' # Concatenate lines that are escaped with backtick
# Alternative to remove superfluous whitespace:
# $Text = $Text -replace '\s+`\r?\n\s*', ' '
Write-Host "ECHO: $Text" -ForegroundColor Yellow
continue # or 'break' to stop on the line
}.GetNewClosure() # Create a closure, to capture the value of $Statement
$Params = #{
Script = $Path
Line = $Statement.Extent.StartLineNumber
Column = $Statement.Extent.StartColumnNumber
Action = $Action
}
$null = Set-PSBreakpoint #Params
}
}
Sample script:
# foo.ps1
gci `
-Name `
-File `
-Filter Victor.*
gci -File -Name *.md; gci -File -Name *.psd1
Usage:
# Without echo
❯ .\foo.ps1
Victor.build.ps1
Victor.psd1
Victor.psm1
README.md
Victor.psd1
❯ . .\Echo.ps1
❯ Enable-Echo .\foo.ps1
❯ .\foo.ps1
ECHO: gci -Name -File -Filter Victor.*
Victor.build.ps1
Victor.psd1
Victor.psm1
ECHO: gci -File -Name *.md
README.md
ECHO: gci -File -Name *.psd1
Victor.psd1
Tested on PSv5 and PSv7. Should work on PSv2, although the sample foo.ps1 is PSv3+ (IIRC).
This will not echo calls to other scripts. For that, you'd probably want to do more AST inspection, identify CommandAsts that call scripts, and recursively enable echo on those scripts too. Alternatively, there might be joy in Set-PSBreakpoint -Variable - the $$ variable might be a good place to start - but this would likely be a PITA to work with as it would invoke while you're trying to debug the echo function. You could inspect Get-PSCallStack to skip the action while you're at the command prompt.

I expect four answers and you have already mentioned three that do not work for you (Set-PSDebug, Start-Transaction, -Verbose). As much as they may be viable but not in the format you are looking for, I will not talk more of them.
For the third option, try using Get-History. Now, this will not print out each command as you execute it (only when you call it) like I assume you want. It will also likely not print out each of the lines inside another script (you would want a trace but you did not like that because it prints out more than just the execution).
You can try asking around the PowerShell repository but I do not expect you to find what you are seeking.

If Event logs is an option, start tracing by enabling this Group Policy.
Administrative Templates -> Windows Components -> Windows PowerShell
See Microsoft Docs - Script Tracing and Logging
Then you would of course need to parse the Event logs accordingly...

Related

How can I elevate Powershell while keeping the current working directory AND maintain all parameters passed to the script?

function Test-IsAdministrator
{
$Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = New-Object System.Security.Principal.WindowsPrincipal($Identity)
$Principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Test-IsUacEnabled
{
(Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System).EnableLua -ne 0
}
if (!(Test-IsAdministrator))
{
if (Test-IsUacEnabled)
{
[string[]]$argList = #('-NoProfile', '-NoExit', '-File', $MyInvocation.MyCommand.Path)
$argList += $MyInvocation.BoundParameters.GetEnumerator() | ForEach-Object {"-$($_.Key)", "$($_.Value)"}
$argList += $MyInvocation.UnboundArguments
Start-Process PowerShell.exe -Verb Runas -WorkingDirectory $pwd -ArgumentList $argList
return
}
else
{
throw "You must be an administrator to run this script."
}
}
If I run the script above, it successfully spawns another PowerShell instance with elevated privileges but the current working directory is lost and automatically set to C:\Windows\System32. Bound Parameters are also lost or incorrectly parsed.
After reading similar questions I learned that when using Start-Process with -Verb RunAs, the -WorkingDirectory argument is only honored if the target executable is a .NET executable. For some reason PowerShell 5 doesn't honor it:
The problem exists at the level of the .NET API that PowerShell uses behind the scenes (see System.Diagnostics.ProcessStartInfo), as of this writing (.NET 6.0.0-preview.4.21253.7).
Quote from this related question:
In practice - and the docs do not mention that - the -WorkingDirectory parameter is not respected if you start a process elevated (with administrative privileges, which is what -Verb RunAs - somewhat obscurely - does): the location defaults to $env:SYSTEMROOT\system32 (typically, C:\Windows\System32).
So the most common solution I've seen involves using -Command instead of -File. I.E:
Start-Process -FilePath powershell.exe -Verb Runas -ArgumentList '-Command', 'cd C:\ws; & .\script.ps1'
This looks really hack-ish but works. The only problem is I can't manage to get an implementation that can pass both bound and unbound parameters to the script being called via -Command.
I am trying my hardest to find the most robust implementation of self-elevation possible so that I can nicely wrap it into a function (and eventually into a module I'm working on) such as Request-AdminRights which can then be cleanly called immediately in new scripts that require admin privileges and/or escalation. Pasting the same auto-elevation code at the beginning of every script that needs admin rights feels really sloppy.
I'm also concerned I might be overthinking things, and to just leave elevation to the script level instead of wrapping it into a function.
Any input at all is greatly appreciated.
Note: On 15 Nov 2021 a bug was fixed in the code below in order to make it work properly with advanced scripts - see this answer for details.
The closest you can get to a robust, cross-platform self-elevating script solution that supports:
both positional (unnamed) and named arguments
while preserving type fidelity within the constraints of PowerShell's serialization (see this answer)
preserving the caller's working directory.
On Unix-like platforms only: synchronous, same-window execution with exit-code reporting (via the standard sudo utility).
is the following monstrosity (I certainly wish this were easier):
Note:
For (relative) brevity, I've omitted your Test-IsUacEnabled test, and simplified the test for whether the current session is already elevated to [bool] (net.exe session 2>$null)
You can drop everything between # --- BEGIN: Helper function for self-elevation. and # --- END: Helper function for self-elevation. into any script to make it self-elevating.
If you find yourself in repeated need of self-elevation, in different scripts, you can copy the code into your $PROFILE file or - better suited to wider distribution - convert the dynamic (in-memory) module used below (via New-Module) into a regular persisted module that your scripts can (auto-)load. With the Ensure-Elevated function available available via an auto-loading module, all you need in a given script is to call Ensure-Elevated, without arguments (or with -Verbose for verbose output).
# Sample script parameter declarations.
# Note: Since there is no [CmdletBinding()] attribute and no [Parameter()] attributes,
# the script also accepts *unbound* arguments.
param(
[object] $First,
[int] $Second,
[array] $Third
)
# --- BEGIN: Helper function for self-elevation.
# Define a dynamic (in-memory) module that exports a single function, Ensure-Elevated.
# Note:
# * In real life you would put this function in a regular, persisted module.
# * Technically, 'Ensure' is not an approved verb, but it seems like the best fit.
$null = New-Module -Name "SelfElevation_$PID" -ScriptBlock {
function Ensure-Elevated {
[CmdletBinding()]
param()
$isWin = $env:OS -eq 'Windows_NT'
# Simply return, if already elevated.
if (($isWin -and (net.exe session 2>$null)) -or (-not $isWin -and 0 -eq (id -u))) {
Write-Verbose "(Now) running as $(("superuser", "admin")[$isWin])."
return
}
# Get the relevant variable values from the calling script's scope.
$scriptPath = $PSCmdlet.GetVariableValue('PSCommandPath')
$scriptBoundParameters = $PSCmdlet.GetVariableValue('PSBoundParameters')
$scriptArgs = $PSCmdlet.GetVariableValue('args')
Write-Verbose ("This script, `"$scriptPath`", requires " + ("superuser privileges, ", "admin privileges, ")[$isWin] + ("re-invoking with sudo...", "re-invoking in a new window with elevation...")[$isWin])
# Note:
# * On Windows, the script invariably runs in a *new window*, and by design we let it run asynchronously, in a stay-open session.
# * On Unix, sudo runs in the *same window, synchronously*, and we return to the calling shell when the script exits.
# * -inputFormat xml -outputFormat xml are NOT used:
# * The use of -encodedArguments *implies* CLIXML serialization of the arguments; -inputFormat xml presumably only relates to *stdin* input.
# * On Unix, the CLIXML output created by -ouputFormat xml is not recognized by the calling PowerShell instance and passed through as text.
# * On Windows, the elevated session's working dir. is set to the same as the caller's (happens by default on Unix, and also in PS Core on Windows - but not in *WinPS*)
# Determine the full path of the PowerShell executable running this session.
# Note: The (obsolescent) ISE doesn't support the same CLI parameters as powershell.exe, so we use the latter.
$psExe = (Get-Process -Id $PID).Path -replace '_ise(?=\.exe$)'
if (0 -ne ($scriptBoundParameters.Count + $scriptArgs.Count)) {
# ARGUMENTS WERE PASSED, so the CLI must be called with -encodedCommand and -encodedArguments, for robustness.
# !! To work around a bug in the deserialization of [switch] instances, replace them with Boolean values.
foreach ($key in #($scriptBoundParameters.Keys)) {
if (($val = $scriptBoundParameters[$key]) -is [switch]) { $null = $scriptBoundParameters.Remove($key); $null = $scriptBoundParameters.Add($key, $val.IsPresent) }
}
# Note: If the enclosing script is non-advanced, *both*
# $scriptBoundParameters and $scriptArgs may be present.
# !! Be sure to pass #() when $args is $null (advanced script), otherwise a scalar $null will be passed on reinvocation.
# Use the same serialization depth as the remoting infrastructure (1).
$serializedArgs = [System.Management.Automation.PSSerializer]::Serialize(($scriptBoundParameters, (#(), $scriptArgs)[$null -ne $scriptArgs]), 1)
# The command that receives the (deserialized) arguments.
# Note: Since the new window running the elevated session must remain open, we do *not* append `exit $LASTEXITCODE`, unlike on Unix.
$cmd = 'param($bound, $positional) Set-Location "{0}"; & "{1}" #bound #positional' -f (Get-Location -PSProvider FileSystem).ProviderPath, $scriptPath
if ($isWin) {
Start-Process -Verb RunAs $psExe ('-noexit -encodedCommand {0} -encodedArguments {1}' -f [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd)), [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($serializedArgs)))
}
else {
sudo $psExe -encodedCommand ([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd))) -encodedArguments ([Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($serializedArgs)))
}
}
else {
# NO ARGUMENTS were passed - simple reinvocation of the script with -c (-Command) is sufficient.
# Note: While -f (-File) would normally be sufficient, it leaves $args undefined, which could cause the calling script to break.
# Also, on WinPS we must set the working dir.
if ($isWin) {
Start-Process -Verb RunAs $psExe ('-noexit -c Set-Location "{0}"; & "{1}"' -f (Get-Location -PSProvider FileSystem).ProviderPath, $scriptPath)
}
else {
# Note: On Unix, the working directory is always automatically inherited.
sudo $psExe -c "& `"$scriptPath`"; exit $LASTEXITCODE"
}
}
# EXIT after reinvocation, passing the exit code through, if possible:
# On Windows, since Start-Process was invoked asynchronously, all we can report is whether *it* failed on invocation.
exit ($LASTEXITCODE, (1, 0)[$?])[$isWin]
}
}
# --- END: Helper function for self-elevation.
"Current location: $($PWD.ProviderPath)"
# Call the self-elevation helper function:
# * If this session is already elevated, the call is a no-op and execution continues,
# in the current console window.
# * Otherwise, the function exits the script and re-invokes it with elevation,
# passing all arguments through and preserving the working directory.
# * On Windows:
# * UAC will prompt for confirmation / adming credentials every time.
# * Of technical necessity, the elevated session runs in a *new* console window,
# asynchronously, and the window running the elevated session remains open.
# Note: The new window is a regular *console window*, irrespective of the
# environment you're calling from (including Windows Terminal, VSCode,
# or the (obsolescent) ISE).
# * Due to running asynchronously in a new window, the calling session won't know
# the elevated script call's exit code.
# * On Unix:
# * The `sudo` utility used for elevation will prompt for a password,
# and by default remember it for 5 minutes for repeat invocations.
# * The elevated script runs in the *current* window, *synchronously*,
# and $LASTEXITCODE reflects the elevated script's exit code.
# That is, the elevated script runs and returns control to the non-elevated caller.
# Note that $LASTEXITCODE is only meaningful if the elevated script
# sets its intentionally, via `exit $n`.
# Omit -Verbose to suppress verbose output.
Ensure-Elevated -Verbose
# For illustration:
# Print the arguments received in diagnostic form.
Write-Verbose -Verbose '== Arguments received:'
[PSCustomObject] #{
PSBoundParameters = $PSBoundParameters.GetEnumerator() | Select-Object Key, Value, #{ n='Type'; e={ $_.Value.GetType().Name } } | Out-String
# Only applies to non-advanced scripts
Args = $args | ForEach-Object { [pscustomobject] #{ Value = $_; Type = $_.GetType().Name } } | Out-String
CurrentLocation = $PWD.ProviderPath
} | Format-List
Sample call:
If you save the above code to file script.ps1 and invoke it as follows:
./script.ps1 -First (get-date) -Third ('foo', 'bar') -Second 42 #{ unbound=1 } 'last unbound'
you'll see the following:
In the non-elevated session, which triggers the UAC / sudo password prompt (Windows example):
Current location: C:\Users\jdoe\sample
VERBOSE: This script, "C:\Users\jdoe\sample\script.ps1", requires admin privileges, re-invoking in a new window with elevation...
In the elevated session (which on Unix runs transiently in the same window):
VERBOSE: (Now) running as admin.
VERBOSE: == Arguments received:
PSBoundParameters :
Key Value Type
--- ----- ----
First 10/30/2021 12:30:08 PM DateTime
Third {foo, bar} Object[]
Second 42 Int32
Args :
Value Type
----- ----
{unbound} Hashtable
last unbound String
CurrentLocation : C:\Users\jdoe\sample
I figured out a really short solution:
if (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
$CommandLine = "-NoExit -c cd '$pwd'; & `"" + $MyInvocation.MyCommand.Path + "`""
Start-Process powershell -Verb runas -ArgumentList $CommandLine
Exit
}
}
#Elevated script content after that

How to execute powershell code which is in variable

suppose I have the following powershell code stored in a file:
## file1.ps1
$myvar = "i am here"
if ($myvar -ne $null) {
"($myvar) variable is Full"
} else {
"($myvar) variable is Empty"
}
And this code is then stored in a variable:
$code_in_var = cat file1.ps1
How do I execute the code in the variable through piping?
I have tried the following:
PS C:\Mrm> $code_in_var = cat file1.ps1
PS C:\Mrm>
PS C:\Mrm> cat file1.ps1 | powershell -
PS C:\Mrm>
PS C:\Mrm> cat file1.ps1 | Invoke-expression
Invoke-expression ; At line:1 char:23
+ if ($myvar -ne $null) {
+ ~
Missing closing '}' in statement bllock or type definition
At line:1 char:17
PS C:\Mrm>
PS C:\Mrm> $code_in_var | powershell - ***(this does not return anything)***
PS C:\Mrm>
PS C:\Mrm>
PS C:\Mrm> $code_in_var | Invoke-expression
**same error**
However, If I run this script directly:
PS C:\Mrm> .\file1.ps1
(i am here) variable is Full
It works as it is expected to.
My question is, how do I run a full powershell code that is stored in a variable, as though it were in a file?
You can store a ScriptBlock in a variable and use the & call operator like this:
$scriptBlock = {
$myvar = "i am here"
if ($myvar -ne $null) {
"($myvar) variable is Full"
} else {
"($myvar) variable is Empty"
}
}
& $scriptBlock
Output:
(i am here) variable is Full
A similar thing is fully documented in the MS Docs PowerShell help file.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-expression?view=powershell-7
You will note that at no point is the formatted multi-line code used. It's all one-liner execution code.
Note that in the call of your code, it's just this.
#($myvar = "i am here"
if ($myvar -ne $null) {
"($myvar) variable is Full"
} else {
"($myvar) variable is Empty"
}) |
Out-File -FilePath 'D:\Temp\MyCode.ps1'
($Command = Get-Content -Path 'D:\Temp\MyCode.ps1')
# Results the code just runs with no Invoke-* needed
<#
(i am here) variable is Full
#>
Update as per my comments
You define a var, with whatever string you want, and just type the var name.
But you must do it all in the PowerShell process.
Why define PowerShell code outside the PowerShell process, just to send it to the PowerShell process?
# In a PowerShell Script development session
$MyCode = #($myvar = "i am here"
if ($myvar -ne $null) {
"($myvar) variable is Full"
} else {
"($myvar) variable is Empty"
})
$MyCode
# Results
<#
(i am here) variable is Full
#>
$myvar = "i am here"
if ($myvar -ne $null) {
"($myvar) variable is Full"
} else {
"($myvar) variable is Empty"
}
$myvar
# Results
<#
i am here
#>
# In a PowerShell terminal interactive session.
$myvar = "i am here";if ($myvar -ne $null) {"($myvar) variable is Full"} else {"($myvar) variable is Empty"}
$myvar
# Results
<#
i am here
#>
$myvar = $null;if ($myvar -ne $null) {"$myvar variable is Full"} else {Write-Warning -Message "$myvar variable is Empty"}
# Results
<#
WARNING: variable is Empty
#>
You cannot define a var outside of PowerShell then start PowerShell and try to use that var content. It's not in scope. All code, vars, functions, etc., have a scope and the scope must be defined in the PowerShell process
If you are saying you are wanting to run PowerShell code started from another terminal, say cmd.exe, then PowerShell has start parameters, and proper quoting, when needed, is a thing.
# There are several PowerShell startup parameters
<#
The syntax of Powershell.exe Command is:
PowerShell[.exe]
[-EncodedCommand ]
[-ExecutionPolicy ]
[-InputFormat {Text | XML}]
[-Mta]
[-NoExit]
[-NoLogo]
[-NonInteractive]
[-NoProfile]
[-OutputFormat {Text | XML}]
[-PSConsoleFile ]
[ -Version <Windows PowerShell version> ]
[-Sta]
[-WindowStyle ]<style>
[-File <FilePath> [<Args>]]
[-Command { - | <script-block> [-args <arg-array> ] | <string> [<CommandParameters>] } ]
#>
powershell -Command "Whatever command you want to run"
# Example, from a cmd.exe prompt
powershell -NoProfile -Command Get-Date
# Results
<#
Sunday, 14 June, 2020 04:31:38
#>
powershell -NoProfile -Command '{"Hello World"}'
# Results
<#
Hello World
#>
You cannot do this ...
${yourcode} | powershell
... outside of a PowerShell session. The PowerShell pipeline is only available in a PowerShell session. If you are already in a session, then again, just type and run the code.
Whatever terminal you are in has to be able to send this to PowerShell to run via the normal PowerShell startup. Yet, again, this is just introducing unneeded complexity, vs just running the script of running the code in a PowerShell interactive session directly.
You could store the code from the variable in a temporary script file and then execute it:
$code_in_var = cat .\file1.ps1
$tmpScriptFile = New-TemporaryFile | Rename-Item -NewName {$_.Name -ireplace 'tmp$', 'ps1'} -PassThru
Set-Content -Path $tmpScriptFile -Value $code_in_var
& $tmpScriptFile
Remove-Item -Path $tmpScriptFile
But if the code already originates from a file, just directly execute that instead of course.

How to simultaneously capture external command output and print it to the terminal

Can I pipe back from:
$OUTPUT = $(flutter build ios --release --no-codesign | tail -1)
I would like to get both the last line from the build AND show progress, something like
$OUTPUT = $(flutter build ios --release --no-codesign | out | tail -1)
where the hypothetical out utility would also send the output to the terminal.
Do you know how?
Note:
On Unix-like platforms, with external-program output, js2010's elegant tee /dev/tty solution is the simplest.
The solutions below, which also work on Windows, may be of interest for processing external-program output line by line in PowerShell.
A general solution that also works with the complex objects that PowerShell-native commands can output, requires different approaches:
In PowerShell (Core) 7+, use the following:
# PS v7+ only. Works on both Windows and Unix
... | Tee-Object ($IsWindows ? 'CON' : '/dev/tty')
In Windows PowerShell, where Tee-Object unfortunately doesn't support targeting CON, a proxy function that utilizes Out-Host is required - see this answer.
A PowerShell solution (given that the code in your question is PowerShell[1]):
I'm not sure how flutter reports its progress, but the following may work:
If everything goes to stdout:
$OUTPUT = flutter build ios --release --no-codesign | % {
Write-Host $_ # print to host (console)
$_ # send through pipeline
} | select -Last 1
Note: % is the built-in alias for ForEach-Object, and select the one for Select-Object.
If progress messages go to stderr:
$OUTPUT = flutter build ios --release --no-codesign 2>&1 | % {
Write-Host $_.ToString() # print to host (console)
if ($_ -is [string]) { $_ } # send only stdout through pipeline
} | select -Last 1
[1] As evidenced by the $ sigil in the variable name in the LHS of an assignment and the spaces around =
($OUTPUT = ), neither of which would work as intended in bash / POSIX-like shells.
I assume you mean bash because to my knowledge there is no tail in powershell.
Here's how you can see a command's output while still capturing it into a variable.
#!/bin/bash
# redirect the file descriptor 3 to 1 (stdout)
exec 3>&1
longRunningCmd="flutter build ios --release --no-codesign"
# use tee to copy the command's output to file descriptor 3 (stdout) while
# capturing 1 (stdout) into a variable
output=$(eval "$longRunningCmd" | tee >(cat - >&3) )
# last line of output
lastline=$(printf "%s" "$output" | tail -n 1)
echo "$lastline"
I use write-progress in the pipeline.
In order to keep readable pipeline, I wrote a function
function Write-PipedProgress{
<#
.SYNOPSIS
Insert this function in a pipeline to display progress bar to user
.EXAMPLE
$Result = (Get-250Items |
Write-PipedProgress -PropertyName Name -Activity "Audit services" -ExpectedCount 250 |
Process-ItemFurther)
>
[cmdletBinding()]
param(
[parameter(Mandatory=$true,ValueFromPipeline=$true)]
$Data,
[string]$PropertyName=$null,
[string]$Activity,
[int]$ExpectedCount=100
)
begin {
Write-Verbose "Starting $($MyInvocation.MyCommand)"
$ItemCounter = 0
}
process {
Write-Verbose "Start processing of $($MyInvocation.MyCommand)($Data)"
try {
$ItemCounter++
# (3) mitigate unexpected additional input volume"
if ($ItemCounter -lt $ExpectedCount) {
$StatusProperty = if ($propertyName) { $Data.$PropertyName } > > else { ""}
$StatusMessage = "Processing $ItemCounter th $StatusProperty"
$statusPercent = 100 * $ItemCounter / $ExpectedCount
Write-Progress -Activity $Activity -Status $StatusMessage -> > PercentComplete $statusPercent
} else {
Write-Progress -Activity $Activity -Status "taking longer than expected" -PercentComplete 99
}
# return input data to next element in pipe
$Data
} catch {
throw
}
finally {
Write-Verbose "Complete processing of $Data in > $($MyInvocation.MyCommand)"
}
}
end {
Write-Progress -Activity $Activity -Completed
Write-Verbose "Complete $($MyInvocation.MyCommand) - processed $ItemCounter items"
}
}
Hope this helps ;-)
I believe this would work, at least in osx or linux powershell (or even Windows Subsystem for Linux) that have these commands available. I tested it with "ls" instead of "flutter". Is there actually an "out" command?
$OUTPUT = bash -c 'flutter build ios --release --no-codesign | tee /dev/tty | tail -1'
Or, assuming tee isn't aliased to tee-object. Actually, tee-object would work too.
$OUTPUT = flutter build ios --release --no-codesign | tee /dev/tty | tail -1
It would work with the $( ) too, but you don't need it. In powershell, it's used to combine multiple pipelines.

Error in PowerShell one liner for browser process management

I'm trying to run a code in PowerShell in one line. This code is a loop that's used for surveillance. If Microsoft Edge is opened the process has to close Chrome.
My code it works well if Edge is not opened, it goes right by the if condition, but if Edge is opened it returns me an error in the else condition.
System is Windows 10 with PowerShell ISE.
$a = 1 ;DO { 'Starting Loop' ; $vischk = get-process | where-object {$_.mainwindowhandle -ne 0 -and $_.MainWindowTitle -eq 'Start - Microsoft Edge'} | select-object name, mainwindowtitle ; if (!($vischk)) {Write-Warning 'Microsoft Edge is off'}else{Write-Warning 'Closing Chrome' Stop-Process -name chrome} ; Write-Warning 'Active surveillance' ; Start-Sleep -s 15} While ($a -le 2)
I need to run the surveillance window and when Microsoft Edge is opened, close another browser like chrome or another process name.
This is also not a One-Liner, it's a long script all put on one line. ;-},
hence your use of the semicolon. Semicolon means what is before it and after it
are independent code blocks.
I get that use case in an interactive consolehost thing, but in a script, well,
that's an entirely different thing.
Yet, doing this. the way you have it is a choice. Dev in the ISE, save the file,
run from the console directly or shell out to it from the ISE.
Also the way you are checking for the MS Edge instance is not correct. The
MainWindowTitle is only 'Microsoft Edge'
Get-Process -Name MicrosoftEdge | select *
<#
Name : MicrosoftEdge
Id : 6388
PriorityClass : Normal
...
ProductVersion : 11.00.17763.529
Description : Microsoft Edge
Product : Microsoft Edge
__NounName : Process
...
SafeHandle : Microsoft.Win32.SafeHandles.SafeProcessHandle
MachineName : .
MainWindowHandle : 132100
MainWindowTitle : Microsoft Edge
MainModule : System.Diagnostics.ProcessModule (MicrosoftEdge.exe)
MaxWorkingSet : 1413120
MinWorkingSet : 204800
Modules : {System....
#>
$Code = #'
$a = 1
DO {
'Starting Loop'
$vischk = get-process |
where-object {
$_.mainwindowhandle -ne 0 -and
$_.MainWindowTitle -eq 'Microsoft Edge'
} | select-object name, mainwindowtitle
if (!($vischk))
{Write-Warning 'Microsoft Edge is off'}
else{
Write-Warning 'Closing Chrome'
Stop-Process -name chrome
}
Write-Warning 'Active surveillance'
Start-Sleep -s 3}
While ($a -le 2)
'#
You can stay in the ISE to test code, but as Olaf points out, your user may not be the same. So, you need to validate both environments. You can stay in the ISE and test your code there as well as in the consolehost.
So, to test code using a consolehost instance from the ISE/VSCode without typing in the console.
Start-Process powershell -ArgumentList "-NoExit","-Command &{ $Code }" -Wait
Or
Start-Process pwsh -ArgumentList "-NoExit","-Command &{ $Code }" -Wait
Or just open the consolehost and run the script
The above works, as designed based on your defined case.
I agree with Olaf here as well, you need to add more error checking for what is and is not running for this to be more operationally sound. Don't run code you don't need to run if a target does not exist.

redirect stdout, stderr from powershell script as admin through start-process

Inside a powershell script, I'm running a command which starts a new powershell as admin (if I'm not and if needed, depending on $arg) and then runs the script.
I'm trying to redirect stdout and stderr to the first terminal.
Not trying to make things easier, there are arguments too.
param([string]$arg="help")
if($arg -eq "start" -Or $arg -eq "stop")
{
if(![bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544"))
{
Start-Process powershell -Verb runas -ArgumentList " -file servicemssql.ps1 $arg"
exit
}
}
$Services = "MSSQLSERVER", "SQLSERVERAGENT", "MSSQLServerOLAPService", "SSASTELEMETRY", "SQLBrowser", `
"SQLTELEMETRY", "MSSQLLaunchpad", "SQLWriter", "MSSQLFDLauncher"
function startsql {
"starting SQL services"
Foreach ($s in $Services) {
"starting $s"
Start-Service -Name "$s"
}
}
function stopsql {
"stopping SQL services"
Foreach ($s in $Services) {
"stopping $s"
Stop-Service -Force -Name "$s"
}
}
function statussql {
"getting SQL services status"
Foreach ($s in $Services) {
Get-Service -Name "$s"
}
}
function help {
"usage: StartMssql [status|start|stop]"
}
Switch ($arg) {
"start" { startsql }
"stop" { stopsql }
"status" { statussql }
"help" { help }
"h" { help }
}
Using the following answers on SO doesn't work:
Capturing standard out and error with Start-Process
Powershell: Capturing standard out and error with Process object
How to deal with the double quote inside double quote while preserving the variable ($arg) expansion ?
PowerShell's Start-Process cmdlet:
does have -RedirectStandardOut and -RedirectStandardError parameters,
but syntactically they cannot be combined with -Verb Runas, the argument required to start a process elevated (with administrative privileges).
This constraint is also reflected in the underlying .NET API, where setting the .UseShellExecute property on a System.Diagnostics.ProcessStartInfo instance to true - the prerequisite for being able to use .Verb = "RunAs" in order to run elevated - means that you cannot use the .RedirectStandardOutput and .RedirectStandardError properties.
Overall, this suggests that you cannot directly capture an elevated process' output streams from a non-elevated process.
A pure PowerShell workaround is not trivial:
param([string] $arg='help')
if ($arg -in 'start', 'stop') {
if (-not (([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))) {
# Invoke the script via -Command rather than -File, so that
# a redirection can be specified.
$passThruArgs = '-command', '&', 'servicemssql.ps1', $arg, '*>', "`"$PSScriptRoot\out.txt`""
Start-Process powershell -Wait -Verb RunAs -ArgumentList $passThruArgs
# Retrieve the captured output streams here:
Get-Content "$PSScriptRoot\out.txt"
exit
}
}
# ...
Instead of -File, -Command is used to invoke the script, because that allows appending a redirection to the command: *> redirects all output streams.
#soleil suggests using Tee-Object as an alternative so that the output produced by the elevated process is not only captured, but also printed to the (invariably new window's) console as it is being produced:
..., $arg, '|', 'Tee-Object', '-FilePath', "`"$PSScriptRoot\out.txt`""
Caveat: While it doesn't make a difference in this simple case, it's important to know that arguments are parsed differently between -File and -Command modes; in a nutshell, with -File, the arguments following the script name are treated as literals, whereas the arguments following -Command form a command that is evaluated according to normal PowerShell rules in the target session, which has implications for escaping, for instance; notably, values with embedded spaces must be surrounded with quotes as part of the value.
The $PSScriptRoot\ path component in output-capture file $PSScriptRoot\out.txt ensures that the file is created in the same folder as the calling script (elevated processes default to $env:SystemRoot\System32 as the working dir.)
Similarly, this means that script file servicemssql.ps1, if it is invoked without a path component, must be in one of the directories listed in $env:PATH in order for the elevated PowerShell instance to find it; otherwise, a full path is also required, such as $PSScriptRoot\servicemssql.ps1.
-Wait ensures that control doesn't return until the elevated process has exited, at which point file $PSScriptRoot\out.txt can be examined.
As for the follow-up question:
To go even further, could we have a way to have the admin shell running non visible, and read the file as we go with the Unix equivalent of tail -f from the non -privileged shell ?
It is possible to run the elevated process itself invisibly, but note that you'll still get the UAC confirmation prompt. (If you were to turn UAC off (not recommended), you could use Start-Process -NoNewWindow to run the process in the same window.)
To also monitor output as it is being produced, tail -f-style, a PowerShell-only solution is both nontrivial and not the most efficient; to wit:
param([string]$arg='help')
if ($arg -in 'start', 'stop') {
if (-not (([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))) {
# Delete any old capture file.
$captureFile = "$PSScriptRoot\out.txt"
Remove-Item -ErrorAction Ignore $captureFile
# Start the elevated process *hidden and asynchronously*, passing
# a [System.Diagnostics.Process] instance representing the new process out, which can be used
# to monitor the process
$passThruArgs = '-noprofile', '-command', '&', "servicemssql.ps1", $arg, '*>', $captureFile
$ps = Start-Process powershell -WindowStyle Hidden -PassThru -Verb RunAs -ArgumentList $passThruArgs
# Wait for the capture file to appear, so we can start
# "tailing" it.
While (-not $ps.HasExited -and -not (Test-Path -LiteralPath $captureFile)) {
Start-Sleep -Milliseconds 100
}
# Start an aux. background that removes the capture file when the elevated
# process exits. This will make Get-Content -Wait below stop waiting.
$jb = Start-Job {
# Wait for the process to exit.
# Note: $using:ps cannot be used directly, because, due to
# serialization/deserialization, it is not a live object.
$ps = (Get-Process -Id $using:ps.Id)
while (-not $ps.HasExited) { Start-Sleep -Milliseconds 100 }
# Get-Content -Wait only checks once every second, so we must make
# sure that it has seen the latest content before we delete the file.
Start-Sleep -Milliseconds 1100
# Delete the file, which will make Get-Content -Wait exit (with an error).
Remove-Item -LiteralPath $using:captureFile
}
# Output the content of $captureFile and wait for new content to appear
# (-Wait), similar to tail -f.
# `-OutVariable capturedLines` collects all output in
# variable $capturedLines for later inspection.
Get-Content -ErrorAction SilentlyContinue -Wait -OutVariable capturedLines -LiteralPath $captureFile
Remove-Job -Force $jb # Remove the aux. job
Write-Verbose -Verbose "$($capturedLines.Count) line(s) captured."
exit
}
}
# ...