Allow function to run in parent scope? - powershell

Pardon the title wording if it's a bit confusing. . .
I have a very simple script that just dot sources a .ps1 file but, since it runs inside a function, it doesn't get loaded into the parent scope which is my ultimate objective.
function Reload-ToolBox {
Param (
[Parameter(Mandatory=$false)]
[ValidateSet('TechToolBox',
'NetworkToolBox','All')]
[string[]]$Name = 'TechToolBox'
)
Begin
{
$ToolBoxes = #{
TechToolBox = "\\path\to\my\ps1.ps1"
NetworkToolBox = "\\path\to\my\ps2.ps1"
}
if ($($PSBoundParameters.Values) -contains 'All') {
$null = $ToolBoxes.Add('All',$($ToolBoxes.Values | Out-String -Stream))
}
$DotSource = {
foreach ($PS1Path in $ToolBoxes.$ToolBox)
{
. $PS1Path
}
}
}
Process
{
foreach ($ToolBox in $Name)
{
Switch -Regex ($ToolBoxes.Keys)
{
{$_ -match "^$ToolBox$"} { & $DotSource }
}
}
}
End { }
}
Question:
How would I be able to load the ps1 being called in the function, into the parent scope?
google no helped:(

In order for dot-sourcing performed inside your function to also take effect for the function's caller, you must dot-source the function call itself (. Reload-TooBox ...)
Unfortunately, there is no way to make this dot-sourcing automatic, but you can at least check whether the function was called via dot-sourcing, and report an error with instructions otherwise.
Here's a streamlined version of your function that includes this check:
function Reload-ToolBox {
[CmdletBinding()]
Param (
[ValidateSet('TechToolBox', 'NetworkToolBox', 'All')]
[string[]] $Name = 'TechToolBox'
)
Begin {
# Makes sure that *this* function is also being dot-sourced, as only
# then does dot-sourcing of scripts from inside it also take effect
# for the caller.
if ($MyInvocation.CommandOrigin -ne 'Internal') { # Not dot-sourced?
throw "You must DOT-SOURCE calls to this function: . $((Get-PSCallStack)[1].Position.Text)"
}
$ToolBoxes = #{
TechToolBox = "\\path\to\my\ps1.ps1"
NetworkToolBox = "\\path\to\my\ps2.ps1"
}
$ToolBoxes.All = #($ToolBoxes.Values)
if ($Name -Contains 'All') { $Name = 'All' }
}
Process {
foreach ($n in $Name) {
foreach ($script in $ToolBoxes.$n) {
Write-Verbose "Dot-sourcing $script..."
. $script
}
}
}
}

Related

Looking for docs/explainer on powershell syntax ":Label foreach ($item in $items) { }"

