Parameter-specific usage of "SupportsShouldProcess - powershell

Please have a look at the code snippet bellow. I am making use of the SupportsShouldProcess parameter within the CmdletBinding decorator, which provides me with two additional "builtin" functions namely -whatif and -confirm
However, having to use the parameter SupportsShouldProcess on top provides the ability to all functions/parameters that are going to be used by the user. For the example below, my ParameterSetname="Help" also gets the ability.
So the question is: How can I provide -whatif and -confirm only to specific parameters/functions?
[CmdletBinding(DefaultParameterSetName="Help", SupportsShouldProcess)]
Param(
[parameter(ParameterSetName="Help")]
[switch]$Help,
# Client Management
[parameter(ParameterSetName="MoveAdComputerObjects", Position=0)]
[switch]$MoveAdComputerObjects,
# Additional parameters
[parameter(ParameterSetName="MoveAdComputerObjects", Mandatory)]
[ValidatePattern("foobar")]
[string]$Src,
[parameter(ParameterSetName="MoveAdComputerObjects", Mandatory)]
[ValidatePattern("foobar")]
[string]$Dst
)
Function Help
{
Write-Host
Get-Command -Syntax $MyInvocation.ScriptName
}
Function MoveAdComputerObjects
{
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidatePattern("foobar")]
[string]$Src,
[ValidatePattern("foobar")]
[string]$Dst
)
# Do something useful
Write-Host $Src, $Dst
}
switch($PSCmdlet.ParameterSetName)
{
"Help" { Help }
"MoveAdComputerObjects" { MoveAdComputerObjects -Src $Src -Dst $Dst }
}

Related

Couldn't use predefined array inside Validateset - Powershell

