Better way of multiplayer IFs with PowerShell - powershell

Im wondering if I am tackling this in the best way or if there is a better way of achieving my task.
I've written a function in PowerShell which takes different parameters, but some of the parameters won't work together.
So for example if I'm running the function and specifying a computerName parameter then I can't also pass list of multiple computer names.
I know I can write multiple If statements as along the lines if If(($computerName) - and ($computerList)){Then write and error}
but there are several parameters not just two, so do I need to do an if for each set of parameters someone could type in, or is there a better way of me tackling this?
currently I have multiple Ifs like If $computerName -and !(log file) and $computerlist) then write an error etc.

The PowerShell-idiomatic solution here is to declare a parameter that accepts either:
function Get-Stuff
{
param(
[Parameter(ValueFromPipeline = $true)]
[string[]]$ComputerName
)
process {
foreach($Computer in $ComputerName){
# do stuff with each individual -ComputerName argument
}
}
}
Now the user can do both
Get-Stuff -ComputerName oneComputerName
... and
"many","computer","names" |Get-Stuff
or for that matter
$computers = Get-Content .\Computers.txt
Get-Stuff -ComputerName $computers
# or
$computers |Get-Stuff
I know I can write multiple If statements as along the lines of If(($computerName) -and ($computerList)){Then write and error}
You can, but this is generally a bad idea - to test whether an argument value was passed to a parameter is better done through the automatic variable $PSBoundParameters:
function Get-Stuff
{
param(
[string]$AParameter,
[string]$ADifferentOne
)
if($PSBoundParameters.ContainsKey('AParameter')){
# an argument was definitely provided to $AParameter
}
if($PSBoundParameters.ContainsKey('ADifferentOne')){
# an argument was definitely provided to $ADifferentOne
}
}
The answer to the implied question of "how do I declare and work with mutually exclusive parameters" is parameter sets:
function Verb-Noun
{
param(
[Parameter(Mandatory = $true, ParameterSetName = 'SingleComputer')]
[string]$ComputerName,
[Parameter(Mandatory = $true, ParameterSetName = 'MultipleComputers')]
[string[]]$ComputerList
)
if($PSCmdlet.ParameterSetName -eq 'SingleComputer'){
# just one, we can deal with $ComputerName
}
else {
# we got multiple names via $ComputerList
}
}
PowerShell now recognizes two distinct parameter sets, each of which only accepts one of our parameters:
PS ~> Get-Command Verb-Noun -Syntax
Verb-Noun -ComputerName <string> [<CommonParameters>]
Verb-Noun -ComputerList <string[]> [<CommonParameters>]

Related

how to use both parameters and hard-code the servers list through powershell

