How to reuse/extend functions on Powershell? - powershell

I'm trying to develop 2 functions with Powershell. The first, will check my database status (online/offline). The second function should loop on the first function until a certain state is achieve.
function Get-DBStatus
{
<# .. removed help section for brevity .. #>
[CmdletBinding()]
[OutputType([System.Object])]
param
(
[Parameter(Mandatory = $true)]
[String]$ServerName,
[Parameter(Mandatory = $true)]
[String]$ServerUser,
[Parameter(Mandatory = $true)]
[String]$ServerPassword,
[Parameter(Mandatory = $true)]
[String]$DatabaseName,
)
try
{
$params = #{ ... }
$dbStatus = Invoke-SqlConnection #params | Where-Object {$_.Name -match $AltDBName }
}
catch
{
Write-Error -Message ('An error has occured while ...')
}
if ([String]::IsNullOrEmpty($dbStatus) -eq $false)
{
$dbStatus
}
# <<< function Get-DbStatusOnlyIf
# <<< same parameters as the function above
# <<< get the desired status as a new parameter
# <<< loop the function above until the desired status is achieved or a timeout is reached
}
I'm new to Powershell and I think I shouldn't repeat myself rewriting the same parameters from the first function into the second one since they're dependent. However, I might be wrong, thus the question.
Thank you for your assistance!

You have to rewrite this parameters on your second function and pass them through or add another paramter to your first function that will do the looping. I would go with the second solution.
Try something like that
function Get-DBStatus {
<# .. removed help section for brevity .. #>
[CmdletBinding()]
[OutputType([System.Object])]
param
(
[Parameter(Mandatory = $true)]
[String]$ServerName,
[Parameter(Mandatory = $true)]
[String]$ServerUser,
[Parameter(Mandatory = $true)]
[String]$ServerPassword,
[Parameter(Mandatory = $true)]
[String]$DatabaseName,
$WaitForStatus, #or something like that
[int]$Timeout=10
)
do {
try {
#$params = #{ ... }
$dbStatus = Invoke-SqlConnection #params | Where-Object {$_.Name -match $AltDBName }
}
catch {
Write-Error -Message ('An error has occured while ...')
return
}
if ([String]::IsNullOrEmpty($dbStatus) -eq $false) {
if ($WaitForStatus){
if ($dbStatus -eq $WaitForStatus) {
$dbStatus
$EndLoop = $true
}
else {
Write-Host -NoNewline "." #only for test
Start-Sleep -Seconds 1
$Timeout -= 1
}
}
else{
$dbStatus
$EndLoop = $true
}
}
}
until ($EndLoop -or $Timeout -eq 0)
}
or with recursion
function Get-DBStatus {
<# .. removed help section for brevity .. #>
[CmdletBinding()]
[OutputType([System.Object])]
param
(
[Parameter(Mandatory = $true)]
[String]$ServerName,
[Parameter(Mandatory = $true)]
[String]$ServerUser,
[Parameter(Mandatory = $true)]
[String]$ServerPassword,
[Parameter(Mandatory = $true)]
[String]$DatabaseName,
$WaitForStatus, #or something like that
[int]$timeout = 3
)
if ($WaitForStatus) {
$start = Get-Date
while (((get-date) - $start).TotalSeconds -lt $timeout) {
$res = Get-DBStatus -ServerName $ServerName -ServerUser $ServerUser -ServerPassword $ServerPassword -DatabaseName $DatabaseName
if ($WaitForStatus -eq $res) {
return $res
}
Start-Sleep -Seconds 1
}
}
else {
try {
$params = #{ ... }
$dbStatus = Invoke-SqlConnection #params | Where-Object {$_.Name -match $AltDBName }
}
catch {
Write-Error -Message ('An error has occured while ...')
}
if ([String]::IsNullOrEmpty($dbStatus) -eq $false) {
$dbStatus
}
}
}

Related

Unable to evaluate against a specific value