I'm looking for a way to make a cmdlet which receives parameter and while typing, it prompts suggestions for completion from a predefined array of options.
I was trying something like this:
$vf = #('Veg', 'Fruit')
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateSet($vf)]
$Arg
)
}
The expected result should be:
When writing 'Test-ArgumentCompleter F', after clicking the tub button, the F autocompleted to Fruit.
To complement the answers from #mklement0 and #Mathias, using dynamic parameters:
$vf = 'Veg', 'Fruit'
function Test-ArgumentCompleter {
[CmdletBinding()]
param ()
DynamicParam {
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$AttributeCollection.Add($ParameterAttribute)
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($vf)
$AttributeCollection.Add($ValidateSetAttribute)
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter('Arg', [string], $AttributeCollection)
$RuntimeParameterDictionary.Add('Arg', $RuntimeParameter)
return $RuntimeParameterDictionary
}
}
Depending on how you want to predefine you argument values, you might also use dynamic validateSet values:
Class vfValues : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() { return 'Veg', 'Fruit' }
}
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateSet([vfValues])]$Arg
)
}
note: The IValidateSetValuesGenerator class [read: interface] was introduced in PowerShell 6.0
In addition to mklement0's excellent answer, I feel obligated to point out that in version 5 and up you have a slightly simpler alternative available: enum's
An enum, or an "enumeration type", is a static list of labels (strings) associated with an underlying integral value (a number) - and by constraining a parameter to an enum type, PowerShell will automatically validate the input value against it AND provide argument completion:
enum MyParameterType
{
Veg
Fruit
}
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[MyParameterType]$Arg
)
}
Trying to tab complete the argument for -Arg will now cycle throw matching valid enum labels of MyParameterType:
PS ~> Test-ArgumentCompleter -Arg v[<TAB>]
# gives you
PS ~> Test-ArgumentCompleter -Arg Veg
PowerShell generally requires that attribute properties be literals (e.g., 'Veg') or constants (e.g., $true).
Dynamic functionality requires use of a script block (itself specified as a literal, { ... }) or, in specific cases, a type literal.
However, the [ValidateSet()] attribute only accepts an array of string(ified-on-demand) literals or - in PowerShell (Core) v6 and above - a type literal (see below).
Update:
If you're using PowerShell (Core) v6+, there's a simpler solution based on defining a custom class that implements the System.Management.Automation.IValidateSetValuesGenerator interface - see the 2nd solution in iRon's helpful answer.
Even in Windows PowerShell a simpler solution is possible if your validation values can be defined as an enum type - see Mathias R. Jessen's helpful answer.
To get the desired functionality based on a non-literal array of values, you need to combine two other attributes:
[ArgumentCompleter()] for dynamic tab-completion.
[ValidateScript()] for ensuring on command submission that the argument is indeed a value from the array, using a script block.
# The array to use for tab-completion and validation.
[string[]] $vf = 'Veg', 'Fruit'
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
# Tab-complete based on array $vf
[ArgumentCompleter({
param($cmd, $param, $wordToComplete) $vf -like "$wordToComplete*"
})]
# Validate based on array $vf.
# NOTE: If validation fails, the (default) error message is unhelpful.
# You can work around that in *Windows PowerShell* with `throw`, and in
# PowerShell (Core) 7+, you can add an `ErrorMessage` property:
# [ValidateScript({ $_ -in $vf }, ErrorMessage = 'Unknown value: {0}')]
[ValidateScript({
if ($_ -in $vf) { return $true }
throw "'$_' is not in the set of the supported values: $($vf -join ', ')"
})]
$Arg
)
"Arg passed: $Arg"
}
To add to the other helpful answers, I use something similiar for a script I made for work:
$vf = #('Veg', 'Fruit','Apple','orange')
$ScriptBlock = {
Foreach($v in $vf){
New-Object -Type System.Management.Automation.CompletionResult -ArgumentList $v,
$v,
"ParameterValue",
"This is the description for $v"
}
}
Register-ArgumentCompleter -CommandName Test-ArgumentCompleter -ParameterName Arg -ScriptBlock $ScriptBlock
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[String]$Arg )
}
Documentation for Register-ArgumentCompleter is well explained on Microsoft Docs. I personally don't like to use the enum statement as it didnt allow me to uses spaces in my Intellisense; same for the Validate parameter along with nice features to add a description.
Output:
EDIT:
#Mklement made a good point in validating the argument supplied to the parameter. This alone doesnt allow you to do so without using a little more powershell logic to do the validating for you (unfortunately, it would be done in the body of the function).
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
$Arg )
if($PSBoundParameters.ContainsKey('Arg')){
if($VF -contains $PSBoundParameters.Values){ "It work:)" }
else { "It no work:("}
}
}
I think it's worth sharing another alternative that complements the helpful answers from mklement0, Mathias, iRon and Abraham. This answer attempts to show the possibilities that PowerShell can offer when it comes to customization of a Class.
The Class used for this example offers:
Compatibility with Windows PowerShell 5.1 and PowerShell Core.
Validation, Completion and custom Error Message.
For the example below I'll be using completion and validation on values from a current directory, the values are fed dynamically at runtime with Get-ChildItem -Name.
Class
When referring to the custom validation set I've decided to use the variable $this, however that can be easily change for a variable name of one's choice:
[psvariable]::new('this', (& $this.CompletionSet))
The completion set could be also a hardcoded set, i.e.:
[string[]] $CompletionSet = 'foo', 'bar', 'baz'
However that would also require some modifications in the class logic itself.
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Collections
using namespace System.Collections.Generic
class CustomValidationCompletion : ValidateEnumeratedArgumentsAttribute, IArgumentCompleter {
[scriptblock] $CompletionSet = { Get-ChildItem -Name }
[scriptblock] $Validation
[scriptblock] $ErrorMessage
CustomValidationCompletion() { }
CustomValidationCompletion([scriptblock] $Validation, [scriptblock] $ErrorMessage) {
$this.Validation = $Validation
$this.ErrorMessage = $ErrorMessage
}
[void] ValidateElement([object] $Element) {
$context = #(
[psvariable]::new('_', $Element)
[psvariable]::new('this', (& $this.CompletionSet))
)
if(-not $this.Validation.InvokeWithContext($null, $context)) {
throw [MetadataException]::new(
[string] $this.ErrorMessage.InvokeWithContext($null, $context)
)
}
}
[IEnumerable[CompletionResult]] CompleteArgument(
[string] $CommandName,
[string] $ParameterName,
[string] $WordToComplete,
[CommandAst] $CommandAst,
[IDictionary] $FakeBoundParameters
) {
[List[CompletionResult]] $result = foreach($item in & $this.CompletionSet) {
if(-not $item.StartsWith($wordToComplete)) {
continue
}
[CompletionResult]::new("'$item'", $item, [CompletionResultType]::ParameterValue, $item)
}
return $result
}
}
Implementation
function Test-CompletionValidation {
[alias('tcv')]
[CmdletBinding()]
param(
[CustomValidationCompletion(
Validation = { $_ -in $this },
ErrorMessage = { "Not in set! Must be one of these: $($this -join ', ')" }
)]
[ArgumentCompleter([CustomValidationCompletion])]
[string] $Argument
)
$Argument
}
Demo

Mutually exclusive switch parameters

