Clumsy function in Powershell - ideas to make it succinct? - powershell

I have just written a simple function in Powershell
Function Get-Stage
{
$myEnv = Get-Environment
$devEnvs = "D","Dev","Test","T"
$prodEnvs = "P","U","PTA","B"
if($devEnvs -contains $myEnv)
{
return "D"
}
elseif($prodEnvs -contains $myEnv)
{
return "P"
}
}
EDIT
Get-Environment is a function that reads the registry to find the environment text which can be any string value listed in $devEnvs and $prodEnvs. The function will just return either a D or a P based on what is returned from Get-Environment
I don't like it. Is there a nice readable, concise way to write it that you can think of?

Well, questions on better ways to write code is very much based on different opinions, but you could do something like the following:
function Get-Stage
{
$environment = Get-Environment
switch ($environment)
{
{$PSItem -in "D","Dev","Test","T"}{
Write-Output "D"
}
{$PSItem -in "P","U","PTA","B"}{
Write-Output "P"
}
default{
Write-Error "Invalid environment value in registry ('$PSItem')"
}
}
}
If you want to support PowerShell v2, just change the switch statement to the following:
switch ($environment)
{
{"D","Dev","Test","T" -contains $_}{
Write-Output "D"
}
{"P","U","PTA","B" -contains $_}{
Write-Output "P"
}
default{
Write-Error "Invalid environment value in registry ('$_')"
}
}

Does this make it any better?
Function Get-Stage
{
$myEnv = Get-Environment;
$envDict = #{
"D" = #("Dev","Test","T");
"P" = #("U","PTA","B")
};
$envDict.Keys | foreach {
$envs = #($_) + $dict[$_];
if($envs -contains $myenv) { return $_; }
}
}

Related

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 '\'

Allow function to run in parent scope?

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

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.

Using a variables string value in variable name

It should work like:
$part = 'able'
$variable = 5
Write-Host $vari$($part)
And this should print "5", since that's the value of $variable.
I want to use this to call a method on several variables that have similar, but not identical names without using a switch-statement. It would be enough if I can call the variable using something similar to:
New-Variable -Name "something"
but for calling the variable, not setting it.
Editing to add a concrete example of what I'm doing:
Switch($SearchType) {
'User'{
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJUsers_ListBox.Items.Add($Item)
}
}
'Computer' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJComputers_ListBox.Items.Add($Item)
}
}
'Group' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJGroups_ListBox.Items.Add($Item)
}
}
}
I want this to look like:
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJ$($SearchType)s_ListBox.Items.Add($Item)
}
You're looking for Get-Variable -ValueOnly:
Write-Host $(Get-Variable "vari$part" -ValueOnly)
Instead of calling Get-Variable every single time you need to resolve a ListBox reference, you could pre-propulate a hashtable based on the partial names and use that instead:
# Do this once, just before launching the GUI:
$ListBoxTable = #{}
Get-Variable OBJ*_ListBox|%{
$ListBoxTable[($_.Name -replace '^OBJ(.*)_ListBox$','$1')] = $_.Value
}
# Replace your switch with this
foreach($Item in $OBJResults_ListBox.SelectedItems) {
$ListBoxTable[$SearchType].Items.Add($Item)
}

Is there a short circuit 'or' that returns the first 'true' value?

Scheme has a short-circuiting or that will return the first non-false value:
> (or 10 20 30)
10
> (or #f 20 30)
20
> (or #f)
#f
It does not evaluate its arguments until needed.
Is there something like this already in PowerShell?
Here's an approximation of it:
function or ()
{
foreach ($arg in $args) {
$val = & $arg; if ($val) { $val; break }
}
}
Example:
PS C:\> or { 10 } { 20 } { 30 }
10
Example:
PS C:\> $abc = $null
PS C:\> or { $abc } { 123 }
123
PS C:\> $abc = 456
PS C:\> or { $abc } { 123 }
456
You could do something like this:
10, $false, 20 | ? { $_ -ne $false } | select -First 1
The result is either the first value from the input list that isn't $false, or $null. Since $null is among the values that PowerShell treats as $false in comparisons, the above should do what you want.
As far as I know, there isn't anything like this built in. I think your function looks pretty good.
It might be more idiomatic to make it take pipelined input:
function or
{
foreach ($x in $input) {
$val = & $x; if ($val) { $val; break }
}
}
Example:
PS > $abc = $null
PS > { $abc },{ 123 } | or
123
PS > $abc = 456
PS > { $abc },{ 123 } | or
456
You're trying to make PowerShell use a Scheme-like syntax by way of your function. Don't do this. Write idiomatic PowerShell. Trying to coerce one language into looking like another language just makes things harder on yourself, introduces lots of room for bugs, and will confuse the %$^%&^*( out of whoever has to maintain your code after you're gone.
PowerShell does appear to short-circuit. Put this code in the ISE and set a breakpoint on the write-output lines in each function, then start the debugger (F5):
function first () {
write-output "first"
}
function second() {
write-output "second"
}
$true -or $(first) -or $(second);
$false -or $(first) -or $(second);
$false -or $(second) -or $(first);
$true evaluates to true (obviously), so it doesn't attempt to process the expression beyond that point. When the next to last line processes, only the breakpoint in first processes. When the last line processes, only the breakpoint in second() is hit.
As long as you give the function a good name, I think creating such a function is perfectly idiomatic Powershell. One tweak I would make to OP's implementation is to make passing in a script block optional:
function Select-FirstValue {
$args |
foreach { if ($_ -is [scriptblock]) { & $_ } else { $_ } } |
where { $_ } |
select -First 1
}
Then the caller only has to add { } brackets around arguments that could have side-effects or be performance costly.
I'm using such a function as a simple way to provide default values for params from a config file. It looks something like:
function Do-Something {
param([string]$Arg1)
$Arg1 = Select-FirstValue $Arg1 { Get-ConfigValue 'arg1' } 'default arg1 val'
[...]
}
This way, the config file only is only attempted to be read if the user does not pass in the argument.