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
}
}
}
Related
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
}
I have been writing a Powershell library which uses a lot of wrappers around one or two functions. Thanks to a previous question, I discovered that I could use the GetSteppablePipeline() method to do the wrapping. However, I would like to be able to generalize this code. Googling the command, I discovered that there are only one or two samples which have been copied to death, but no single function. So I tried it. It seemed to work at first, and the sample below is roughly what I created first. But when I started to use the code in anger, it just seemed to break all over the place.
If you run the below straight through, it works ok. But if you put in any breakpoints in the ISE, it will break with errors like "Cannot find drive. A drive with the name 'function' does not exist.". If you incorporate the code into any other script, it will also break with seemingly incomprehensible errors (for instance, Where-Object not being recognised as a Cmdlet).
Function CreateSteppablePipeline
{
Param(
[Parameter(Mandatory = $true, Position = 0)]
[string]
$InnerFunctionName,
[Parameter(Mandatory = $true, Position = 1)]
[System.Collections.Generic.Dictionary`2[System.String,System.Object]]
$OuterPSBoundParameters,
[Parameter(Mandatory = $true, Position = 2)]
[System.Management.Automation.EngineIntrinsics]
$OuterExecutionContext,
[Parameter(Mandatory = $true, Position = 3)]
[System.Management.Automation.InvocationInfo]
$OuterMyInvocation
)
$outBuffer = $null
if ($OuterPSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$OuterPSBoundParameters['OutBuffer'] = 1
}
[System.Management.Automation.FunctionInfo] $wrappedCmd = $OuterExecutionContext.InvokeCommand.GetCommand($InnerFunctionName, [System.Management.Automation.CommandTypes]::Function);
[ScriptBlock] $scriptCmd = { & $wrappedCmd #OuterPSBoundParameters };
[System.Management.Automation.SteppablePipeline] $steppablePipeline = $scriptCmd.GetSteppablePipeline($OuterMyInvocation.CommandOrigin);
$steppablePipeline.Begin($OuterMyInvocation.ExpectingInput);
$steppablePipeline;
}
Function Inner
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position=0)]
[string] $ExParam,
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
[string[]] $Value
)
Begin {
[int] $i = 0;
}
Process {
$i++;
ForEach ($myValue In $Value)
{
$myValue;
}
}
End {
$i;
$ExParam;
}
}
Function Outer
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position=0)]
[string] $ExParam,
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
[string[]] $Value
)
Begin {
[System.Management.Automation.SteppablePipeline] $steppablePipeline = CreateSteppablePipeline "Inner" $PSBoundParameters $ExecutionContext $MyInvocation;
}
Process {
$steppablePipeline.Process($_) |
ForEach-Object {
$_ + " : Pipelined";
}
}
End {
$steppablePipeline.End()
}
}
Outer -ExParam 22 "Name1","Name2","Name3","Name4","Name5","Name6","Name7"
"Name1","Name2","Name3","Name4","Name5","Name6","Name7" | Outer -ExParam 22
It has probably to do with scoping (a problem I have come across a number of times in scripts). I tried moving parts of the CreateSteppablePipeline() function between the calling and function scope, but with no luck.
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
}
}
}
I am trying to rename a few of my cmdlets and want to do it without breaking existing scripts. I want to do it without using Set-Alias/New-Alias because I do not want the Aliases to show up when we do Get-Command from the powershell prompt and I thought it might be possible to use exported functions to achieve the same thing that aliasing cmdlets would do.
Here is an example of what I want to do
Old cmdlet - Add-Foo
Renamed cmdlet - Add-FooBar
Expectation - Scripts using Add-Foo should continue to work the same way as it used to
I am thinking of introducing the following function
function Add-Foo()
{
# Delegate parameter inputs to cmdlet Add-FooBar
}
I have a simple version of it but I am not sure if it would work in more complex cases.
function Add-Foo()
{
$cmd = "Add-FooBar"
if ($arguments.Length -eq 0){
Invoke-Expression $cmd;
}
else{
# Concatentate cmdlet and arguments into an expression
$expr = "$($cmd) $($args)";
Write-Debug $expr;
Invoke-Expression $expr;
}
}
I am not sure if my function is going to be 100% compatible with existing usages. Can the function Add-Foo be made such that it behaves well with parameter attributes (pipeline binding) and any other possible usages? Essentially I want the function to take the arguments as is and pass it to the underlying renamed cmdlet.
Any help is appreciated.
Thanks
PowerShell has a built-in feature for this: Proxy commands.
The [System.Management.Automation.ProxyCommand] class has several static methods to help out with this. Below is a template you can use to generate a proxy command and add a condition choosing whether or not to call the original command.
function New-ProxyCommand($command)
{
$cmd = Get-Command $command
$blocks = #{
CmdletBinding = [System.Management.Automation.ProxyCommand]::GetCmdletBindingAttribute($cmd)
Params = [System.Management.Automation.ProxyCommand]::GetParamBlock($cmd)
Begin = [System.Management.Automation.ProxyCommand]::GetBegin($cmd)
Process = [System.Management.Automation.ProxyCommand]::GetProcess($cmd)
End = [System.Management.Automation.ProxyCommand]::GetEnd($cmd)
}
# Indent
filter Indent($indent=' ') { $_ | foreach { ($_ -split "`r`n" | foreach { "${indent}$_" }) -join "`r`n" } }
[array]$blocks.Keys | foreach { $blocks[$_] = $blocks[$_] | Indent }
#"
function $command
{
$($blocks.CmdletBinding)
param
($($blocks.Params)
)
begin
{
`$Reroute = `$false ### Put your conditions here ###
if (`$Reroute) { return }
$($blocks.Begin)}
process
{
if (`$Reroute) { return }
$($blocks.Process)}
end
{
if (`$Reroute) { return }
$($blocks.End)}
}
"#
}
Example:
PS> New-ProxyCommand Get-Item
function Get-Item
{
[CmdletBinding(DefaultParameterSetName='Path', SupportsTransactions=$true, HelpUri='http://go.microsoft.com/fwlink/?LinkID=113319')]
param
(
[Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string[]]
${Path},
[Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
${LiteralPath},
[string]
${Filter},
[string[]]
${Include},
[string[]]
${Exclude},
[switch]
${Force},
[Parameter(ValueFromPipelineByPropertyName=$true)]
[pscredential]
[System.Management.Automation.CredentialAttribute()]
${Credential}
)
begin
{
$Reroute = $false ### Put your conditions here ###
if ($Reroute) { return }
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-Item', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process
{
if ($Reroute) { return }
try {
$steppablePipeline.Process($_)
} catch {
throw
}
}
end
{
if ($Reroute) { return }
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
One option is to use a private function:
function Private:Add-Foo
{
Add-Foobar $args
}
Add-Foo will only call this function in the current scope. The function will not be visible within any child scope (like a called script), and they will use the Add-Foo cmdlet instead.
I'm trying to send an e-mail with a powershell-function using the cmdlet send-mailmessage.
I need to modify the encoding of the text.
The cmdlet send-mailmessage has a parameter "encoding" which uses the class System.Text.Encoding.
So I have to use something like this:
Send-Mailmessage -Encoding ([System.Text.Encoding]::UTF8)
I'd like to use -Encoding UTF8 instead. The Out-File cmdlet works like this. How can I reproduce the behaviour from the Out-File cmdlet?
That's my idea but I find it a bit circumstantial:
[parameter()][ValidateSet("UTF8","Unicode","ASCII")][String]$Encoding
With this I would create the encoding accordingly.
[System.Text.Encoding]::$Encoding
You can create a proxy function, change the type of the Encoding parameter to System.String and manipulate it in the Begin block. I included this example in the PowerShell Proxy Extensions module.
function Send-MailMessage
{
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline=$true)]
[Alias('PsPath')]
[ValidateNotNullOrEmpty()]
[System.String[]]
${Attachments},
[ValidateNotNullOrEmpty()]
[System.String[]]
${Bcc},
[Parameter(Position=2)]
[ValidateNotNullOrEmpty()]
[System.String]
${Body},
[Alias('BAH')]
[Switch]
${BodyAsHtml},
[Parameter()]
[ValidateSet('ASCII','UTF8','UTF7','UTF32','Unicode','BigEndianUnicode','Default','OEM')]
[ValidateNotNullOrEmpty()]
[Alias('BE')]
[System.String]
$Encoding,
[ValidateNotNullOrEmpty()]
[System.String[]]
${Cc},
[Alias('DNO')]
[ValidateNotNullOrEmpty()]
[System.Net.Mail.DeliveryNotificationOptions]
${DeliveryNotificationOption},
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[System.String]
${From},
[Parameter(Position=3)]
[Alias('ComputerName')]
[ValidateNotNullOrEmpty()]
[System.String]
${SmtpServer},
[ValidateNotNullOrEmpty()]
[System.Net.Mail.MailPriority]
${Priority},
[Parameter(Mandatory=$true, Position=1)]
[Alias('sub')]
[ValidateNotNullOrEmpty()]
[System.String]
${Subject},
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[System.String[]]
${To},
[ValidateNotNullOrEmpty()]
[System.Management.Automation.PSCredential]
${Credential},
[Switch]
${UseSsl}
)
begin
{
try
{
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('Encoding'))
{
$null = $PSCmdlet.MyInvocation.BoundParameters.Remove('Encoding')
$newValue = & {
if ($Encoding -eq 'OEM')
{
[System.Text.Encoding]::GetEncoding($Host.CurrentCulture.TextInfo.OEMCodePage)
}
else
{
[System.Text.Encoding]::$Encoding
}
}
$null = $PSCmdlet.MyInvocation.BoundParameters.Add('Encoding',$newValue)
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Send-MailMessage', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
catch
{
throw
}
}
process
{
try
{
$steppablePipeline.Process($_)
}
catch
{
throw
}
}
end
{
try
{
$steppablePipeline.End()
}
catch
{
throw
}
}
}