The function has one required parameter, -Path, and two other mutually exclusive switches. This is not the real function, but a MRE (Minimal Reproducable Example). The default operation is to copy the file to a known location and then remove it.
Do-TheFile [-Path] <String[]> [[-Copy] | [-Remove]]
-Path = filename is mandatory
-CopyOnly = only copy the file, cannot be used with -Remove
-RemoveOnly = only remove the file, cannot be used with -Copy
This is the current code.
param (
[Parameter(Mandatory=$true, Position=0)]
[string[]]$Path
,[Parameter(Mandatory=$false, ParameterSetName='CopyOnly')]
[switch]$CopyOnly
,[Parameter(Mandatory=$false ,ParameterSetName='RemoveOnly')]
[switch]$RemoveOnly
)
The console allows me to specify both -CopyOnly and -RemoveOnly. My expectation was that the console would not permit me to enter both -CopyOnly and -RemoveOnly because they are in different ParameterSets. How can I specify these ParameterSets so that -Copy and -Remove are mutually exclusive?
PS C:\src\t> Do-TheFile -Path t.txt -CopyOnly -RemoveOnly
Do-TheFile: Parameter set cannot be resolved using the specified named parameters. One or more parameters issued cannot be used together or an insufficient number of parameters were provided.
Agree with the others here.
Your code works, as written when using IntelliSense, but PowerShell will not stop you from typing in other valid switches/variables/property names (in either the consolehost, ISE, VSCode, Visual Studio, etc...), that does not mean it would work just because you typed both.
Why make two switches, when you only want to use one option at a time, no matter what.
Just use a simple validation set.
Function Test-MyFunctionTest
{
[cmdletbinding()]
param
(
[Parameter(Mandatory = $true, Position = 0)]
[string[]]$Path,
[Parameter(Mandatory)][ValidateSet('CopyOnly', 'RemoveOnly')]
[string]$FileAction
)
}
# Results
<#
Test-MyFunctionTest -Path $PWD -FileAction CopyOnly
Test-MyFunctionTest -Path $PWD -FileAction RemoveOnly
#>
Otherwise, as you have discovered, you have to code this up yourself. For example:
Function Test-MyFunctionTestAgain
{
[cmdletbinding()]
param
(
[Parameter(Mandatory=$true, Position=0)]
[string[]]$Path,
[switch]$RemoveOnly
)
If($RemoveOnly.IsPresent)
{'Do the remove action'}
Else {'Do the copy action'}
}
Test-MyFunctionTestAgain -Path $PWD
# Results
<#
Do the copy action
#>
Test-MyFunctionTestAgain -Path $PWD -RemoveOnly
# Results
<#
Do the remove action
#>
Update
As for this...
"I agree that this could work. Although, the default operation (using
no switches) is to both Copy and Remove."
... then this...
Function Test-MyFunctionTestMore
{
[cmdletbinding()]
param
(
[Parameter(Mandatory = $true, Position = 0)]
[string[]]$Path,
[Parameter(Mandatory = $false)][ValidateSet('CopyAndRemove', 'CopyOnly', 'RemoveOnly')]
[string]$FileAction = 'CopyAndRemove'
)
Switch ($FileAction)
{
CopyAndRemove {'Do the copy and remove action'}
CopyOnly {'Do the copy only action'}
RemoveOnly {'Do the remove only action'}
}
}
Test-MyFunctionTestMore -Path $PWD
# Results
<#
Do the copy and remove action
#>
Test-MyFunctionTestMore -Path $PWD -FileAction CopyOnly
# Results
<#
Do the copy only action
#>
Test-MyFunctionTestMore -Path $PWD -FileAction RemoveOnly
# Results
<#
Do the remove only action
#>
Or this way, if you are really yearning just to have a switch ;-} ...
Function Test-MyFunctionTestSwitch
{
[cmdletbinding()]
param
(
[Parameter(Mandatory=$true, Position=0)]
[string[]]$Path,
[Parameter(Mandatory = $false)][ValidateSet('CopyAndRemove', 'CopyOnly', 'RemoveOnly')]
[string]$FileAction = 'CopyAndRemove',
[switch]$RemoveOnly
)
If($RemoveOnly.IsPresent)
{
$FileAction = 'RemoveOnly'
'Do the remove only action'
}
ElseIf ($FileAction -eq 'CopyOnly')
{'Do the copy only action'}
Else{'Do the copy and remove action'}
}
Test-MyFunctionTestSwitch -Path $PWD
# Results
<#
Do the copy and remove action
#>
Test-MyFunctionTestSwitch -Path $PWD -FileAction CopyOnly
# Results
<#
Do the copy only action
#>
Test-MyFunctionTestSwitch -Path $PWD -RemoveOnly
# Results
<#
Do the remove only action
#>
Lastly as a point of note:
Trying to emulate some other tools actions, or expecting PowerShell to natively emulate some other tools actions, params, etc., really should not be an expectation.
If you believe PowerShell should have a specific feature, then the option is to submit it to the PowerShell team, to have it upvoted by others for work/inclusion or since PowerShell is open-sourced, you can tool it up and submit it for review/approval of commit.
In contemporary versions of PowerShell, ParameterSets are mutually exclusive.
function greet {
Param(
[String]
$Name = "World",
[Parameter(ParameterSetName="intro")]
[Switch]
$Hello,
[Parameter(ParameterSetName="outro")]
[Switch]
$Farewell
)
if ($Hello) {
echo "Hello, $Name!"
} elseif ($Farewell) {
echo "Farewell, $Name!"
} else {
echo "What's up, $Name"
}
}
This results in the split-groupings that you often see in MS cmdlets:
> greet -?
NAME
greet
SYNTAX
greet [-Name <string>] [-Hello] [<CommonParameters>]
greet [-Name <string>] [-Farewell] [<CommonParameters>]
Doing this requires that the user or the script identify which ParameterSet should be used.
greet -Name "Bob"
> greet -Name "Bob"
greet: Parameter set cannot be resolved using the specified named parameters. One or more parameters issued cannot be used together or an insufficient number of parameters were provided.
This is trying to tell the user they weren't specific enough. See DefaultParameterSetName for how to set it from the script:
There is a limit of 32 parameter sets. When multiple parameter sets are defined, the DefaultParameterSetName keyword of the CmdletBinding attribute specifies the default parameter set. PowerShell uses the default parameter set when it can't determine the parameter set to use based on the information provided to the command.
#Postanote's answer is great and I will prefer it.
However, as #kfsone underlined, DefaultParameterSetName can achieve this with your two switches if you add a ParameterSetName for $Path only and set it as default :
[CmdletBinding(DefaultParameterSetName='CopyAndRemove')]
param (
[Parameter(Mandatory=$true, Position=0)]
[Parameter(ParameterSetName='CopyAndRemove')]
[Parameter(ParameterSetName='CopyOnly')]
[Parameter(ParameterSetName='RemoveOnly')]
[string[]]$Path,
[Parameter(Mandatory=$false, ParameterSetName='CopyOnly')]
[switch]$CopyOnly,
[Parameter(Mandatory=$false ,ParameterSetName='RemoveOnly')]
[switch]$RemoveOnly
)
$Path
$PSCmdlet.ParameterSetName

