How to properly use the -verbose and -debug parameters in a custom cmdlet - powershell

By default, any named function that has the [CmdletBinding()] attribute accepts the -debug and -verbose (and a few others) parameters and has the predefined $debug and $verbose variables. I'm trying to figure out how to pass them on to other cmdlet's that get called within the function.
Let's say I have a cmdlet like this:
function DoStuff() {
[CmdletBinding()]
PROCESS {
new-item Test -type Directory
}
}
If -debug or -verbose was passed into my function, I want to pass that flag into the new-item cmdlet. What's the right pattern for doing this?

$PSBoundParameters isn't what you're looking for. The use of the [CmdletBinding()] attribute allows the usage of $PSCmdlet within your script, in addition to providing a Verbose flag. It is in fact this same Verbose that you're supposed to use.
Through [CmdletBinding()], you can access the bound parameters through $PSCmdlet.MyInvocation.BoundParameters. Here's a function that uses CmdletBinding and simply enters a nested prompt immediately in order examine the variables available inside the function scope.
PS D:\> function hi { [CmdletBinding()]param([string] $Salutation) $host.EnterNestedPrompt() }; hi -Salutation Yo -Verbose
PS D:\>>> $PSBoundParameters
____________________________________________________________________________________________________
PS D:\>>> $PSCmdlet.MyInvocation.BoundParameters
Key Value
--- -----
Salutation Yo
Verbose True
So in your example, you would want the following:
function DoStuff `
{
[CmdletBinding()]
param ()
process
{
new-item Test -type Directory `
-Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
}
}
This covers -Verbose, -Verbose:$false, -Verbose:$true, and the case where the switch is not present at all.

Perhaps it sounds strange, but there isn't any easy way for a cmdlet to know its verbose or debug mode. Take a look at the related question:
How does a cmdlet know when it really should call WriteVerbose()?
One not perfect, but practically reasonable, option is to introduce your own cmdlet parameters (for example, $MyVerbose and $MyDebug) and use them in the code explicitly:
function DoStuff {
[CmdletBinding()]
param
(
# Unfortunately, we cannot use Verbose name with CmdletBinding
[switch]$MyVerbose
)
process {
if ($MyVerbose) {
# Do verbose stuff
}
# Pass $MyVerbose in the cmdlet explicitly
New-Item Test -Type Directory -Verbose:$MyVerbose
}
}
DoStuff -MyVerbose
UPDATE
When we need only a switch (not, say, a verbosity level value) then the approach with $PSBoundParameters is perhaps better than proposed in the first part of this answer (with extra parameters):
function DoStuff {
[CmdletBinding()]
param()
process {
if ($PSBoundParameters['Verbose']) {
# Do verbose stuff
}
New-Item Test -Type Directory -Verbose:($PSBoundParameters['Verbose'] -eq $true)
}
}
DoStuff -Verbose
It's all not perfect anyway. If there are better solutions then I would really like to know them myself.

There is no need. PowerShell already does this as the code below proves.
function f { [cmdletbinding()]Param()
"f is called"
Write-Debug Debug
Write-Verbose Verbose
}
function g { [cmdletbinding()]Param()
"g is called"
f
}
g -Debug -Verbose
The output is
g is called
f is called
DEBUG: Debug
VERBOSE: Verbose
It is not done as direct as passing -Debug to the next cmdlet though. It is done through the $DebugPreference and $VerbrosePreference variables. Write-Debug and Write-Verbose act like you would expect, but if you want to do something different with debug or verbose you can read here how to check for yourself.

Here's my solution:
function DoStuff {
[CmdletBinding()]
param ()
BEGIN
{
$CMDOUT = #{
Verbose = If ($PSBoundParameters.Verbose -eq $true) { $true } else { $false };
Debug = If ($PSBoundParameters.Debug -eq $true) { $true } else { $false }
}
} # BEGIN ENDS
PROCESS
{
New-Item Example -ItemType Directory #CMDOUT
} # PROCESS ENDS
END
{
} #END ENDS
}
What this does different from the other examples is that it will repsect "-Verbose:$false" or "-Debug:$false". It will only set -Verbose/-Debug to $true if you use the following:
DoStuff -Verbose
DoStuff -Verbose:$true
DoStuff -Debug
DoStuff -Debug:$true

