How to make a function be an alias to a cmdlet - powershell

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.

Related

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
}
}
}

How do I get named but undeclared parameters in a function in Powershell?

Here is an example of what I am trying to do.
Function Get-Parameters { Echo $SomeMagicMethod.Get("Name"); }
Get-Parameters -Name "John Doe"
$SomeMagicMethod is an automatic variable or any other method to get named undeclared parameters.
Is that possible in Powershell?
You'll have to parse the unbounded arguments yourself and find the argument that's right after whatever parameter name you're looking for.
I'd abstract it away in a separate function (you could also pass $args, but this is cleaner):
function Get-InvocationParameter
{
param(
[Parameter(Mandatory = $true, Position = 0)]
[System.Management.Automation.InvocationInfo]
$Invocation,
[Parameter(Mandatory = $true, Position = 1)]
[string]
$ParameterName
)
$Arguments = $Invocation.UnboundArguments
$ParamIndex = $Arguments.IndexOf("-$ParameterName")
if($ParamIndex -eq -1){
return
}
return $Arguments[$ParamIndex + 1]
}
Then use it like this:
function Get-Parameter
{
Get-InvocationParameter -Invocation $MyInvocation -ParameterName "Name"
}
And you should be able to see the arguments right after -Name (or nothing):
PS C:\> Get-Parameter -Name "John Doe"
John Doe
PS C:\> Get-Parameter -Name "John Doe","Jane Doe"
John Doe
Jane Doe
PS C:\> Get-Parameter -ID 123
PS C:\>
You can define a special parameter to catch all unbound arguments:
Function Get-Parameters {
Param(
[Parameter(Mandatory=$true)]
$SomeParam,
[Parameter(Mandatory=$false)]
$OtherParam = 'something',
...
[Parameter(Mandatory=$false, ValueFromRemainingArguments=$true)]
$OtherArgs
)
...
}
However, that will give you an array with the remaining arguments. There won't be an association between -Name and "John Doe".
If your function doesn't define any other parameters you could use the automatic variable $args to the same end.
If you want some kind of hashtable with the unbound "named" arguments you need to build that yourself, e.g. like this:
$UnboundNamed = #{}
$UnboundUnnamed = #()
$OtherArgs | ForEach-Object {
if ($_ -like '-*') {
$script:named = $_ -replace '^-'
$UnboundNamed[$script:named] = $null
} elseif ($script:named) {
$UnboundNamed[$script:named] = $_
$script:name = $null
} else {
$UnboundUnnamed += $_
}
}
If you truly want a function with no parameters, $args is the way to go. (Why you would want such a thing is a different question.) Anyway, code like the following will parse the $args array into a hashtable of parameter/argument pairs which you can use in the rest of the function body.
function NoParams
{
$myParams = #{}
switch ($args)
{
-Foo {
if (!$switch.MoveNext()) {throw "Missing argument for Foo"}
$myParams.Foo = $switch.Current
}
-Bar {
if (!$switch.MoveNext()) {throw "Missing argument for Bar"}
$myParams.Bar = $switch.Current
}
-Baz {
if (!$switch.MoveNext()) {throw "Missing argument for Baz"}
$myParams.Baz = $switch.Current
}
default { throw "Invalid parameter '$_'" }
}
$myParams
}

Write-Verbose Wrapper in PowerShell?