I am having trouble thinking of the correct logic on how to go about this. I have a function that accepts two types of switches:
-Add
-Remove
Function Test-Bool {
Param (
# Input Parameters
[Parameter(Mandatory = $false,
HelpMessage='Enter. Workflow. Name.')]
[Alias('OMB','MailBox')]
[string]$Workflow,
[Parameter(Mandatory = $false)]
[Alias('EDIPI','DisplayName')]
[string[]]$UserName
)
DynamicParam {
if ($Workflow -ne $null -and $UserName -ne $null) {
$parameterAttribute = [System.Management.Automation.ParameterAttribute]#{
ParameterSetName = "AddingMembers"
Mandatory = $false
}
$attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$attributeCollection.Add($parameterAttribute)
$dynParam1 = [System.Management.Automation.RuntimeDefinedParameter]::new(
'Add', [switch], $attributeCollection
)
$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$paramDictionary.Add('Add', $dynParam1)
$parameterAttribute1 = [System.Management.Automation.ParameterAttribute]#{
ParameterSetName = "RemovingMembers"
Mandatory = $false
}
$attributeCollection1 = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$attributeCollection1.Add($parameterAttribute1)
$dynParam11 = [System.Management.Automation.RuntimeDefinedParameter]::new(
'Remove', [switch], $attributeCollection1
)
$paramDictionary.Add('Remove', $dynParam11)
return $paramDictionary
}
}
Begin {
$ABool = {
'Add Block'
$Bool = Read-Host -Prompt 'Adding or Removing Member(s)'
if ($Bool.ToLower() -like 'a*') {
$true
}
else {
Break;
}
}
$RBool = {
'Remove Block'
$Bool = Read-Host -Prompt 'Adding or Removing Member(s)'
if ($Bool.ToLower() -like 'r*') {
$true
}
else {
Break;
}
}
if ($PSBoundParameters['Add'].IsPresent) { [bool]$Add = $true }
elseif ($PSBoundParameters['Remove'].IsPresent) { [bool]$Remove = $true }
elseif (-not$PSBoundParameters['Add'].IsPresent) { $Add = & $ABool }
elseif (-not$PSBoundParameters['Remove'].IsPresent) { $Remove = & $RBool }
}
Process {
if ($Add) {
"Add was selected"
}
if ($Remove) {
"Remove was selected"
}
}
}
I can run it several ways:
Test-Bool -Workflow spg -UserName a -Add - works
Test-Bool -Workflow spg -UserName a -Remove - works
Test-Bool -Workflow spg
Should ask to remove or add: Typing Add, works.
Typing Remove, does not work.
Which makes sense, because elseif (-not$PSBoundParameters['Add'].IsPresent) { $Add = & $ABool } is evaluated first and when asked, Remove is typed in which throws it into the else block making it Break/Exit.
QUESTION:
What can I change to make $Remove be evaluated to true and vise-versa when Add is typed (to make $Add True)?
I honestly confused myself so much that I just decided to post here instead of attempting to figure this out on my own.
Use Parameter Sets instead:
Function Test-Bool {
[CmdletBinding(DefaultParameterSetName = 'Undecided')]
Param (
# Input Parameters
[Parameter(Mandatory = $false,
HelpMessage='Enter. Workflow. Name.')]
[Alias('OMB','MailBox')]
[string]$Workflow,
[Parameter(Mandatory = $false)]
[Alias('EDIPI','DisplayName')]
[string[]]$UserName,
[Parameter(Mandatory = $true, ParameterSetName = 'Add')]
[switch]$Add,
[Parameter(Mandatory = $true, ParameterSetName = 'Remove')]
[switch]$Remove
)
begin {
$action = if($PSCmdlet.ParameterSetName -eq 'Undecided'){
$answer = Read-Host -Prompt 'Adding or Removing Member(s)?'
if($answer -like 'a*'){
'Add'
}
elseif($answer -like 'r*') {
'Remove'
}
else {
throw 'Invalid option provided'
}
}
else {
$PSCmdlet.ParameterSetName
}
# $action now contains either 'Add' or 'Remove'
$actionBlock = #{
'Add' = { <# code to add user to workflow #> }
'Remove' = { <# code to remove user from workflow #> }
}[$action]
}
process {
# if/else statements no longer needed, $actionBlock contains the correct scriptblock
. $actionBlock
}
}
If a user doesn't specify either switch, the parameter set name will be Undecided, and the user will be prompted in the begin block - otherwise we simply use the parameter set name associated with the specified switch.
Since the two switch parameters belong to separate parameter sets, the user can no longer pick both.

Custom function, value from pipeline

This a function for changing local user password on a remote machine. I'd like to make it work with a value from pipeline. This works:
$x= #()
$x += Set-UserPassword -ComputerName smz0017d -User localadmin -NewPassword "1"
$x += Set-UserPassword -ComputerName smz0027d -User localadmin -NewPassword "2"
$x | Out-GridView
But with value from pipeline it doesn't. Any tips?
$x = #()
$x += [pscustomobject]#{
ComputerName = 'smzmi0027d'
User = 'localadmin'
NewPassword = 'djkufdjkuf1234'
}
$x += [pscustomobject]#{
ComputerName = 'smzmi0027d'
User = 'localadmin'
NewPassword = '1'
}
foreach ($y in $x)
{
$y | Set-UserPassword
}
The function to accept value from a pipeline. It invokes command on a remote machine and builds custom object with a result:
function Set-UserPasswordLocaly
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
Position = 0)]
[string]$User,
[Parameter(Mandatory = $true,
Position = 1)]
[string]$NewPassword
)
#TODO: Place script here
try
{
Set-LocalUser -Name $User -Password (ConvertTo-SecureString $NewPassword -AsPlainText -Force) -ErrorAction Stop
$pwdSetResult = "$user password has been changed"
$isSuccess = $true
}
catch
{
$pwdSetResult = ($_.Exception).Message
$isSuccess = $false
}
return [PSCustomObject]#{
'ComputerName' = $env:COMPUTERNAME
'isSuccess' = $isSuccess
'Message' = $pwdSetResult
}
}
function Set-UserPasswordRemotely
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
Position = 0)]
[string]$ComputerName,
[Parameter(Mandatory = $true,
Position = 1)]
[string]$User,
[Parameter(Mandatory = $true,
Position = 2)]
[string]$NewPassword
)
#TODO: Place script here
$param = #{
ComputerName = $ComputerName
ScriptBlock = ${function:Set-UserPasswordLocaly}
ArgumentList = $User, $NewPassword
ErrorAction = 'stop'
}
try
{
$invoke = Invoke-Command #param
$invokeResult = $invoke.Message
$isSuccess = $invoke.isSuccess
}
catch
{
$invokeResult = ($_.Exception).Message
$isSuccess = $false
}
return [PSCustomObject]#{
'ComputerName' = $ComputerName
'isSuccess' = $isSuccess
'Message' = $invokeResult
}
}
function Set-UserPassword
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
Position = 0)]
[string]$ComputerName,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
Position = 1)]
[string]$User,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
Position = 2)]
[string]$NewPassword,
[Parameter(Position = 3,ValueFromPipelineByPropertyName = $true)]
[string]$PasswordVersion
)
#TODO: Place script here
PROCESS
{
if ($env:COMPUTERNAME -eq $ComputerName)
{
Set-UserPasswordLocaly $User $NewPassword
}
else
{
Set-UserPasswordRemotely $ComputerName $User $NewPassword
}
}
}
The ComputerName parameter in Set-UserPassword is not configured to accept pipeline input. Change that and it'll work:
function Set-UserPassword
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
Position = 0)]
[string]$ComputerName,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
Position = 1)]
[string]$User,
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true,
Position = 2)]
[string]$NewPassword,
[Parameter(Position = 3,ValueFromPipelineByPropertyName = $true)]
[string]$PasswordVersion
)
# I'm using Write-Host to demonstrate binding behavior, replace with your actual code
process { Write-Host $PSBoundParameters }
}
PS ~> [pscustomobject]#{
>> ComputerName = 'smzmi0027d'
>> User = 'localadmin'
>> NewPassword = '1'
>> } |Set-UserPassword
>>
[NewPassword, 1] [User, localadmin] [ComputerName, smzmi0027d]