You could build a new hash table based on the bound debug or verbose parameters and then splat it to the internal command. If you're just specifying switches (and aren't passing a false switch, like $debug:$false) you can just check for the existence of debug or verbose:
function DoStuff() {
[CmdletBinding()]
PROCESS {
$HT=#{Verbose=$PSBoundParameters.ContainsKey'Verbose');Debug=$PSBoundParameters.ContainsKey('Debug')}
new-item Test -type Directory #HT
}
}
If you want to pass the parameter value it's more complicated, but can be done with:
function DoStuff {
[CmdletBinding()]
param()
PROCESS {
$v,$d = $null
if(!$PSBoundParameters.TryGetValue('Verbose',[ref]$v)){$v=$false}
if(!$PSBoundParameters.TryGetValue('Debug',[ref]$d)){$d=$false}
$HT=#{Verbose=$v;Debug=$d}
new-item Test -type Directory #HT
}
}

The best way to do it is by setting the $VerbosePreference. This will enable the verbose level for the entire script. Do not forget to disable it by the end of the script.
Function test
{
[CmdletBinding()]
param($param1)
if ($psBoundParameters['verbose'])
{
$VerbosePreference = "Continue"
Write-Verbose " Verbose mode is on"
}
else
{
$VerbosePreference = "SilentlyContinue"
Write-Verbose " Verbose mode is Off"
}
# <Your code>
}

You can set the VerbosePreference as a global variable on starting your script and then check for the global variable in your custom cmdlet.
Script:
$global:VerbosePreference = $VerbosePreference
Your-CmdLet
Your-CmdLet:
if ($global:VerbosePreference -eq 'Continue') {
# verbose code
}
Checking explicitly for 'Continue' allows the script to be equal to -verbose:$false when you call the CmdLet from a script that doesn't set the global variable (in which case it's $null)

You do not have to do any checks or comparisons. Even though -Verbose (and -Debug) are of type [switch], they seem to understand not just $true and $false but also their preference variable. The preference variable also gets inherited correctly to all child functions that are called. I tried this on Powershell version 7.3.2 and it works as expected.
function Parent {
[CmdletBinding()]param()
Child
}
function Child {
[CmdletBinding()]param()
New-Item C:\TEST\SomeDir -Force -ItemType Directory -Verbose:$VerbosePreference -Debug:$DebugPreference
}
Parent -Verbose
Parent -Debug

I think this is the easiest way:
Function Test {
[CmdletBinding()]
Param (
[parameter(Mandatory=$False)]
[String]$Message
)
Write-Host "This is INFO message"
if ($PSBoundParameters.debug) {
Write-Host -fore cyan "This is DEBUG message"
}
if ($PSBoundParameters.verbose) {
Write-Host -fore green "This is VERBOSE message"
}
""
}
Test -Verbose -Debug

Related

PowerShell -Confirm only once per function call