I am trying to create a script which should run using parameter and without parameter should result for for multiple list:
Ex: I have a function under which I gave param as servername which is working fine for single server ad fetching the results.
(RestoreFileList -servername XXXXXXXXXXX1)
If I do not want to give a parameter as servername and output should result full set of servers list data
Ex : Restorefilecount
Am i missing something between parameter and serverlist condition which should fetch the results, Any help on this?
Script *************
function Restorefilecount()
{
[cmdletbinding()]
param(
[Parameter(position = 0,mandatory = $false)]
[string] $Servername
)
$servername = #()
$servername = ('XXXXXXXX1', 'XXXXXXXXX2', 'XXXXXXXXX3', 'XXXXXXXXXX4')
$result = Invoke-Client -ComputerName $Servername -ScriptBlock {
$Server = HOSTNAME.EXE
$Query = #'
Select ##ServerName AS ServerName , name,(SUBSTRING(NAME ,15,3) * 100 ) / 100 AS
file,state_desc,user_access_desc FROM master.sys.databases where name like 'TKT' ORDER BY 2
'#
Invoke-Sqlcmd -ServerInstance $Server -Database master -Query $Query
}
Building on Abraham Zinala's helpful comments:
It looks like you're simply looking to define a parameter's default value:
function Restore-FileCount {
[CmdletBinding()]
param(
[string[]] $Servername = #('XXXXXXXX1', 'XXXXXXXXX2', 'XXXXXXXXX3', 'XXXXXXXXXX4')
)
# ...
$Servername # Output for demo purposes
}
Note how the parameter type had to be changed from [string] to [string[]] in order to support an array of values.
Incidental changes:
Since you're using a param(...) block to define your parameters (which is generally preferable), there is no need to place () after the function name (Restorefilecount()) - while doing so doesn't cause a syntax error as long as there's nothing or only whitespace between ( and ), note that you declare parameters either via a param(...) block or via function foo(...); also, in parameter-less functions () is never needed - see the conceptual about_Functions help topic.
I've inserted a hyphen (-) in your function name, to make it conform to PowerShell's naming convention.
I've omitted [Parameter(position = 0, mandatory = $false)], because what this attribute instance specifies amounts to the default behavior (all parameters are non-mandatory by default, and Position=0 is implied by $ServerName being the first (and only) parameter).

How do you call a PowerShell function with an Object of Arguments

In PowerShell, one can generally call a function with arguments as follows:
DoRoutineStuff -Action 'HouseKeeping' -Owner 'Adamma George' -Multiples 4 -SkipEmail
To trap these 4 supplied arguments at runtime, one might place this inside the function definition
""
"ARGUMENTS:"
$PSBoundParameters
And the resulting object displayed might look like so:
ARGUMENTS:
Key Value
--- -----
Action HouseKeeping
Owner Adamma George
Multiples 4
SkipEmail True
Now, my question is: If I were to manually build the $MyObject identical to $PSBoundParameters displayed above, is there a way to say:
RunFunction 'DoRoutineStuff' -oArgument $MyObject
Again, if it were to be a script file rather than the function DoRoutineStuff, does that make any difference?
Why might one need to do this?
Picture a situation where you need to catch the arguments supplied to first script or function, using $PSBoundParameters, like so:
DoRoutineStuff{
param(
[string]$Action,
[string]$Owner,
[Int]$Multiples,
[switch]$SkipEmail
)
$Data = $PSBoundParameters
#Update one object property
$Data.Multiples = 1
#Then, recursively call `DoRoutineStuff` using `$Data`
#Other tasks
exit;
}
It sounds like the language feature you're looking for is splatting.
You simply pack you're named parameter arguments into a hashtable, store that in a variable and then pass the variable using # in front of its name:
$myArguments = #{
Action = 'HouseKeeping'
Owner = 'Adamma George'
Multiples = 4
SkipEmail = $true
}
Do-Stuff #myArguments
You can also use this technique to only pass a partial set of parameter arguments (or none at all), great for passing along conditional arguments:
$myArguments = #{}
if($someCondition){
$myArguments['Multiples'] = 1
$myArguments['SkipEmail'] = $true
}
if($somethingElse){
$myArguments['Multiple'] = 4
}
Do-Stuff -Action 'HouseKeeping' -Owner 'Adamma George' #myArguments
You can also reuse $PSBoundParameters for splatting further - very useful for proxy functions:
function Measure-Files
{
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $false)]
[string]$Filter,
[Parameter(Mandatory = $false)]
[switch]$Recurse
)
return (Get-ChildItem #PSBoundParameters |Measure-Object -Property Length).Sum
}

Dynamic invoke command with different parameters

In a PowerShell script, I want to read a CSV file that contains something like this:
Type Title Param1 Param2
---- ----- ------ ------
Type1 Foo type 1 ValueForType1
Type2 Foo type 2 ValueForType2
When type is Type1, I have to call a function named New-FooType1, when type is Type2, the funcation is named New-FooType2, and so on:
function New-FooType1{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param1
)
Write-Host "New-FooType1 $Title with $Param1"
}
function New-FooType2{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param2
)
Write-Host "New-FooType2 $Title with $Param2"
}
I'm trying to route the call to either of the functions, using a dynamic invocation:
$csv | % {
$cmdName = "New-Foo$($_.Type)"
Invoke-Command (gcm $cmdName) -InputObject $_
}
However, I always get an error:
Parameter set cannot be resolved using the specified named parameters
As you can see, different type mean different parameters set.
How can I solve this? I would like to avoid manipulating properties manually, because in my real life script, I have a dozen of different types, with up to 6 parameters.
Here is a complete repro sample of the issue:
$csvData = "Type;Title;Param1;Param2`nType1;Foo type 1;ValueForType1;;`nType2;Foo type 2;;ValueForType2"
$csv = ConvertFrom-csv $csvData -Delimiter ';'
$csv | ft -AutoSize
function New-FooType1{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param1
)
Write-Host "New-FooType1 $Title with $Param1"
}
function New-FooType2{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param2
)
Write-Host "New-FooType2 $Title with $Param2"
}
$csv | % {
$cmdName = "New-Foo$($_.Type)"
Invoke-Command (gcm $cmdName) -InputObject $_
}
The expected output of this script is:
New-FooType1 Foo type 1 with ValueForType1
New-FooType2 Foo type 2 with ValueForType2
Use the call operator &:
$CmdName = "New-FooType1"
$Arguments = "type1"
& $CmdName $Arguments
the call operator also supports splatting if you want the arguments bound to specific named parameters:
$Arguments = #{
"title" = "type1"
}
& $CmdName #Arguments
To invoke command by name you should use invoke operator &. Invoke-Command cmdlet support only ScriptBlock and file invocation, and file invocation only supported for remote calls.
For dynamic parameter binding you can use spatting, but in that case you have to convert PSCustomObjects, returned by ConvertFrom-Csv cmdlet, to Hashtable. You also have to strip any extra parameters from Hashtable because splatting will fail if you try to bind non-existing parameter.
Another approach for dynamic parameter binding would be to use binding from pipeline object. It looks like it is what you want to do, since you mark all your parameters with ValueFromPipelineByPropertyName option. And this approach will just ignore any extra property it can not bind to parameter. I recommend you to remove ValueFromPipeline option, because with this option in case of absence of property with parameter name PowerShell will just convert PSCustomObject to string (or to whatever type you use for parameter) and pass it as value for parameter.
So, all you need is to pass object by pipeline and use invoke operator for invocation of command with dynamic name:
$_ | & "New-Foo$($_.Type)"
dont know exactly what your trying to do, but
Invoke-Command (gcm $cmdName) ?
Try invoke-expression $cmdname

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:

