I am using PowerShell 2.0 to write data to PowerShell's debug stream via the Write-Debug function. Now I want to read that stream from the same PowerShell script. I tried to redirect the debug stream with "2>&1", but this works only for the error stream.
Is there a way to read the PowerShell debug stream from a PowerShell script?
You can also experiment with your function that will be called instead of cmdlet Write-Debug. Here is a very quick implementation:
$global:__DebugInfo = new-object PSObject -prop #{
Enabled=$false
Messages=new-object System.Collections.ArrayList
}
function Write-Debug {
param([Parameter(Mandatory=$true)][string]$Message)
$d = Get-Command Write-Debug -CommandType cmdlet; & $d $Message;
if ($global:__DebugInfo.Enabled) {
$global:__DebugInfo.Messages.Add($Message) > $null
}
}
function Enable-Debug {
$global:DebugPreference = 'continue'
$global:__DebugInfo.Enabled = $true
}
function Disable-Debug {
$global:DebugPreference = 'silentlycontinue'
$global:__DebugInfo.Enabled = $false
}
# Test
Enable-Debug
Write-Debug 'this is test debug message'
Write-Debug 'switch off'
Disable-Debug
Write-Debug 'this message should not be included'
Write-Host "Debug messages:"
Write-Host ($__DebugInfo.Messages -join "`n")
It can be done but it is complex. See Oisin's writeup on script logging. In general, this issue of being able to redirect streams other than stdout and stderr has been logged as a suggestion on the connect site. You might want to vote on it.
Related
I am passing an argument to my test application via powershell. I wish to get the return value of the application once it finishes.
How can I get the return value of the application instead of the console output.
I'm running the application by running
Test.ps1 2
Test.ps1
param ([string]$param1)
$path = "C:\Workspaces\myapplication\"
$executable = "Test.exe"
$filepath = "$($path)$($executable) $($param1)"
Try
{
$Result = iex $filepath
#this writes out the console output of Test.exe instead of the return value.
Write-Host $Result
Write-Host $LASTEXITCODE
}
Catch
{
Write-Host "Exit Code"
Write-Host $LASTEXITCODE
}
you should use Start-Process:
$p = Start-Process $($path)$($executable) -ArgumentList $input
$p.HasExited
$p.ExitCode
I am writing a powershell script that is running on Linux. The purpose of this script is to run the MS SQL Server and show its output, but when the user presses Ctrl+C or some error happens, the script takes a copy of the data folder and then after that exits.
[CmdletBinding()]
PARAM (
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Command,
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Args
)
BEGIN {
Write-Output "started $PSScriptRoot"
$currentErrorLevel = $ErrorActionPreference
# [console]::TreatControlCAsInput = $true
}
PROCESS {
try {
$ErrorActionPreference = 'SilentlyContinue';
$IsSqlRunning = Get-Process -name "sqlservr";
if ($null -eq $IsSqlRunning) {
start-process "/opt/mssql/bin/sqlservr" -wait -NoNewWindow
}
}
catch {
$ErrorActionPreference = $currentErrorLevel;
Write-Error $_.Exception
}
}
End {
$ErrorActionPreference = $currentErrorLevel;
#do backup
Create-Backup "/opt/mssql/bin/data"
Write-Output "finishd!"
}
I got a couple of problems with this script:
When I press Ctrl+C it breaks the main script and it never reaches to the Create-Backup section at the bottom of the script.
If I remove -Wait then the script won't show the sql log output
So my prefered solution is to run the sql with -Wait parameter and prevent the powershell to exit the code after I press Ctrl+C, but instead Ctrl+C close the sql instance
So I'm looking for a way to achieve both.
For simplicity I'll assume that your function only needs to support a single input object, so I'm using a simple function body without begin, process and end blocks, which is equivalent to having just an end block:
[CmdletBinding()]
PARAM (
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Command,
[Parameter(ValueFromPipelineByPropertyName)]
[string] $Args
)
Write-Output "started $PSScriptRoot"
# No need to save the current value, because the modified
# value is local to the function and goes out of scope on
# exiting the function.
$ErrorActionPreference = 'SilentlyContinue';
try {
$IsSqlRunning = Get-Process -name "sqlservr";
if ($null -eq $IsSqlRunning) {
start-process "/opt/mssql/bin/sqlservr" -wait -NoNewWindow
}
}
catch {
Write-Error $_.Exception
}
finally {
# This block is *always* called - even if Ctrl-C was used.
Create-Backup "/opt/mssql/bin/data"
# CAVEAT: If Ctrl-C was used to terminate the run,
# you can no longer produce *pipeline* input at this point
# (it will be quietly ignored).
# However, you can still output to the *host*.
Write-Host "finished!"
}
If you really need to support multiple input objects, it gets more complicated.
I'm using PowerShell 5.1 and I am trying to determine why Write-Information messages do not show in the transcript log created by Start-Transcript unless I set $InformationPreference to SilentlyContinue. I want to both display the messages in the console and have them written to the log file.
I looked here:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-5.1#informationpreference
Then I decided to create this script to test what gets written and when. See the preference section right underneath Testing explicit behavior with transcripts -------------
Clear-Host
$ErrorActionPreference = "Stop"
try {
Write-Host "Starting transcript"
Start-Transcript -Force -Path "$PSScriptRoot\default.txt"
<#
In PowerShell 5.1 the default behavior is as follows:
$DebugPreference = SilentlyContinue
$InformationPreference = SilentlyContinue
$ProgressPreference = Continue
$VerbosePreference = SilentlyContinue
See the following for more information:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-5.1
#>
# I am not testing Write-Output as I am not worried about programmatic/pipeline stuff, just contextual messages for end-users or logging
Write-Host "`nTesting default behavior with transcripts --------------------------------`n"
# Setting these just in case I launch this script in a session where a previous script might have modified the preference variables
$DebugPreference = "SilentlyContinue"
$InformationPreference = "SilentlyContinue"
$ProgressPreference = "Continue"
$VerbosePreference = "SilentlyContinue"
Write-Host "Calling Write-Host"
Write-Debug "Calling Write-Debug"
Write-Error "Calling Write-Error" -ErrorAction "Continue"
Write-Information "Calling Write-Information"
Write-Progress "Calling Write-Progress"
Write-Verbose "Calling Write-Verbose"
Stop-Transcript
Start-Transcript -Force -Path "$PSScriptRoot\everything_continue.txt"
Write-Host "`nTesting explicit behavior with transcripts --------------------------------`n"
# Turn everything on
$DebugPreference = "Continue"
$InformationPreference = "Continue" # Setting this to SilentlyContinue makes it show up in the log but not the console. Setting this to 'Continue' makes it show up in the console but not the log.
$ProgressPreference = "Continue"
$VerbosePreference = "Continue"
Write-Host "Calling Write-Host"
Write-Debug "Calling Write-Debug"
Write-Error "Calling Write-Error" -ErrorAction "Continue"
Write-Information "Calling Write-Information"
Write-Progress "Calling Write-Progress"
Write-Verbose "Calling Write-Verbose"
Stop-Transcript
Write-Host "`nResults -------------------------------------------------------------------`n"
# See what actually gets captured and written by the transcriber
$messageTypes = #("Write-Debug", "Write-Error", "Write-Host", "Write-Information", "Write-Verbose")
Write-Host "Default" -ForegroundColor Cyan
$lines = Get-Content "$PSScriptRoot\default.txt"
foreach ($message in $messageTypes) {
if ($lines -like "*Calling $message*") {
Write-Host " $message PRESENT" -ForegroundColor Green
}
else {
Write-Host " $message MISSING" -ForegroundColor Red
}
}
Write-Host "Everything Continue" -ForegroundColor Cyan
$lines = Get-Content "$PSScriptRoot\everything_continue.txt"
foreach ($message in $messageTypes) {
if ($lines -like "*Calling $message*") {
Write-Host " $message PRESENT" -ForegroundColor Green
}
else {
Write-Host " $message MISSING" -ForegroundColor Red
}
}
}
catch {
Write-Host "----------------------------------------------------------------------------------------------------"
Write-Host $_.Exception
Write-Host $_.ScriptStackTrace
Write-Host "----------------------------------------------------------------------------------------------------"
try { Stop-Transcript } catch { }
throw $_
}
What you're seeing is a bug in Windows PowerShell (as of v5.1.17134.590) that has been fixed in PowerShell Core (as of at least v6.1.0 - though other transcript-related problems persist; see this GitHub issue).
I encourage you to report it in the Windows PowerShell UserVoice forum (note that the PowerShell GitHub-repo issues forum is only for errors also present in PowerShell Core).
Here's how to verify if the bug is present in your PowerShell version:
Create a script with the code below and run it:
'--- Direct output'
$null = Start-Transcript ($tempFile = [io.path]::GetTempFileName())
# Note that 'SilentlyContinue' is also the default value.
$InformationPreference = 'SilentlyContinue'
# Produces no output.
Write-Information '1-information'
# Prints '2-Information' to the console.
Write-Information '2-information' -InformationAction Continue
$null = Stop-Transcript
'--- Write-Information output transcribed:'
Select-String '-information' $tempFile | Select-Object -ExpandProperty Line
Remove-Item $tempFile
With the bug present (Windows PowerShell), you'll see:
--- Direct output
2-information
--- Write-Information output transcribed:
INFO: 1-information
That is, the opposite of the intended behavior occurred: the transcript logged the call it should'nt have (because it produced no output), and it didn't log the one it should have.
Additionally, the logged output is prefixed with INFO: , which is an inconsistency that has also been fixed in PowerShell Core.
There is no full workaround, except that you can use Write-Host calls in cases where do you want the output logged in the transcript - but such calls will be logged unconditionally, irrespective of the value of preference variable $InformationPreference (while Write-Host formally provides an -InformationAction common parameter, it is ignored).
With the bug fixed (PowerShell Core), you'll see:
--- Direct output
2-information
--- Write-Information output transcribed:
2-information
The transcript is now consistent with the direct output.
Is it possible to use the adavanced function features Begin, Process, End in a script-block?
For example I've the following script block:
$startStopService = {
Param(
[bool] $startService)
if ($startService){
...
Start-Service "My-Service"
}
else {
Stop-Service "My-Service"
}
}
Since I want to be able to control the verbose output of the scriptblock I want to change the block to:
$startStopService = {
Param(
[bool] $startService)
Begin {
$oldPreference = $VerbosePreference
$VerbosePreference = $Using:VerbosePreference
}
Process {
if ($startService){
...
Start-Service "My-Service"
}
else {
Stop-Service "My-Service"
}
}
End {
# Restore the old preference
$VerbosePreference = $oldPreference
}
}
Is it possible to use Begin, Process, End here, though the scriptblock isn't a cmdlet? I simply want that the VerbosePreference gets restored to the old value, regardless an error occurred or not. Of course I could use try{}finally{} as an alternative, but I find that Begin, Process, End is more intuitive.
Thx
It is possible, as described in about_script_blocks:
Like functions, script blocks can include the DynamicParam, Begin,
Process, and End keywords. For more information, see about_Functions
and about_Functions_Advanced.
To test this out, I modified your scriptblock and ran this:
$startStopService = {
Param(
# a bool needs $true or $false passed AFAIK
# A switch is $true if specified, $false if not included
[switch] $startService
)
Begin {
$oldPreference = $VerbosePreference
Write-Output "Setting VerbosePreference to Continue"
# $Using:VerbosePreference gave me an error
$VerbosePreference = "Continue"
}
Process {
if ($startService){
Write-Verbose "Service was started"
}
else {
Write-Verbose "Service was not started"
}
}
End {
# Restore the old preference
Write-Output "Setting VerbosePreference back to $oldPreference"
$VerbosePreference = $oldPreference
}
}
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
. $startStopService -startService
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
What functionality are you after? If you would to print verbose messages when running a scriptblock but not change the $VerbosePreference in the rest of the script , consider using [CmdletBinding()] and the -Verbose flag:
$startStopService = {
[CmdLetBinding()]
Param(
[switch] $startService
)
Write-Verbose "This is a verbose message"
}
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
. $startStopService -verbose
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
Edit - Invoke-Command
After your comment, I looking into the functionality of Invoke-Command. And found a lot of things that don't work.
The short version that I believe is most useful to you: you can declare $VerbosePreference = "Continue" within a scriptblock and this will be limited to the scope of the scriptblock. No need to change back after.
$startStopService = {
[CmdLetBinding()]
Param(
[parameter(Position=0)]
[switch]$startStopService,
[parameter(Position=1)]
[switch]$Verbose
)
if($Verbose){
$VerbosePreference = "Continue"
}
Write-Verbose "This is a verbose message"
}
Write-output "VerbosePreference: $VerbosePreference"
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
Invoke-Command -Scriptblock $startStopService -ArgumentList ($true,$true)
Write-output "VerbosePreference: $VerbosePreference"
Write-Verbose "This message will not print if VerbosePreference is the default SilentlyContinue"
Trying to pass the -Verbose switch CommonParameter to Invoke-Command was a no-go. This uses a standard Verbose switch parameter that allows you to pass $true/$false (or omit) to control the verbose output.
Related:
about_Functions
about_Functions_Advanced
I'm writing a script to download several repositories from GitHub. Here is the command to download a repository:
git clone "$RepositoryUrl" "$localRepoDirectory"
When I run this command it displays some nice progress information in the console window that I want displayed.
The problem is that I also want to be able to detect if any errors have occurred while downloading. I found this post that talks about redirecting the various streams, so I tried:
(git clone "$RepositoryUrl" "$localRepoDirectory") 2> $errorLogFilePath
This pipes any errors from stderr to my file, but no longer displays the nice progress information in the console.
I can use the Tee-Object like so:
(git clone "$RepositoryUrl" "$localRepoDirectory") | Tee-Object -FilePath $errorLogFilePath
and I still get the nice progress output, but this pipes stdout to the file, not stderr; I'm only concerned with detecting errors.
Is there a way that I can store any errors that occur in a file or (preferably) a variable, while also still having the progress information piped to the console window? I have a feeling that the answer might lie in redirecting various streams into other streams as discusses in this post, but I'm not really sure.
======== Update =======
I'm not sure if the git.exe is different than your typical executable, but I've done some more testing and here is what I've found:
$output = (git clone "$RepositoryUrl" "$localRepoDirectory")
$output always contains the text "Cloning into '[localRepoDirectory]'...", whether the command completed successfully or produced an error. Also, the progress information is still written to the console when doing this. This leads me to think that the progress information is not written via stdout, but by some other stream?
If an error occurs the error is written to the console, but in the usual white foreground color, not the typical red for errors and yellow for warnings. When this is called from within a cmdlet function and the command fails with an error, the error is NOT returned via the function's -ErrorVariable (or -WarningVariable) parameter (however if I do my own Write-Error that does get returned via -ErrorVariable). This leads me to think that git.exe doesn't write to stderr, but when we do:
(git clone "$RepositoryUrl" "$localRepoDirectory") 2> $errorLogFilePath
The error message is written to the file, so that makes me think that it does write to stderr. So now I'm confused...
======== Update 2 =======
So with Byron's help I've tried a couple of more solutions using a new process, but still can't get what I want. When using a new process I never get the nice progress written to the console.
The three new methods that I've tried both use this bit of code in common:
$process = New-Object System.Diagnostics.Process
$process.StartInfo.Arguments = "clone ""$RepositoryUrl"" ""$localRepoDirectory"""
$process.StartInfo.UseShellExecute = $false
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.RedirectStandardError = $true
$process.StartInfo.CreateNoWindow = $true
$process.StartInfo.WorkingDirectory = $WORKING_DIRECTORY
$process.StartInfo.FileName = "git"
Method 1 - Run in new process and read output afterwards:
$process.Start()
$process.WaitForExit()
Write-Host Output - $process.StandardOutput.ReadToEnd()
Write-Host Errors - $process.StandardError.ReadToEnd()
Method 2 - Get output synchronously:
$process.Start()
while (!$process.HasExited)
{
Write-Host Output - $process.StandardOutput.ReadToEnd()
Write-Host Error Output - $process.StandardError.ReadToEnd()
Start-Sleep -Seconds 1
}
Even though this looks like it would write the output while the process is running, it doesn't write anything until after the process exits.
Method 3 - Get output asynchronously:
Register-ObjectEvent -InputObject $process -EventName "OutputDataReceived" -Action {Write-Host Output Data - $args[1].Data }
Register-ObjectEvent -InputObject $process -EventName "ErrorDataReceived" -Action { Write-Host Error Data - $args[1].Data }
$process.Start()
$process.BeginOutputReadLine()
$process.BeginErrorReadLine()
while (!$process.HasExited)
{
Start-Sleep -Seconds 1
}
This does output data while the process is working which is good, but it still doesn't display the nice progress information :(
I think I have your answer. I'm working with PowerShell for a while and created several build systems. Sorry if the script is a bit long, but it works.
$dir = <your dir>
$global:log = <your log file which must be in the global scope> # Not global = won't work
function Create-Process {
$process = New-Object -TypeName System.Diagnostics.Process
$process.StartInfo.CreateNoWindow = $false
$process.StartInfo.RedirectStandardError = $true
$process.StartInfo.UseShellExecute = $false
return $process
}
function Terminate-Process {
param([System.Diagnostics.Process]$process)
$code = $process.ExitCode
$process.Close()
$process.Dispose()
Remove-Variable process
return $code
}
function Launch-Process {
param([System.Diagnostics.Process]$process, [string]$log, [int]$timeout = 0)
$errorjob = Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -SourceIdentifier Common.LaunchProcess.Error -action {
if(-not [string]::IsNullOrEmpty($EventArgs.data)) {
"ERROR - $($EventArgs.data)" | Out-File $log -Encoding ASCII -Append
Write-Host "ERROR - $($EventArgs.data)"
}
}
$outputjob = Register-ObjectEvent -InputObject $process -EventName OutputDataReceived -SourceIdentifier Common.LaunchProcess.Output -action {
if(-not [string]::IsNullOrEmpty($EventArgs.data)) {
"Out - $($EventArgs.data)" | Out-File $log -Encoding ASCII -Append
Write-Host "Out - $($EventArgs.data)"
}
}
if($errorjob -eq $null) {
"ERROR - The error job is null" | Out-File $log -Encoding ASCII -Append
Write-Host "ERROR - The error job is null"
}
if($outputjob -eq $null) {
"ERROR - The output job is null" | Out-File $log -Encoding ASCII -Append
Write-Host "ERROR - The output job is null"
}
$process.Start()
$process.BeginErrorReadLine()
if($process.StartInfo.RedirectStandardOutput) {
$process.BeginOutputReadLine()
}
$ret = $null
if($timeout -eq 0)
{
$process.WaitForExit()
$ret = $true
}
else
{
if(-not($process.WaitForExit($timeout)))
{
Write-Host "ERROR - The process is not completed, after the specified timeout: $($timeout)"
$ret = $false
}
else
{
$ret = $true
}
}
# Cancel the event registrations
Remove-Event * -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier Common.LaunchProcess.Error
Unregister-Event -SourceIdentifier Common.LaunchProcess.Output
Stop-Job $errorjob.Id
Remove-Job $errorjob.Id
Stop-Job $outputjob.Id
Remove-Job $outputjob.Id
$ret
}
$repo = <your repo>
$process = Create-Process
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.FileName = "git.exe"
$process.StartInfo.Arguments = "clone $($repo)"
$process.StartInfo.WorkingDirectory = $dir
Launch-Process $process $global:log
Terminate-Process $process
The log file must be in the global scope because the routine which runs the event processing is not in the script scope.
Sample of my log file:
Out - Cloning into ''...
ERROR - Checking out files: 22% (666/2971)
ERROR - Checking out files: 23% (684/2971)
ERROR - Checking out files: 24% (714/2971)
You can do this by putting the git clone command inside an advanced function e.g.:
function Clone-Git {
[CmdletBinding()]
param($repoUrl, $localRepoDir)
git clone $repoUrl $localRepoDir
}
Clone-Git $RepositoryUrl $localRepoDirectory -ev cloneErrors
$cloneErrors
If you use System.Diagnostics.Process to start Git, you can redirect all the error and output.
I just had to solve this problem for Inkscape:
$si = New-Object System.Diagnostics.ProcessStartInfo
$si.Arguments = YOUR PROCESS ARGS
$si.UseShellExecute = $false
$si.RedirectStandardOutput = $true
$si.RedirectStandardError = $true
$si.WorkingDirectory = $workingDir
$si.FileName = EXECUTABLE LOCATION
$process = [Diagnostics.Process]::Start($si)
while (!($process.HasExited))
{
// Do what you want with strerr and stdout
Start-Sleep -s 1 // Sleep for 1 second
}
You can, of course, wrap this in a function with proper arguments...