I am looking for a way to programmatically determine if PowerShell wishes to make changes using the whatif flag. I have a set of scripts that implement the ShouldProcess, whatif only returns output if the script determines something is not in the desired state.
I would like PowerShell to alert me if whatif would like to make changes so a human can review the plan. However after reading the documentation I cannot find any suggestion that whatif returns if it wishes to make changes or not.
Any help would be appreciated.
It's a bit hacky, but you could execute the command in a non-interactive context with -Confirm - if it throws any exceptions indicating ShouldProcess couldn't be called, you'll know you reached a potentially destructive code path:
# Create non-interactive sandbox for executing with `-Confirm`
$sandbox = [PowerShell]::Create().AddScript({Verb-Noun -Confirm})
$null = $sandbox.Invoke()
if($sandbox.HadErrors){
if($sandbox.Streams.Error.Where({$_.Exception -is [System.Management.Automation.MethodInvocationException] -and $_.Exception.Message -match '*"ShouldProcess"*'})){
# Code reached ShouldProcess
}
}
Related
In a PowerShell (5.1 and later) Script Module, I want to ensure that every Script and System exception which is thrown calls a logging Cmdlet (e.g. Write-Log). I know that I can wrap all code within the module Cmdlets into try/catch blocks, but my preferred solution would be to use trap for all exceptions thrown while executing Cmdlets of the Module
trap { Write-Log -Level Critical -ErrorRecord $_ }
Using the above statement works as intended if I add it to each Cmdlet inside the module, but I would like to only have one trap statement which catches all exceptions thrown by Cmdlets of the Module to not replicate code and also ensure that I do not miss the statement in any Cmdlet. Is this possible?
What I would do is this.
Set multiple Try/Catch block as needed.
Group multiple cmdlet calls under the same block when you can. As you mentionned, we don't want to group everything under 1 giant try/catch block but still, related calls can go together.
Design your in-module functions as Advanced functions, so you can make use of the common parameters, such as... -ErrorAction
Set $PSDefaultParameterValues = #{'*:ErrorAction'='Stop'} so all cmdlets that support -ErrorAction don't fall through the try/catch block.
(You could also manually set -ErrorAction Stop everywhere but since you want this as default, it make sense to do it that way. In any cases You don't want to touch $ErrorActionPreference as this has a global scope amd your users won't like you if you change defaults outside of the module scope.)
You can also redirect the error stream to a file so instead of showing up in the output, it is written to a file.
Here's a self contained example of this:
& {
Write-Warning "hello"
Write-Error "hello"
Write-Output "hi"
} 2>> 'C:\Temp\redirection.log'
See : About_Redirection for more on this.
(Now I am wondering if you can redirect the stream to something else than a file
Additional note
External modules can help with logging too and might provide a more streamlined approach.
I am not familiar with any of them though.
I know PSFramework have some interesting stuff regarding logging.
You can take a look and experiment to see if fit your needs.
Otherwise, you can do a research on PSGallery for logging modules
(this research is far from perfect but some candidates might be interesting)
find-module *logging* | Select Name, Description, PublishedDate,Projecturi | Sort PublishedDate -Descending
I have a PowerShell script which does a bunch of stuff (Checks some config out from Git, compares a few files, works out whether to create a package, uploads it, and then cleans up after itself).
I want to support WhatIf functionality - but only for the main part of the functionality - the script needs to create the temp folder, check out the config, and clean up after itself. I just want the WhatIf to affect whether it does the package creation/upload.
However, if I put [CmdletBinding(SupportsShouldProcess)] into my function's header then it means that every cmdlet call that supports WhatIf then doesn't run (and this happens all the way down the call chain.
Is there any way of saying "Run everything except for the code which I've wrapped in pscmdlet.ShouldProcess" ?
In order to make it work the way you expect it to you have to understand how both parameters affect preference variables inside scope of your function.
Running advanced function with -WhatIf will set $WhatIfPreference to $true - to avoid changing behaviour of commands you need it to set it back to $false, or run all commands that SupportShouldProcess with -WhatIf:$false.
Running advanced function with -Confirm will set $ConfirmPreference to 'Low' (so anything that SupportsShouldProcess will prompt) - to avoid changing behaviour of commands you need to set it back to 'High', or run any command that SupportsShouldProcess with -Confirm:$false.
Example code that cleans both flags (and prompts/ returns WhatIf message only when you like it to do that):
function Invoke-WhatIf {
[CmdletBinding(
SupportsShouldProcess
)]
param ()
$WhatIfPreference = $false
$ConfirmPreference = 'High'
$null = New-Item -Path $env:TEMP\so.tmp -Force
if ($PSCmdlet.ShouldProcess('whatIf message','confirm message','confirmCaption')) {
Remove-Item $env:TEMP\so.tmp -Force
}
}
Read more in about_Preference_Variables
I am calling an external .ps1 file which contains a break statement in certain error conditions. I would like to somehow catch this scenario, allow any externally printed messages to show as normal, and continue on with subsequent statements in my script. If the external script has a throw, this works fine using try/catch. Even with trap in my file, I cannot stop my script from terminating.
For answering this question, assume that the source code of the external .ps1 file (authored by someone else and pulled in at run time) cannot be changed.
Is what I want possible, or was the author of the script just not thinking about playing nice when called externally?
Edit: providing the following example.
In badscript.ps1:
if((Get-Date).DayOfWeek -ne "Yesterday"){
Write-Warning "Sorry, you can only run this script yesterday."
break
}
In myscript.ps1:
.\badscript.ps1
Write-Host "It is today."
The results I would like to achieve is to see the warning from badscript.ps1 and for it to continue on with my further statements in myscript.ps1. I understand why the break statement causes "It is today." to never be printed, however I wanted to find a way around it, as I am not the author of badscript.ps1.
Edit: Updating title from "powershell try/catch does not catch a break statement" to "how to prevent external script from terminating your script with break statement". The mention of try/catch was really more about one failed solution to the actual question which the new title better reflects.
Running a separate PowerShell process from within my script to invoke the external file has ended up being a solution good enough for my needs:
powershell -File .\badscript.ps1 will execute the contents of badscript.ps1 up until the break statement including any Write-Host or Write-Warning's and let my own script continue afterwards.
I get where you're coming from. Probably the easiest way would be to push the script off as a job, and wait for the results. You can even echo the results out with Receive-Job after it's done if you want.
So considering the bad script you have above, and this script file calling it:
$path = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$start = Start-Job -ScriptBlock { . "$using:Path\badScript.ps1" } -Name "BadScript"
$wait = Wait-Job -Name "BadScript" -Timeout 100
Receive-Job -Name "BadScript"
Get-Command -Name "Get-ChildItem"
This will execute the bad script in a job, wait for the results, echo the results, and then continue executing the script it's in.
This could be wrapped in a function for any scripts you might need to call (just to be on the safe side.
Here's the output:
WARNING: Sorry, you can only run this script yesterday.
CommandType Name Version Source
----------- ---- ------- ------
Cmdlet Get-ChildItem 3.1.0.0 Microsoft.PowerShell.Management
In the about_Break documentation it says
PowerShell does not limit how far labels can resume execution. The
label can even pass control across script and function call
boundaries.
This got me thinking, "How can I trick this stupid language design choice?". And the answer is to create a little switch block that will trap the break on the way out:
.\NaughtyBreak.ps1
Write-Host "NaughtyBreak about to break"
break
.\OuterScript.ps1
switch ('dummy') { default {.\NaughtyBreak.ps1}}
Write-Host "After switch() {NaughtyBreak}"
.\NaughtyBreak.ps1
Write-Host "After plain NaughtyBreak"
Then when we call OuterScript.ps1 we get
NaughtyBreak about to break
After switch() {NaughtyBreak}
NaughtyBreak about to break
Notice that OuterScript.ps1 correctly resumed after the call to NaughtyBreak.ps1 embedded in the switch, but was unceremoniously killed when calling NaughtyBreak.ps1 directly.
Putting break back inside a loop (including switch) where it belongs.
foreach($i in 1) { ./badscript.ps1 }
'done'
Or
switch(1) { 1 { ./badscript.ps1 } }
'done'
Kind of like <statement> || die in perl, something concise that I can put with every critical statement to avoid bothering powershell with the rest of the script if something goes wrong.
Most commands support the -ErrorAction common parameter. Specifying -ErrorAction Stop will generally halt the script on an error. See Get-Help about_CommonParameters.
By default, -ErrorAction is Continue. You can change the default option by changing the value of $ErrorActionPreference. See Get-Help about_Preference_Variables.
If verbosity is really an issue, -ErrorAction is aliased to -ea.
Another way to implement a ...|| die-like construct in PowerShell without the need to add huge try-catch constructs, would be to use the automatic variable $?.
From Get-Help about_Automatic_variables:
$?
Contains the execution status of the last operation. It contains
TRUE if the last operation succeeded and FALSE if it failed.
Simply add the following right after each critical statement:
if(-not $?){
# Call Write-EventLog or write $Error[0] to an xml or txt file if you like
Exit(1)
}
I have written my own Powershell logging function Log with parameters stream (on which stream to write the message) and message (the message to write).
The idea is that i can write the outputs both to the console and to a log-file. What I do in the function is basically determine on which stream to publish the message (with a switch statement) and then write the message to the stream and the log-file:
switch ($stream) {
Verbose {
Write-Output "$logDate [VERBOSE] $message" | Out-File -FilePath $sgLogFileName -Append
Write-Verbose $message
break
}
}
The question is now, is it possible to check if the -Verbose argument was given?
The goal is to write the message to the log-file only if the -Verbose was given.
I looked already in the following help docs but didn't find anything helpful:
- help about_Parameters
- help about_commonparameters
Also, the -WhatIf parameter does not work with Write-Verbose.
Thanks a lot for your answers!
Inside your script check this:
$PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent
Also available: Check the parameter '$VerbosePreference'. If it is set to 'SilentlyContinue' then $Verbose was not given at the command line. If it is set to '$Continue' then you can assume it was set.
Also applies to the following other common parameters:
Name Value
---- -----
DebugPreference SilentlyContinue
VerbosePreference SilentlyContinue
ProgressPreference Continue
ErrorActionPreference Continue
WhatIfPreference 0
WarningPreference Continue
ConfirmPreference High
Taken from an MSDN blog page from long ago... so it should be relevant with relatively old versions of Powershell. Also see "Get-Help about_CommonParameters" in Powershell v4.
More generally: since one might specify -Verbose:$false on the command line, the following code handles that case. It also works for any other switch parameter:
$Verbose = $false
if ($PSBoundParameters.ContainsKey('Verbose')) { # Command line specifies -Verbose[:$false]
$Verbose = $PsBoundParameters.Get_Item('Verbose')
}
Came across this looking for the same answer and found some good info, also some not so good.
The marked answer seems to be dated and not correct as the comments stated. The PSBoundParameter property object from the MyInvocation object is a Dictionary (PoSH 5.1 up maybe earlier didn't check) which does not contain the IsPresent property. The asker also forgot to consider $VerbosePreference where other answers have presented this option.
Here's a solution that makes it simple and easy:
if ( $PSBoundParameters['Verbose'] -or $VerbosePreference -eq 'Continue' ) {
# do something
}
$PSBoundParameters is a hashtable object, if the value is present and true it will evaluate to true, if it is not present or present and not true it will evaluate to false. VerbosePreference which is set at the session level will display verbose statements when the value is Continue. Put this together in a condition using a logical or and you will get an accurate representation if verbose output is desired.
If you're determining whether or not to print depending on the value of the -Verbose parameter, consider using Write-Verbose instead of Write-Host: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-verbose?view=powershell-7.1