I'm obviously trying to do something wrong, so hoping someone can help since it seems easy and it is probably an easy mistake. Basically I am writing a little script to create new folders and I want to include -Confirm, but I want it to basically ask ONCE if the user wants to continue, instead of asking for each nested folder that is created.
Here's what I have so far
function Create-NewFolderStructure{
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory)]
[string]$NewFolderName,
[Parameter()]
$LiteralPath = (Get-Location),
[Parameter()]
$inputFile = "zChildDirectoryNames.txt"
)
process {
$here = $LiteralPath
$directories = Get-Content -LiteralPath "$($here)\$($inputFile)"
If (-not (Test-Path -Path "$($here)\$($NewFolderName)")){
$directories | ForEach-Object {New-Item -ItemType "directory" -Path "$($here)\$($NewFolderName)" -Name $_}
}
Else {
Write-Host "A $($NewFolderName) folder already exists"
}
}
end {
}
}
The issue is that if I use -Confirm when calling my function, or directly at New-Item - I end up getting the confirmation prompt for every single 'child folder' that is being created. Even with the first prompt, saying "Yes to all" doesn't suppress future prompts
In order to consume the user's answer to the confirmation prompt, you have to actually ask them!
You can do so by calling $PSCmdlet.ShouldContinue():
function Create-NewFolderStructure {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[string]$NewFolderName,
[Parameter()]
$LiteralPath = (Get-Location),
[Parameter()]
$inputFile = "zChildDirectoryNames.txt",
[switch]$Force
)
process {
if(-not $Force){
$yesToAll = $false
$noToAll = $false
if($PSCmdlet.ShouldContinue("Do you want to go ahead and create new directory '${NewFolderName}'?", "Danger!", [ref]$yesToAll, [ref]$noToAll)){
if($yesToAll){
$Force = $true
}
}
else{
return
}
}
# Replace with actual directory creation logic here,
# pass `-Force` or `-Confirm:$false` to New-Item to suppress its own confirmation prompt
Write-Host "Creating new folder ${NewFolderName}" -ForegroundColor Red
}
}
Now, if the user presses [A] ("Yes To All"), we'll remember it by setting $Force to true, which will skip any subsequent calls to ShouldContinue():
PS ~> "folder 1","folder 2"|Create-NewFolderStructure
Danger!
Do you want to go ahead and create new directory 'folder 1'?
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): A
Creating new folder folder 1
Creating new folder folder 2
PS ~>
I'd strongly suggest reading through this deep dive to better understand the SupportsShouldProcess facility: Everything you wanted to know about ShouldProcess
Mathias R. Jessen's helpful answer provides an alternative to using the common -Confirm parameter, via a slightly different mechanism based on a custom -Force switch that inverts the logic of your own attempt (prompt is shown by default, can be skipped with -Force).
If you want to make your -Confirm-based approach work:
Call .ShouldProcess() yourself at the start of the process block, which presents the familiar confirmation prompt, ...
... and, if the method returns $true - indicating that the use confirmed - perform the desired operations after setting a local copy of the $ConfirmPreference preference variable to 'None', so as to prevent the cmdlets involved in your operations to each prompt as well.
function New-FolderStructure {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[string] $NewFolderName,
[string] $LiteralPath = $PWD,
[string] $InputFile = "zChildDirectoryNames.txt"
)
# This block is called once, before pipeline processing begins.
begin {
$directories = Get-Content -LiteralPath "$LiteralPath\$InputFile"
}
# This block is called once if parameter $NewFolderName is bound
# by an *argument* and once for each input string if bound via the *pipeline*.
process {
# If -Confirm was passed, this will prompt for confirmation
# and return `$true` if the user confirms.
# Otherwise `$true` is automatically returned, which also happens
# if '[A] Yes to All' was chosen for a previous input string.
if ($PSCmdlet.ShouldProcess($NewFolderName)) {
# Prevent the cmdlets called below from *also* prompting for confirmation.
# Note: This creates a *local copy* of the $ConfirmPreference
# preference variable.
$ConfirmPreference = 'None'
If (-not (Test-Path -LiteralPath "$LiteralPath\$NewFolderName")) {
$directories | ForEach-Object { New-Item -ItemType Directory -Path "$LiteralPath\$NewFolderName" -Name $_ }
}
Else {
Write-Host "A $NewFolderName folder already exists"
}
}
}
}
Note:
I've changed your function's name from Create-NewFolderStructure to New-FolderStructure to comply with PowerShell's naming conventions, using the approved "verb" New and streamlined the code in a number of ways.
ValueFromPipeline was added as a property to the $NewFolderName parameter so as to support passing multiple values via the pipeline (e.g.
'foo', 'bar' | New-FolderStructure -Confirm)
Note that the process block is then called for each input string, and will also prompt for each, unless you respond with [A] Yes to All); PowerShell remembers this choice between process calls and makes .ShouldProcess() automatically return $true in subsequent calls; similarly, responding to [L] No to All automatically returns $false in subsequent calls.
The need to set $ConfirmPreference to 'None' stems from the fact that PowerShell automatically translates the caller's use of -Confirm into a function-local $ConfirmPreference variable with value 'Low', which makes all cmdlets that support -Confirm act as if -Confirm had been passed.
.ShouldProcess() does not pick up in-function changes to the value of this $ConfirmPreference copy, so it is OK to set it to 'None', without affecting the prompt logic of subsequent process invocations.