Powershell functions, parameters and arg

I write own powershell func for debug like:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg
)
try {& $FunctionName $OtherArg} catch {...} finally {...}
and use it everyway, but i need more arg after $FunctionName. is it realistic to pass many arguments in this case bec use from 0 to 10 arg. do I have to list all the arguments that can be in the parameters of the function? like:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg,
[PARAMETER(Mandatory = $false)]
$OtherArg1,
[PARAMETER(Mandatory = $false)]
$OtherArg2,
[PARAMETER(Mandatory = $false)]
$OtherArg3
)
try {& $FunctionName $OtherArg OtherArg1 OtherArg2 OtherArg3 } catch {...} finally {...}
but i dont use positional parameters in code and too many named parameters in code (~100)
Interested in any ideas about this. tnx!
The magic word is Splatting. You can provide an array or a hashtable containing your arguments to a function. The splatter is written with an #VariableName instead of the $:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg
)
try {& $FunctionName #OtherArg # Watch out for the # in the OtherArg
} catch {$_} finally {}
}
$FunctionName = 'Get-ChildItem'
$Splatter = #{
Path = 'C:\'
Filter = 'Users'
Directory = $true
}
$Splatter2 = #('c:\')
StartDebug $FunctionName $Splatter
StartDebug $FunctionName $Splatter2
However if you want to use single items as $OtherArg you will have to provide them as single element array as can be seen with $Splatter2. Or extend your function to transform single arguments in arrays automatically, but thats up to you.
I think you better run it using scriptblock:
$result = Invoke-DeepDebug { Get-ChildItem -Path 'C:\' ; Get-Service -InformationAction Continue}
And in Invoke-DeepDebug you can work with $Command.AST as deep and detailed as you want.
Function Invoke-DeepDebug {
param(
[Parameter(Mandatory=$true, Position=0)]
[Scriptblock]$Command
)
Write-Host -f Cyan "Executing " -n
Write-Host -f Magenta $Command.Ast.Extent.Text -n
Write-Host -f Yellow " ... " -n
$result = $null
try {
$result = Invoke-Command $Command -ErrorAction Stop
Write-Host -f Green "OK!"
} catch {
Write-Host -f Red "Error"
Write-Host -f Red "`t$($_.Exception.Message)"
}
return $result
}

Pipelining internally from one function to another

I have a module which has the following two functions, which are almost identical:
<#
.SYNOPSIS
Retrieves a VApp from VCloud.
#>
Function Get-VApp
{
[CmdletBinding()]
[OutputType([System.Xml.XmlElement])]
Param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlElement] $Session,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string[]] $VAppName
)
Begin {
[System.Xml.XmlElement] $queryList = $Session.GetQueryList();
[System.Xml.XmlElement[]] $vAppRecords = $queryList.GetVAppsByRecords().VAppRecord;
}
Process {
ForEach ($VAN in $VAppName)
{
$vAppRecords |
Where-Object { $_.name -eq $VAN } |
ForEach-Object { $_.Get(); }
}
}
End
{
#
}
}
and
<#
.SYNOPSIS
Retrieves a VAppRecord from VCloud.
#>
Function Get-VAppRecord
{
[CmdletBinding()]
[OutputType([System.Xml.XmlElement])]
Param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlElement] $Session,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string[]] $VAppName
)
Begin {
[System.Xml.XmlElement] $queryList = $Session.GetQueryList();
[System.Xml.XmlElement[]] $vAppRecords = $queryList.GetVAppsByRecords().VAppRecord;
}
Process {
ForEach ($VAN in $VAppName)
{
$vAppRecords |
Where-Object { $_.name -eq $VAN } |
ForEach-Object { $_; }
}
}
End
{
#
}
}
Essentially, Get-VApp is like Get-VAppRecord, except that the former calls a Get() method on the returned object. This seems wasteful. If I wasn't bothering with pipelines, it would be easy:
Function Get-VApp
{
[CmdletBinding()]
[OutputType([System.Xml.XmlElement])]
Param(
[Parameter(Mandatory = $true)]
[System.Xml.XmlElement] $Session,
[Parameter(Mandatory = $true)]
[string[]] $VAppName
)
Get-VAppRecord $Session $VAppName |
ForEach-Object {
$_.Get();
}
}
But obviously the pipeline messes things up. I don't call the code in the Begin block multiple times for efficiency, and I would like to find a way to "play nice" with the pipeline without having to batch up records.
The SteppablePipeline class is designed for wrapping pipeline-enabled commands without messing up their pipeline support.
You don't even need to know how to set it up, ProxyCommand.Create() will generate the scaffolding for it!
So let's start out by creating a proxy function for Get-VAppRecord:
$GetVAppRecordCommand = Get-Command Get-VAppRecord
$GetVAppRecordCommandMetadata = [System.Management.Automation.CommandMetadata]::new($GetVAppRecordCommand)
# returns the body of the new proxy functions
[System.Management.Automation.ProxyCommand]::Create($GetVAppRecordCommandMetadata)
... and then we just need to add the Get() call in the process block of it:
function Get-VApp {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[System.Xml.XmlElement]
${Session},
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
[string[]]
${VAppName})
begin
{
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-VAppRecord', [System.Management.Automation.CommandTypes]::Function)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline()
$steppablePipeline.Begin($MyInvocation.ExpectingInput) # Many examples use $PSCmdlet; however setting this ensures that $steppablePipeline.Process() returns the output of the inner function.
} catch {
throw
}
}
process
{
try {
$steppablePipeline.Process($_) |ForEach-Object {
# call Get() on the record
$_.Get()
}
} catch {
throw
}
}
end
{
try {
$steppablePipeline.End()
} catch {
throw
}
}
}