Use the Get-Help cmdlet to display comment-based help in the same format

I am trying to use the Get-Help cmdlet to display comment-based help in the same format in which it displays the cmdlet help topics that are generated from XML files. The ability to do this is documented in about_Comment_based_Help on TechNet, but when I execute the get-help cmdlet against my script I only get the script name returned. Any help would be appreciated!
PS C:\Admin> Get-Help .\checksystem.ps1 -full
checksystem.ps1
checksystem.ps1 script:
function IsAlive {
<#
.DESCRIPTION
Checks to see whether a computer is pingable or not.
.PARAMETER computername
Specifies the computername.
.EXAMPLE
IsAlive -computername testwks01
.NOTES
This is just an example function.
#>
param (
$computername
)
Test-Connection -count 1 -ComputerName $computername -TimeToLive 5 |
Where-Object { $_.StatusCode -eq 0 } |
Select-Object -ExpandProperty Address
}
IsAlive -computername 192.168.1.1
It will work, but you are trying to run get help on the script. You have added the help to the function. If you dot source your script, and then type get-help isalive, you will see your help for the function.
. .\checksystem.ps1 ; get-help isalive -full
It works, you just gotta make sure you have the right headings. I've always put the comment block right above the function too. I'm not sure if it's supposed to work inside of the function or not.
Below is an example of one of my functions that has a working doc help.
##############################################################################
#.SYNOPSIS
# Gets a COM object from the running object table (ROT) similar to GetObject
# in Visual Basic.
#
#.DESCRIPTION
# To maintain consistency with New-Object this cmdlet requires the -ComObject
# parameter to be provided and the TypeName parameter is not supported.
#
#.PARAMETER TypeName
# Not supported, but provided to maintain consistency with New-Object.
#
#.PARAMETER ComObject
# The ProgID of a registered COM object, such as MapPoint.Application.
#
#.PARAMETER Force
# If an existing object is not found, instead of writing an error, a new
# instance of the object will be created and returned.
#
#.EXAMPLE
# $olMailItem = 0
# Get-Object -ComObject Outlook.Application | %{$_.CreateItem($olMailItem).Display()}
##############################################################################
function Get-Object {
[CmdletBinding(DefaultParameterSetName='Net')]
param (
[Parameter(ParameterSetName='Net', Position=1, Mandatory=$true)]
[String]$TypeName,
[Parameter(ParameterSetName='Com', Mandatory=$true)]
[String]$ComObject,
[Parameter()]
[Switch]$Force
)
if ( $TypeName ) { throw '-TypeName is not supported. Use -ComObject instead.' }
if ( $ComObject ) {
try {
[System.Runtime.InteropServices.Marshal]::GetActiveObject($ComObject)
}
catch [System.Management.Automation.MethodInvocationException] {
if ( $Force ) { New-Object -ComObject $ComObject }
else { Write-Error "An active object of type $ComObject is not available." }
}
}
}
Note - If you forget to add the name of a parameter after .PARAMETER, none of your custom help text will show when you run get-help
Similarly, if you misspell any of the keywords the custom help will not be displayed.