I have a script that uses Write-Verbose calls on it. I want to create a wrapper - override it, add another call, and then invoke the regular Write-Verbose command.
Something like:
Function global:Write-Verbose ($msg) {
MyLogger $msg
Real-Write-Verbose $msg
}
How can I do it?
Possible with $ExecutionContext.InvokeCommand.GetCommand:
function Write-Verbose {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
[Alias('Msg')]
[AllowEmptyString()]
[System.String]
${Message})
begin {
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Write-Verbose', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process {
try {
MyLogger $Message
$steppablePipeline.Process($_)
} catch {
throw
}
}
end {
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
Credit goes to Joel Bennett#Poshcode.
If you just want to override it in your script scope (but not in any called scripts or functions), you can do it like this:
function Private:Write-Verbose ($msg) {
MyLogger $msg
&{Write-Verbose $msg}
}

Powershell function dispose or abort handler

I have a pipe function that allocates some resources in begin block that need to be disposed at the end. I've tried doing it in the end block but it's not called when function execution is aborted for example by ctrl+c.
How would I modify following code to ensure that $sw is always disposed:
function Out-UnixFile([string] $Path, [switch] $Append) {
<#
.SYNOPSIS
Sends output to a file encoded with UTF-8 without BOM with Unix line endings.
#>
begin {
$encoding = new-object System.Text.UTF8Encoding($false)
$sw = new-object System.IO.StreamWriter($Path, $Append, $encoding)
$sw.NewLine = "`n"
}
process { $sw.WriteLine($_) }
# FIXME not called on Ctrl+C
end { $sw.Close() }
}
EDIT: simplified function
Unfortunately, there is no good solution for this. Deterministic cleanup seems to be a glaring omission in PowerShell. It could be as simple as introducing a new cleanup block that is always called regardless of how the pipeline ends, but alas, even version 5 seems to offer nothing new here (it introduces classes, but without cleanup mechanics).
That said, there are some not-so-good solutions. Simplest, if you enumerate over the $input variable rather than use begin/process/end you can use try/finally:
function Out-UnixFile([string] $Path, [switch] $Append) {
<#
.SYNOPSIS
Sends output to a file encoded with UTF-8 without BOM with Unix line endings.
#>
$encoding = new-object System.Text.UTF8Encoding($false)
$sw = $null
try {
$sw = new-object System.IO.StreamWriter($Path, $Append, $encoding)
$sw.NewLine = "`n"
foreach ($line in $input) {
$sw.WriteLine($line)
}
} finally {
if ($sw) { $sw.Close() }
}
}
This has the big drawback that your function will hold up the entire pipeline until everything is available (basically the whole function is treated as a big end block), which is obviously a deal breaker if your function is intended to process lots of input.
The second approach is to stick with begin/process/end and manually process Control-C as input, since this is really the problematic bit. But by no means the only problematic bit, because you also want to handle exceptions in this case -- end is basically useless for purposes of cleanup, since it is only invoked if the entire pipeline is successfully processed. This requires an unholy mix of trap, try/finally and flags:
function Out-UnixFile([string] $Path, [switch] $Append) {
<#
.SYNOPSIS
Sends output to a file encoded with UTF-8 without BOM with Unix line endings.
#>
begin {
$old_treatcontrolcasinput = [console]::TreatControlCAsInput
[console]::TreatControlCAsInput = $true
$encoding = new-object System.Text.UTF8Encoding($false)
$sw = new-object System.IO.StreamWriter($Path, $Append, $encoding)
$sw.NewLine = "`n"
$end = {
[console]::TreatControlCAsInput = $old_treatcontrolcasinput
$sw.Close()
}
}
process {
trap {
&$end
break
}
try {
if ($break) { break }
$sw.WriteLine($_)
} finally {
if ([console]::KeyAvailable) {
$key = [console]::ReadKey($true)
if (
$key.Modifiers -band [consolemodifiers]"control" -and
$key.key -eq "c"
) {
$break = $true
}
}
}
}
end {
&$end
}
}
Verbose as it is, this is the shortest "correct" solution I can come up with. It does go through contortions to ensure the Control-C status is restored properly and we never attempt to catch an exception (because PowerShell is bad at rethrowing them); the solution could be slightly simpler if we didn't care about such niceties. I'm not even going to try to make a statement about performance. :-)
If someone has ideas on how to improve this, I'm all ears. Obviously checking for Control-C could be factored out to a function, but beyond that it seems hard to make it simpler (or at least more readable) because we're forced to use the begin/process/end mold.
It's possible to write it in C# where one can implement IDisposable - confirmed to be called by powershell in case of ctrl-c.
I'll leave the question open in case someone comes up with some way of doing it in powershell.
using System;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Text;
namespace MarcWi.PowerShell
{
[Cmdlet(VerbsData.Out, "UnixFile")]
public class OutUnixFileCommand : PSCmdlet, IDisposable
{
[Parameter(Mandatory = true, Position = 0)]
public string FileName { get; set; }
[Parameter(ValueFromPipeline = true)]
public PSObject InputObject { get; set; }
[Parameter]
public SwitchParameter Append { get; set; }
public OutUnixFileCommand()
{
InputObject = AutomationNull.Value;
}
public void Dispose()
{
if (sw != null)
{
sw.Close();
sw = null;
}
}
private StreamWriter sw;
protected override void BeginProcessing()
{
base.BeginProcessing();
var encoding = new UTF8Encoding(false);
sw = new StreamWriter(FileName, Append, encoding);
sw.NewLine = "\n";
}
protected override void ProcessRecord()
{
sw.WriteLine(InputObject);
}
protected override void EndProcessing()
{
base.EndProcessing();
Dispose();
}
}
}
The following is an implementation of "using" for PowerShell (from Solutionizing .Net). using is a reserved word in PowerShell, hence the alias PSUsing:
function Using-Object {
param (
[Parameter(Mandatory = $true)]
[Object]
$inputObject = $(throw "The parameter -inputObject is required."),
[Parameter(Mandatory = $true)]
[ScriptBlock]
$scriptBlock
)
if ($inputObject -is [string]) {
if (Test-Path $inputObject) {
[system.reflection.assembly]::LoadFrom($inputObject)
} elseif($null -ne (
new-object System.Reflection.AssemblyName($inputObject)
).GetPublicKeyToken()) {
[system.reflection.assembly]::Load($inputObject)
} else {
[system.reflection.assembly]::LoadWithPartialName($inputObject)
}
} elseif ($inputObject -is [System.IDisposable] -and $scriptBlock -ne $null) {
Try {
&$scriptBlock
} Finally {
if ($inputObject -ne $null) {
$inputObject.Dispose()
}
Get-Variable -scope script |
Where-Object {
[object]::ReferenceEquals($_.Value.PSBase, $inputObject.PSBase)
} |
Foreach-Object {
Remove-Variable $_.Name -scope script
}
}
} else {
$inputObject
}
}
New-Alias -Name PSUsing -Value Using-Object
With example usage:
psusing ($stream = new-object System.IO.StreamReader $PSHOME\types.ps1xml) {
foreach ($_ in 1..5) { $stream.ReadLine() }
}
Obviously this is really just some packaging around Jeroen's first answer but may be useful for others who find their way here.