Powershell only returns 2 values, using "return x,y"

I have a weird problem. I have a function that is supposed to return 1 or 2 values, a letter and a number.
For some reason, it only works when I specify the return as
return $x, $y
but it doesn't work like this:
return $x
return $y
The code:
$ModelsDesktop = #("Dimension","Optiplex")
$ModelsLaptop = #("Latitude","Venue")
<#
Returns L or D depending on the Computer Name. Sets U if the model is uncertain.
#>
Function Get-TypeByComputerName{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName
)
Process {
if ($ComputerName -like "*-L-*" -or $ComputerName -like "*-LT-*") {
$ModelType = "L"
}
elseif ($ComputerName -like "*-D-*" -or $ComputerName -like "*-WRK-*") {
$ModelType = "D"
}
else {
$ModelType = "U" #unsure
}
return $ModelType
}
}
<#
Returns L or D depending on the Computer Model. Sets U if the model is uncertain.
#>
Function Get-TypeByModel{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$Model
)
Process {
if (($ModelsLaptop | %{($Model) -like("*$_*")}) -contains $true) {
$ModelType = "L"
}
elseif (($ModelsDesktop | %{($Model) -like("*$_*")}) -contains $true) {
$ModelType = "D"
}
else {
$ModelType = "U"
}
return $ModelType
}
}
<#
Returns L or D depending on the Computer Name and Model. Sets a flag if the model is uncertain.
#>
Function Get-Type{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Model
)
Process {
if ((($ComputerName | Get-TypeByComputerName) -eq ($Model | Get-TypeByModel)) -and (($ComputerName | Get-TypeByComputerName) -ne "U")) {
$ModelType = ($ComputerName | Get-TypeByComputerName)
}
elseif (($ComputerName | Get-TypeByComputerName) -ne "U") {
$ModelType = ($ComputerName | Get-TypeByComputerName)
$Flag = 1
}
elseif (($Model | Get-TypeByModel) -ne "U") {
$ModelType = ($Model | Get-TypeByModel)
$Flag = 1
}
else {
$ModelType = "D"
$Flag = 1
}
return $ModelType
return $Flag
}
}
The value:
$test = New-Object psobject -Property #{ComputerName="crd-l-02-00001";Model="opti 343"}
Output with 2 return statements (as in the previous code):
$test
ComputerName Model
------------ -----
crd-l-02-00001 opti 343
PS C:\Users\u0096902> (Get-Type -ComputerName $test.ComputerName -Model $test.Model)
L
Output with the corrected "return $ModelType, $Flag":
$test
ComputerName Model
------------ -----
crd-l-02-00001 opti 343
PS C:\Users\u0096902> (Get-Type -ComputerName $test.ComputerName -Model $test.Model)
L
1
What am I missing? Can't seem to figure it out. It only seems to return the first "return", but I don't know why.
This example code seems to work perfectly fine:
function get-multiplereturnvalues {
"Return value 1"
"Return value 2"
}
$return = get-multiplereturnvalues
$return[0] # Outputs "Return value 1"
$return[1] # Outputs "Return value 2"
"In computer programming, a return statement causes execution to leave the current subroutine and resume at the point in the code immediately after where the subroutine was called, known as its return address."
Once you call return you are saying that you are finished with that function.
From Get-Help about_Return:
LONG DESCRIPTION
The Return keyword exits a function, script, or script block. It can be
used to exit a scope at a specific point, to return a value, or to indicate
that the end of the scope has been reached.
Your first Return is forcing immediate exit from the function, so the second one never gets a chance to run.