Debugging PowerShell

I'm not certain what is wrong with this scriptlet.
I'm trying to break out functionality into several other functions (I have a programming background not a scripting one per se) and to me LOGICALLY the following should execute starting at the "main" function Test-SgnedMpsPackage, accepting the various optional parameters (the script is not yet complete) then when the function Check-Path is called, that is run, then work would resume in the original calling function.
Am I missing something here?
On a side note, how does one return a value to the calling function? a simple return?
function CheckPath($path)
{
if ( test-path -Path $path )
{ Write-Host "{0} confirmed to exist." -f $path }
else
{ Write-Host "{0} DOES NOT exis.\nPlease check and run the script again" -f $path }
exit { exit }
}
function Test-SignedMpsPackage
{
Param(
[string] $PkgSource,
[string] $SigSource,
[string] $Destination
)
Process
{
#Check that both files exist
Write-Host "Check for file existence..."
CheckPath($PkgSource)
CheckPath($SigSource)
#retrieve signatures from file
}
}
Unlike C, C++ or C# there is no "main" entry point function. Any script at the top level - outside of a function - executes. You have defined two functions above but you haven't called either one. You need to do something like this:
function Test-SignedMpsPackage
{
...
}
Test-SignedMpsPackage params
Also as mentioned by #Bill_Stewart, you call your defined functions just like you call PowerShell commands - arguments are space separated and you don't use parens except to evaluate an expression inside the parens.
As for returning a value from a function, any output (Output stream) not captured by assigning to a variable or being redirected to a file is automatically part of the function's output. So I would modify your CheckPath function to this:
function CheckPath($path)
{
if (Test-Path -Path $path) {
Write-Verbose "{0} confirmed to exist." -f $path
$true
}
else {
Write-Verbose "{0} DOES NOT exist.\nPlease check and run the script again" -f $path
$false
}
}
You can use Write-Host as you had before but sometimes, perhaps in a script, you don't want to see the extra output. That is where Write-Verbose comes in handy. Set $VerbosePreference = 'Continue' to see the verbose output.

Recursive -Verbose in Powershell

Is there an easy way to make the -Verbose switch "passthrough" to other function calls in Powershell?
I know I can probably search $PSBoundParameters for the flag and do an if statement:
[CmdletBinding()]
Function Invoke-CustomCommandA {
Write-Verbose "Invoking Custom Command A..."
if ($PSBoundParameters.ContainsKey("Verbose")) {
Invoke-CustomCommandB -Verbose
} else {
Invoke-CustomCommandB
}
}
Invoke-CustomCommandA -Verbose
It seems rather messy and redundant to do it this way however... Thoughts?
One way is to use $PSDefaultParameters at the top of your advanced function:
$PSDefaultParameterValues = #{"*:Verbose"=($VerbosePreference -eq 'Continue')}
Then every command you invoke with a -Verbose parameter will have it set depending on whether or not you used -Verbose when you invoked your advanced function.
If you have just a few commands the do this:
$verbose = [bool]$PSBoundParameters["Verbose"]
Invoke-CustomCommandB -Verbose:$verbose
I began using KeithHill's $PSDefaultParameterValues technique in some powershell modules. I ran into some pretty surprising behavior which I'm pretty sure resulted from the effect of scope and $PSDefaultParameterValues being a sort-of global variable. I ended up writing a cmdlet called Get-CommonParameters (alias gcp) and using splat parameters to achieve explicit and terse cascading of -Verbose (and the other common parameters). Here is an example of how that looks:
function f1 {
[CmdletBinding()]
param()
process
{
$cp = &(gcp)
f2 #cp
# ... some other code ...
f2 #cp
}
}
function f2 {
[CmdletBinding()]
param()
process
{
Write-Verbose 'This gets output to the Verbose stream.'
}
}
f1 -Verbose
The source for cmdlet Get-CommonParameters (alias gcp) is in this github repository.
How about:
$vb = $PSBoundParameters.ContainsKey('Verbose')
Invoke-CustomCommandB -Verbose:$vb