Dot-sourcing a script without executing it

Is it possible in Powershell to dot-source or re-use somehow script functions without it being executed? I'm trying to reuse the functions of a script, without executing the script itself. I could factor out the functions into a functions only file but I'm trying to avoid doing that.
Example dot-sourced file:
function doA
{
Write-Host "DoAMethod"
}
Write-Host "reuseme.ps1 main."
Example consuming file:
. ".\reuseme.ps1"
Write-Host "consume.ps1 main."
doA
Execution results:
reuseme.ps1 main.
consume.ps1 main.
DoAMethod
Desired result:
consume.ps1 main.
DoAMethod
You have to execute the function definitions to make them available. There is no way around it.
You could try throwing the PowerShell parser at the file and only executing function definitions and nothing else, but I guess the far easier approach would be to structure your reusable portions as modules or simply as scripts that don't do anything besides declaring functions.
For the record, a rough test script that would do exactly that:
$file = 'foo.ps1'
$tokens = #()
$errors = #()
$result = [System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$tokens, [ref]$errors)
$tokens | %{$s=''; $braces = 0}{
if ($_.TokenFlags -eq 'Keyword' -and $_.Kind -eq 'Function') {
$inFunction = $true
}
if ($inFunction) { $s += $_.Text + ' ' }
if ($_.TokenFlags -eq 'ParseModeInvariant' -and $_.Kind -eq 'LCurly') {
$braces++
}
if ($_.TokenFlags -eq 'ParseModeInvariant' -and $_.Kind -eq 'RCurly') {
$braces--
if ($braces -eq 0) {
$inFunction = $false;
}
}
if (!$inFunction -and $s -ne '') {
$s
$s = ''
}
} | iex
You will have problems if functions declared in the script reference script parameters (as the parameter block of the script isn't included). And there are probably a whole host of other problems that can occur that I cannot think of right now. My best advice is still to distinguish between reusable library scripts and scripts intended to be invoked.
After your function, the line Write-Host "reuseme.ps1 main." is known as "procedure code" (i.e., it is not within the function). You can tell the script not to run this procedure code by wrapping it in an IF statement that evaluates $MyInvocation.InvocationName -ne "."
$MyInvocation.InvocationName looks at how the script was invoked and if you used the dot (.) to dot-source the script, it will ignore the procedure code. If you run/invoke the script without the dot (.) then it will execute the procedure code. Example below:
function doA
{
Write-Host "DoAMethod"
}
If ($MyInvocation.InvocationName -ne ".")
{
Write-Host "reuseme.ps1 main."
}
Thus, when you run the script normally, you will see the output. When you dot-source the script, you will NOT see the output; however, the function (but not the procedure code) will be added to the current scope.
The best way to re-use code is to put your functions in a PowerShell module. Simply create a file with all your functions and give it a .psm1 extension. You then import the module to make all your functions available. For example, reuseme.psm1:
function doA
{
Write-Host "DoAMethod"
}
Write-Host "reuseme.ps1 main."
Then, in whatever script you want to use your module of functions,
# If you're using PowerShell 2, you have to set $PSScriptRoot yourself:
# $PSScriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Import-Module -Name (Join-Path $PSScriptRoot reuseme.psm1 -Resolve)
doA
While looking a bit further for solutions for this issue, I came across a solution which is pretty much a followup to the hints in Aaron's answer. The intention is a bit different, but it can be used to achieve the same result.
This is what I found:
https://virtualengine.co.uk/2015/testing-private-functions-with-pester/
It needed it for some testing with Pester where I wanted to avoid changing the structure of the file before having written any tests for the logic.
It works quite well, and gives me the confidence to write some tests for the logic first, before refactoring the structure of the files so I no longer have to dot-source the functions.
Describe "SomeFunction" {
# Import the ‘SomeFunction’ function into the current scope
. (Get-FunctionDefinition –Path $scriptPath –Function SomeFunction)
It "executes the function without executing the script" {
SomeFunction | Should Be "fooBar"
}
}
And the code for Get-FunctionDefinition
#Requires -Version 3
<#
.SYNOPSIS
Retrieves a function's definition from a .ps1 file or ScriptBlock.
.DESCRIPTION
Returns a function's source definition as a Powershell ScriptBlock from an
external .ps1 file or existing ScriptBlock. This module is primarily
intended to be used to test private/nested/internal functions with Pester
by dot-sourcsing the internal function into Pester's scope.
.PARAMETER Function
The source function's name to return as a [ScriptBlock].
.PARAMETER Path
Path to a Powershell script file that contains the source function's
definition.
.PARAMETER LiteralPath
Literal path to a Powershell script file that contains the source
function's definition.
.PARAMETER ScriptBlock
A Powershell [ScriptBlock] that contains the function's definition.
.EXAMPLE
If the following functions are defined in a file named 'PrivateFunction.ps1'
function PublicFunction {
param ()
function PrivateFunction {
param ()
Write-Output 'InnerPrivate'
}
Write-Output (PrivateFunction)
}
The 'PrivateFunction' function can be tested with Pester by dot-sourcing
the required function in the either the 'Describe', 'Context' or 'It'
scopes.
Describe "PrivateFunction" {
It "tests private function" {
## Import the 'PrivateFunction' definition into the current scope.
. (Get-FunctionDefinition -Path "$here\$sut" -Function PrivateFunction)
PrivateFunction | Should BeExactly 'InnerPrivate'
}
}
.LINK
https://virtualengine.co.uk/2015/testing-private-functions-with-pester/
#>
function Get-FunctionDefinition {
[CmdletBinding(DefaultParameterSetName='Path')]
[OutputType([System.Management.Automation.ScriptBlock])]
param (
[Parameter(Position = 0,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
ParameterSetName='Path')]
[ValidateNotNullOrEmpty()]
[Alias('PSPath','FullName')]
[System.String] $Path = (Get-Location -PSProvider FileSystem),
[Parameter(Position = 0,
ValueFromPipelineByPropertyName = $true,
ParameterSetName = 'LiteralPath')]
[ValidateNotNullOrEmpty()]
[System.String] $LiteralPath,
[Parameter(Position = 0,
ValueFromPipeline = $true,
ParameterSetName = 'ScriptBlock')]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.ScriptBlock] $ScriptBlock,
[Parameter(Mandatory = $true,
Position =1,
ValueFromPipelineByPropertyName = $true)]
[Alias('Name')]
[System.String] $Function
)
begin {
if ($PSCmdlet.ParameterSetName -eq 'Path') {
$Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path);
}
elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') {
## Set $Path reference to the literal path(s)
$Path = $LiteralPath;
}
} # end begin
process {
$errors = #();
$tokens = #();
if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
$ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptBlock.ToString(), [ref] $tokens, [ref] $errors);
}
else {
$ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref] $tokens, [ref] $errors);
}
[System.Boolean] $isFunctionFound = $false;
$functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true);
foreach ($f in $functions) {
if ($f.Name -eq $Function) {
Write-Output ([System.Management.Automation.ScriptBlock]::Create($f.Extent.Text));
$isFunctionFound = $true;
}
} # end foreach function
if (-not $isFunctionFound) {
if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
$errorMessage = 'Function "{0}" not defined in script block.' -f $Function;
}
else {
$errorMessage = 'Function "{0}" not defined in "{1}".' -f $Function, $Path;
}
Write-Error -Message $errorMessage;
}
} # end process
} #end function Get-Function