So hard to Google this one...
Looking for docs/explainer on the syntax :Label foreach ($item in $items) { }
I came across an interesting example in the official docs and I'm trying to wrap my head around some of the concepts used. The example I'm referencing is at the very bottom of the about_foreach page (using Get-Help) and also online here: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_foreach?view=powershell-7.3#:~:text=%3AtokenLoop%20foreach
The example defines an AST/parser utility for showing info about where functions are defined within a given script file (pretty cool advanced example imo). There are a few concepts in the example that I've seen before and understand the usefulness of, but haven't used personally, like do/until statements and enumerator methods like $foreach.MoveNext()
But it's the first time I've seen the :myLabel for () {} syntax which seems to only be relevant to specific expressions like loops, and I'm curious about the usage of this construct (like how/can you reference this label), does anyone here make use of this or know where to find docs/explainer on it?
Thanks in advance !
Here's the full raw example from the docs in case you like clicking links:
function Get-FunctionPosition {
[CmdletBinding()]
[OutputType('FunctionPosition')]
param(
[Parameter(Position = 0, Mandatory,
ValueFromPipeline, ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[Alias('PSPath')]
[System.String[]]
$Path
)
process {
try {
$filesToProcess = if ($_ -is [System.IO.FileSystemInfo]) {
$_
} else {
Get-Item -Path $Path
}
$parser = [System.Management.Automation.Language.Parser]
foreach ($item in $filesToProcess) {
if ($item.PSIsContainer -or
$item.Extension -notin #('.ps1', '.psm1')) {
continue
}
$tokens = $errors = $null
$ast = $parser::ParseFile($item.FullName, ([REF]$tokens),
([REF]$errors))
if ($errors) {
$msg = "File '{0}' has {1} parser errors." -f $item.FullName,
$errors.Count
Write-Warning $msg
}
:tokenLoop foreach ($token in $tokens) {
if ($token.Kind -ne 'Function') {
continue
}
$position = $token.Extent.StartLineNumber
do {
if (-not $foreach.MoveNext()) {
break tokenLoop
}
$token = $foreach.Current
} until ($token.Kind -in #('Generic', 'Identifier'))
$functionPosition = [pscustomobject]#{
Name = $token.Text
LineNumber = $position
Path = $item.FullName
}
Add-Member -InputObject $functionPosition `
-TypeName FunctionPosition -PassThru
}
}
}
catch {
throw
}
}
}
A label example from Windows Powershell in Action. Labels don't come up that often. In your example, it's breaking out of both the do loop and the token loop with the label.
# loop label, break out of both loops
$target = 'outer'
:outer while (1) {
while(1) {
break $target # break or continue label
}
}

Anonymous Recursive Function

Is it possible to create an anonymous Recursive Function in PowerShell? (if yes, how?)
I have a recursive object and using a recursive function to drill down through the properties, like:
$Object = ConvertFrom-Json '
{
"Name" : "Level1",
"Folder" : {
"Name" : "Level2",
"Folder" : {
Name : "Level3"
}
}
}'
Function GetPath($Object) {
$Object.Name
if ($Object.Folder) { GetPath $Object.Folder }
}
(GetPath($Object)) -Join '\'
Level1\Level2\Level3
The function is relative small and only required ones, therefore I would like to directly invoke it as an anonymous function, some like:
(&{
$Object.Name
if ($Object.Folder) { ???? $Object.Folder }
}) -Join '\'
Is this possible in PowerShell?
If yes, how can I (as clean as possible) refer to the current function at ?????
Unfortunately there is not much documentation on this topic but you could execute the anonymous script block via $MyInvocation automatic variable, specifically it's ScriptInfo.ScriptBlock Property.
A simple example:
& {
param([int] $i)
if($i -eq 10) { return $i }
($i++)
& $MyInvocation.MyCommand.ScriptBlock $i
}
# Results in 0..10
Using your current code and Json provided in question:
(& {
param($s)
$s.Name; if ($s.Folder) { & $MyInvocation.MyCommand.ScriptBlock $s.Folder }
} $Object) -join '\'
# Results in Level1\Level2\Level3
Same as the above but using pipeline processing instead:
($Object | & {
process {
$_.Name; if($_.Folder) { $_.Folder | & $MyInvocation.MyCommand.ScriptBlock }
}
}) -join '\'
A bit more code but the same can be accomplished using a Collections.Queue instead of recursion, which is likely to be more resource efficient:
$(
$queue = [System.Collections.Queue]::new()
$queue.Enqueue($object)
while($queue.Count) {
$node = $queue.Dequeue()
$node.Name
if($node.Folder) { $queue.Enqueue($node.Folder) }
}
) -join '\'
#Santiago's helpful answer was exactly where I was initially looking for.
Nevertheless, it doesn't always require a recursive function to crawl through a recursive object.
As in the mcve, I could just have done:
#(
do {
$Object.Name
$Object = $Object.Folder
} while ($Object)
) -Join '\'

Show content of hashtable when Pester test case fails

Problem
When a Hashtable is used as input for Should, Pester outputs only the typename instead of the content:
Describe 'test' {
It 'test case' {
$ht = #{ foo = 21; bar = 42 }
$ht | Should -BeNullOrEmpty
}
}
Output:
Expected $null or empty, but got #(System.Collections.Hashtable).
Expected output like:
Expected $null or empty, but got #{ foo = 21; bar = 42 }.
Cause
Looking at Pester source, the test input is formatted by private function Format-Nicely, which just casts to String if the value type is Hashtable. This boils down to calling Hashtable::ToString(), which just outputs the typename.
Workaround
As a workaround I'm currently deriving a class from Hashtable that overrides the ToString method. Before passing the input to Should, I cast it to this custom class. This makes Pester call my overridden ToString method when formatting the test result.
BeforeAll {
class MyHashTable : Hashtable {
MyHashTable( $obj ) : base( $obj ) {}
[string] ToString() { return $this | ConvertTo-Json }
}
}
Describe 'test' {
It 'test case' {
$ht = #{ foo = 21; bar = 42 }
[MyHashTable] $ht | Should -BeNullOrEmpty
}
}
Now Pester outputs the Hashtable content in JSON format, which is good enough for me.
Question
Is there a more elegant way to customize Pester output of Hashtable, which doesn't require me to change the code of each test case?
Somewhat of a hack, override Pester's private Format-Nicely cmdlet by defining a global alias of the same name.
BeforeAll {
InModuleScope Pester {
# HACK: make private Pester cmdlet available for our custom override
Export-ModuleMember Format-Nicely
}
function global:Format-NicelyCustom( $Value, [switch]$Pretty ) {
if( $Value -is [Hashtable] ) {
return $Value | ConvertTo-Json
}
# Call original cmdlet of Pester
Pester\Format-Nicely $Value -Pretty:$Pretty
}
# Overrides Pesters Format-Nicely as global aliases have precedence over functions
New-Alias -Name 'Format-Nicely' -Value 'Format-NicelyCustom' -Scope Global
}
This enables us to write test cases as usual:
Describe 'test' {
It 'logs hashtable content' {
$ht = #{ foo = 21; bar = 42 }
$ht | Should -BeNullOrEmpty
}
It 'logs other types regularly' {
$true | Should -Be $false
}
}
Log of 1st test case:
Expected $null or empty, but got #({
"foo": 21,
"bar": 42
}).
Log of 2nd test case:
Expected $false, but got $true.
A cleaner (albeit more lengthy) way than my previous answer is to write a wrapper function for Should.
Such a wrapper can be generated using System.Management.Automation.ProxyCommand, but it requires a little bit of stitchwork to generate it in a way that it works with the dynamicparam block of Should. For details see this answer.
The wrappers process block is modified to cast the current pipeline object to a custom Hashtable-derived class that overrides the .ToString() method, before passing it to the process block of the original Should cmdlet.
class MyJsonHashTable : Hashtable {
MyJsonHashTable ( $obj ) : base( $obj ) {}
[string] ToString() { return $this | ConvertTo-Json }
}
Function MyShould {
[CmdletBinding()]
param(
[Parameter(Position=0, ValueFromPipeline=$true, ValueFromRemainingArguments=$true)]
[System.Object]
${ActualValue}
)
dynamicparam {
try {
$targetCmd = $ExecutionContext.InvokeCommand.GetCommand('Pester\Should', [System.Management.Automation.CommandTypes]::Function, $PSBoundParameters)
$dynamicParams = #($targetCmd.Parameters.GetEnumerator() | Microsoft.PowerShell.Core\Where-Object { $_.Value.IsDynamic })
if ($dynamicParams.Length -gt 0)
{
$paramDictionary = [Management.Automation.RuntimeDefinedParameterDictionary]::new()
foreach ($param in $dynamicParams)
{
$param = $param.Value
if(-not $MyInvocation.MyCommand.Parameters.ContainsKey($param.Name))
{
$dynParam = [Management.Automation.RuntimeDefinedParameter]::new($param.Name, $param.ParameterType, $param.Attributes)
$paramDictionary.Add($param.Name, $dynParam)
}
}
return $paramDictionary
}
} catch {
throw
}
}
begin {
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Pester\Should', [System.Management.Automation.CommandTypes]::Function)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline()
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process {
try {
# In case input object is a Hashtable, cast it to our derived class to customize Pester output.
$item = switch( $_ ) {
{ $_ -is [Hashtable] } { [MyJsonHashTable] $_ }
default { $_ }
}
$steppablePipeline.Process( $item )
} catch {
throw
}
}
end {
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
To override Pesters Should by the wrapper, define a global alias like this:
Set-Alias Should MyShould -Force -Scope Global
And to restore the original Should:
Remove-Alias MyShould -Scope Global
Notes:
I have also changed the argument of GetCommand() from Should to Pester\Should to avoid recursion due to the alias. Not sure if this is actually necessary though.
A recent version of Pester is required. Failed with Pester 5.0.4 but tested successfully with Pester 5.1.1.

Function pipeline how to skip process block? Similar to continue in a foreach loop

I'm working on a function to accept pipeline input of multiple objects.
I want to be able to skip the processing of an object if the Property matches certain criteria; similar to using "Continue" in a foreach loop. I realize that I could use switch/If statements but I'm trying to find if there's a way to just skip that object altogether.
The simple code below shows more or less what I'm trying to accomplish
Function Get-Foobar{
Param (
[parameter(ValueFromPipelineByPropertyName)][string]$Item
)
Begin{
# DO SOME INIT STUFF
}
Process{
If($Item -ne "Foobar") {
#SOMEHOW SKIP THE PROCESS BLOCK#
# CONTINUE? BREAK? RETURN? WHAT?
}
Else{
Write-host "Item was $($Item)"
}
#Continue doing stuff here.
Write-host "Evertying was totally $Item"
}
End {
# DO CLEAN UP STUFF
}
}
$test = #([pscustomobject]#{Item = "Foobar"},[pscustomobject]#{Item = "NotFubar"} )
$test |Get-Foobar
Item was Foobar
Evertying was totally Foobar
Evertying was totally NotFubar <---- liked to skip this without having it in the upper if/Else
The Process block is a self-contained scriptblock, so continue won't affect it.
Use return inside process to skip to the next input item in the pipeline:
function Get-Foobar {
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Item
)
Begin {
# DO SOME INIT STUFF
}
Process {
if ($Item -ne "Foobar") {
# skip it!
return
}
else {
Write-host "Item was $($Item)"
}
#Continue doing stuff here.
Write-host "Evertying was totally $Item"
}
End {
# DO CLEAN UP STUFF
}
}

How can I pass dynamic parameters to powershell script and iterate over the list?

I want to create a powershell script that accepts dynamic parameters and I also want to iterate through them.
eg:
I call the powershell script in the following manner.
ParametersTest.ps1 -param1 value1 -param2 value2 -param3 value3
And I should be able to access my params inside the script as follows:
for($key in DynamicParams) {
$paramValue = DynamicParams[$key];
}
Is there anyway to do this in powershell? Thanks in advance.
There is nothing built-in like that (essentially you're asking for PowerShell parameter parsing in the absence of any definition of those parameters). You can emulate it, though. With $args you can get at all arguments of the function as an array. You can then iterate that and decompose it into names and values:
$DynamicParams = #{}
switch -Regex ($args) {
'^-' {
# Parameter name
if ($name) {
$DynamicParams[$name] = $value
$name = $value = $null
}
$name = $_ -replace '^-'
}
'^[^-]' {
# Value
$value = $_
}
}
if ($name) {
$DynamicParams[$name] = $value
$name = $value = $null
}
To iterate over dynamic parameters you can either do something like you wrote
foreach ($key in $DynamicParams.Keys) {
$value = $DynamicParams[$key]
}
(note the foreach, not for, the latter of which cannot work like you wrote it) or just iterate normally over the hash table:
$DynamicParams.GetEnumerator() | ForEach-Object {
$name = $_.Key
$value = $_.Value
}