Powershell: Checking if script was invoked with -ErrorAction?

Scripts (with CmdletBinding) and cmdlets all have a standard -ErrorAction parameter available when being invoked. Is there a way then, within your script, if indeed you script was invoked with -ErrorAction?
Reason I ask is because I want to know if the automatic variable value for $ErrorActionPreference, as far as your script is concerned, was set by -ErrorAction or if it is coming from your session level.
$ErrorActionPreference is a variable in the global(session) scope. If you run a script and don't specify the -ErrorAction parameter, it inherits the value from the global scope ($global:ErrorActionPreference).
If you specify -ErrorAction parameter, the $ErrorActionPreference is changed for your private scope, meaning it's stays the same through the script except while running code where you specified something else(ex. you call another script with another -ErrorAction value). Example to test:
Test.ps1
[CmdletBinding()]
param()
Write-Host "Session: $($global:ErrorActionPreference)"
Write-Host "Script: $($ErrorActionPreference)"
Output:
PS-ADMIN > $ErrorActionPreference
Continue
PS-ADMIN > .\Test.ps1
Session: Continue
Script: Continue
PS-ADMIN > .\Test.ps1 -ErrorAction Ignore
Session: Continue
Script: Ignore
PS-ADMIN > $ErrorActionPreference
Continue
If you wanna test if the script was called with the -ErrorAction paramter, you could use ex.
if ($global:ErrorActionPreference -ne $ErrorActionPreference) { Write-Host "changed" }
If you don't know what scopes is, type this in a powershell console: Get-Help about_scopes
Check the $MyInvocation.BoundParameters object. You could use the built-in $PSBoundParameters variable but I found that in some instances it was empty (not directly related to this question), so imo it's safer to use $MyInvocation.
function Test-ErrorAction
{
param()
if($MyInvocation.BoundParameters.ContainsKey('ErrorAction'))
{
'The ErrorAction parameter has been specified'
}
else
{
'The ErrorAction parameter was not specified'
}
}
Test-ErrorAction
If you need to know if the cmdlet was called with the -ErrorAction parameter do like this:
[CmdletBinding()]
param()
if ($myinvocation.Line.ToLower() -match "-erroraction" )
{
"yessss"
}
else
{
"nooooo"
}
This is true alse when the parameter have same value as the global $ErrorActionPreference

How do you support PowerShell's -WhatIf & -Confirm parameters in a Cmdlet that calls other Cmdlets?