How to change output type based on parameter

I have a function (several actually) that write output (using Write-Output). Today, the functions all rely on a having a $logpath defined and they output to a text file. In the fewest lines possible, I would like to configure the option to output to the screen if the user wants to (or if $logpath is not specified).
The code below doesn't work, but is an example of what I have in mind. What's the best way to achieve my goal?
Function Do-Stuff {
Param (
[string]$OutputType
)
If ($OutputType -eq 'Host') {
$out = 'Write-Host'
}
Else {
$out = 'Out-File -FilePath C:\test\log.txt -Append'
}
Write-Output ("You are inside the Do-Stuff function.") | $out
}
Thanks.
The simplest way to output something to the user and to a file is obviously Tee-Object. However, that will always create output both ways, so it's not really configurable the way you want.
I would argue that the best way to go about this is to replace your $out variable with an actual logging function that reads input from the pipeline.
function Write-Log {
[Cmdletbinding()]
Param(
[Parameter(
Mandatory=$true,
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]
[string]$Message
)
...
}
You can control output for instance by checking if a logfile is defined in a (script-)global variable:
if ($script:Logfile) {
$Message | Add-Content $script:Logfile
} else {
Write-Host $Message
}
or make each output method depend on a different variable:
if ($script:Logfile) {
$Message | Add-Content $script:Logfile
}
if ($script:WriteToConsole) {
Write-Host $Message
}
You could also use additional parameters on the function:
function Write-Log {
[Cmdletbinding()]
Param(
[Parameter(...)]
[string]$Message,
[Parameter(Mandatory=$false)]
[string]Logfile = './default.log',
[Parameter(Mandatory=$false)]
[switch]$Console
)
...
}
or any combination of the above.
For logging I'd probably prefer global variables, though. That way you can define all your logging settings in one place at the top of your script (even make the values depend on script parameters) and simply use
... | Write-Log
indiscriminately whereever you want something logged in your code.
Also, whenever you need to change something about the way your logging works, you just need to modify the Write-Log function without having to touch the rest of the code.
Function Do-Stuff {
Param (
[switch]$WriteHost
)
$message = Write-Output ("The function is doing stuff.")
If ($WriteHost -or ($logPath -eq $null)) {Write-Host $message} Else {$message | Out-File -FilePath $logPath -Append}
}
A bit of an old question, but I couldn't find an answer I liked. The solution I came up with was to override the Out-File cmdlet with a filter.
function Export-Stuff {
param([string] $FilePath)
if (-not $FilePath) { filter Out-File { $_ } }
#('one','two','three') | Out-File -FilePath $FilePath
}
PS> Export-Stuff
one
two
three
PS> Export-Stuff -FilePath 'output.txt'
PS> Get-Content 'output.txt'
one
two
three

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:

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

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