I have a PowerShell script cmdlet that supports the -WhatIf & -Confirm parameters.
It does this by calling the $PSCmdlet.ShouldProcess() method before performing the change.
This works as expected.
The problem I have is that my Cmdlet is implemented by calling other Cmdlets and the -WhatIf or -Confirm parameters are not passed along to the Cmdlets I invoke.
How can I pass along the values of -WhatIf and -Confirm to the Cmdlets I call from my Cmdlet?
For example, if my Cmdlet is Stop-CompanyXyzServices and it uses Stop-Service to implement its action.
If -WhatIf is passed to Stop-CompanyXyzServices I want it to also be passed to Stop-Service.
Is this possible?
Passing parameters explicitly
You can pass the -WhatIf and -Confirm parameters with the $WhatIfPreference and $ConfirmPreference variables. The following example achieves this with parameter splatting:
if($ConfirmPreference -eq 'Low') {$conf = #{Confirm = $true}}
StopService MyService -WhatIf:([bool]$WhatIfPreference.IsPresent) #conf
$WhatIfPreference.IsPresent will be True if the -WhatIf switch is used on the containing function. Using the -Confirm switch on the containing function temporarily sets $ConfirmPreference to low.
Passing parameters implicitly
Since the -Confirm and -WhatIf temporarily set the $ConfirmPreference and $WhatIfPreference variables automatically, is it even necessary to pass them?
Consider the example:
function ShouldTestCallee {
[cmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')]
param($test)
$PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Confirm?")
}
function ShouldTestCaller {
[cmdletBinding(SupportsShouldProcess=$true)]
param($test)
ShouldTestCallee
}
$ConfirmPreference = 'High'
ShouldTestCaller
ShouldTestCaller -Confirm
ShouldTestCaller results in True from ShouldProcess()
ShouldTestCaller -Confirm results in an confirm prompt even though I didn't pass the switch.
Edit
#manojlds answer made me realize that my solution was always setting $ConfirmPreference to 'Low' or 'High'. I have updated my code to only set the -Confirm switch if the confirm preference is 'Low'.
After some googling I came up with a good solution for passing common parameters along to called commands. You can use the # splatting operator to pass along all the parameters that were passed to your command. For example, if
Start-Service -Name ServiceAbc #PSBoundParameters
is in the body of your script powershell will pass all the parameters that were passed to your script to the Start-Service command. The only problem is that if your script contains say a -Name parameter it will be passed too and PowerShell will complain that you included the -Name parameter twice. I wrote the following function to copy all the common parameters to a new dictionary and then I splat that.
function Select-BoundCommonParameters
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
$BoundParameters
)
begin
{
$boundCommonParameters = New-Object -TypeName 'System.Collections.Generic.Dictionary[string, [Object]]'
}
process
{
$BoundParameters.GetEnumerator() |
Where-Object { $_.Key -match 'Debug|ErrorAction|ErrorVariable|WarningAction|WarningVariable|Verbose' } |
ForEach-Object { $boundCommonParameters.Add($_.Key, $_.Value) }
$boundCommonParameters
}
}
The end result is you pass parameters like -Verbose along to the commands called in your script and they honor the callers intention.
Here is a complete solution based on #Rynant and #Shay Levy's answers:
function Stop-CompanyXyzServices
{
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')]
Param(
[Parameter(
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]
[string]$Name
)
process
{
if($PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Stop XYZ services '$Name'")){
ActualCmdletProcess
}
if([bool]$WhatIfPreference.IsPresent){
ActualCmdletProcess
}
}
}
function ActualCmdletProcess{
# add here the actual logic of your cmdlet, and any call to other cmdlets
Stop-Service $name -WhatIf:([bool]$WhatIfPreference.IsPresent) -Confirm:("Low","Medium" -contains $ConfirmPreference)
}
We have to see if -WhatIf is passed separately as well so that the whatif can be passed on to the individual cmdlets. ActualCmdletProcess is basically a refactoring so that you don't call the same set of commands again just for the WhatIf. Hope this helps someone.
Updated per #manojlds comment
Cast $WhatIf and $Confirm to Boolean and pass the values to the the underlying cmdlet:
function Stop-CompanyXyzServices
{
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')]
Param(
[Parameter(
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]
[string]$Name
)
process
{
if($PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Stop service '$Name'"))
{
Stop-Service $name -WhatIf:([bool]$WhatIf) -Confirm:([bool]$confirm)
}
}
}
Just so you wont get run around the block for hours by this question and the answers here, I would suggest that you read this article instead:
https://powershellexplained.com/2020-03-15-Powershell-shouldprocess-whatif-confirm-shouldcontinue-everything/#suppressing-nested-confirm-prompts
The answers presented here does not work for many cases and I see a danger in people implementing the answers here, without understanding the fundamentals.
Here is how a hacked it to work across scriptmodules: