#Set-Variable good practice?

41 messages · Page 1 of 1 (latest)

stark nebula
#

would this be considered best/good practice:

we have a hashtable variable $inputhash that can have 1 key, or it can have 10 keys (lets call them key1, key2, key3, etc.) If the key exists, it will need a variable...so would it better to do something like this:

if($inputhash.ContainsKey("key1")){
    $key1= $inputhash.key1
}
if($inputhash.ContainsKey("key2")){
    $key2= $inputhash.key2
}
...and so on

or can I utilize Set-variable and loop through the hashtable:

foreach($key in $inputhash.Keys){
    Set-Variable -Name $key -Value $inputhash.$key
}

I am just not confident if Set-variable is generally viewed positively or as good practice verse the other way (for example in my research New-Variable is typically frowned upon or best to avoid kind of thing)

mellow harness
#

Generally viewed in the same light as New-Variable. What benefit do you get from dynamically adding variables?

They're hard to use (vs that key in the hashtable) as they're essentially impossible to effectively enumerate. Obviously you can do Get-Variable, but which are the right variables?

You see a fair amount of this kind of thing in UI programming in PS. But it's rarely necessary, and certainly not the easiest of choices.

stark nebula
#

I’ll put the full usecase here so you can see from start to finish, gimme a few need to get to my pc 🤓

stark nebula
#

usecase below:

    [OutputType([String])]
    [CmdletBinding(DefaultParameterSetName='Set1')]
    Param(

        [Parameter(Mandatory=$true,ParameterSetName='Set1')]
        [String]$numbers,

        [Parameter(Mandatory=$false,ParameterSetName='Set2')]
        [String]$letters,

        [Parameter(Mandatory=$false,ParameterSetName='Set2')]
        [ValidateSet("Green","Blue")]
        [String]$colors,

        [Parameter(Mandatory=$true,ParameterSetName='set3')]
        [hashtable]$InputParameters
    )

    #SCENARIO 1
    if($PSCmdlet.ParameterSetName -eq "set3"){
        if($InputParameters.ContainsKey("numbers")){
            $numbers= $InputParameters.numbers
        }
        if($InputParameters.ContainsKey("letters")){
            $letters= $InputParameters.letters
        }
        if($InputParameters.ContainsKey("colors")){
            $colors= $InputParameters.colors
        }
    }

    #SCENARIO 2
    foreach($key in $InputParameters.Keys){
        Set-Variable -Name $key -Value $InputParameters.$key
    }

    #DO WHATEVER
    return $true
}

$inputparams = @{
    letters = "abcd"
    colors = "Green"
}
Test-Something -InputParameters $inputparams```
#

i want to know if its appropriate to do #SCENARIO 2 in this case

mellow harness
#

why wouldn't you just encourage the caller to splat the hashtable?

#

what is it you do with these variables that require them to be variables? You could, for instance, reverse your stance just use $InputParameters instead ```ps
$InputParameters = $PSBoundParameters

Fundamentally you have no validation of the keys in that hashtable so you're potentially going to end up setting random variables (granted in local scope, but still).

It's not clear why you'd want to do the above to me personally.
stark nebula
#

ahh there is validation in the inputParameters, it was just too much to put in the example

#

and i want both ways to work, one for pros, other way for people who like -numbers "1234" -whatever

mellow harness
#

fwiw I do have stuff that has a similar pattern of accepting a dict or individual params to describe a thing. However, I model the "stuff I expect to receive" in a class, and I just push incoming values (no matter how I got them) at that class

#

I don't particularly see the benefit of binding back to variables, trying to back-fill is something of an ugly case in PS because it either needs you to explicitly list or to start hitting the variable cmdlets as you've got here

stark nebula
#

to complicate things further, the intent of this is to stuff into an APIcall that accept either parameterSet1 or parameterSet2, the apicall i have no control over the endpoint, so the function is made to make the apicall easier to make

so the people who do not know what they are doing can use the command:
$output = Test-Something -numbers "1234"

$output = Test-Something -letters "abcd" -colors "Green"

or, other folks like to pass objects so:

$output = Test-Something -InputParameters $params``` 

```$params = @{letters="1234"
    colors="Green"
}
$output = Test-Something -InputParameters $params``` 

and then inside `Test-Something` there is logic in there:
```$uri = "https://somelink"
if($letters){
    $body= @{ 
        letters = $letters
    }
}
else{
    $body= @{ 
        numbers= $numbers
        colors = $colors
    }
}

$apicall = invoke-restmethod -uri $uri -body $body```
#

oops sorry, edited message

#

i think i switched the parameterSets a bit, but I think you get the idea, let me know if unclear

mellow harness
#

heh too long. In two parts

#

My use case for this is a DSL I write. I accept input from JSON which is specifically why I need to support dict as input then fix it up.

If the use-case is just user input, encourage splatting instead as a first stance instead of doing your own thing that's like splatting, but not quite splatting.

#

If you still want to go ahead, I'd say your use case is an ideal case for a class, or just re-use of the hashtable.

#

Consider that you're accepting params or a hashtable, then you always create a hashtable so you can send it to your rest method. Using the incoming hashtable as-is is potentially problematic unless you have some very strong validation on keys (including case sensitivity). But if you were to:

#
class NumbersAndColors {
    [string] $numbers
    [string] $letters
    [string] $colors

    NumbersAndColors([System.Collections.IDictionary] $inputDict) {
        foreach ($key in $inputDict.Keys) {
            if (-not $this.PSObject.Properties[$key]) {
                # Just ignore it, handles common params
            }
            $this.$key = $inputDict[$key]
        }
    }

    [Hashtable] ToHashtable() {
        $out = @{}
        foreach ($property in $this.PSObject.Properties) {
            if ($null -eq $property.Value) {
                continue
            }
            $out[$property.Name] = $property.Value
        }
        return $out
    }
}
#

then I could use that like this: ```ps
function Test-Something{
[OutputType([String])]
[CmdletBinding(DefaultParameterSetName='Set1')]
Param(
[Parameter(Mandatory=$true,ParameterSetName='Set1')]
[String]$numbers,

    [Parameter(Mandatory=$false,ParameterSetName='Set2')]
    [String]$letters,

    [Parameter(Mandatory=$false,ParameterSetName='Set2')]
    [ValidateSet("Green","Blue")]
    [String]$colors,

    [Parameter(Mandatory=$true,ParameterSetName='set3')]
    [NumbersAndColors]$InputParameters
)

if ($PSCmdlet.ParameterSetName -ne 'set3') {
    $InputParameters = [NumbersAndColours]::new($PSBoundParameters)
}

<# stuff #>
$apicall = invoke-restmethod -uri $uri -body $InputParameters.ToHashtable()

}

stark nebula
#

i see

#

this is great

#

i need to do my homework on splatting

#

now would this strategy change if say user passes paramSet1 (numbers) and the code invokes another helper function to get letters and colors, but if user passes paramSet2 (letters and colors), ignore the helper function and jump straight to apicall

#

hmm i guess no it wouldnt change anything, just keep referencing the input hash as is

mellow harness
#

it's not necessarily the best way to go, it potentially reduces input validation when you're casting the hashtable. But there's things that can be done there too if necessary.

#

and just in case, the splatting version of this: ps $params = @{letters="1234" colors="Green" } $output = Test-Something -InputParameters $params is just ```ps
$params = @{letters="1234"
colors="Green"
}
$output = Test-Something @params

stark nebula
#

I see

#

i very much appreciate your insight, thank you

#

i have some homework to do 🙂

slim oar
next cedar
#

For what it's worth, I do consider New and Set-Variable an anti-pattern, unless you're using it for the sake of one of it's rarely used features, like setting dozens of variables to the same thing at once, or -Forceing the value of a readonly variable (which is probably an anti-pattern) or setting the description, options, or numeric scope, etc.

stark nebula
stark nebula
finite juniper
# next cedar For what it's worth, I _do_ consider New and Set-Variable an anti-pattern, unles...

Unfortunately, it is still required for common parameters and preference variables propagation workaround in modules (damn, it's been so long, they even changed the blog design! Can somebody ask them to edit the code blocks as well, since it seems to be a basic markdown conversion?):

Weekend Scripter: Access PowerShell Preference Variables

Summary: Windows PowerShell MVP, Dave Wyatt, talks about accessing caller preference variables. Microsoft Scripting Guy, Ed Wilson, is here. Today I would like to welcome a new guest blogger and new Windows PowerShell MVP, Dave Wyatt.      Dave has worked in the IT field for about 14 years as a software developer and a […]

astral knot
#

mmmm, that works without using the -Variable commands.

New-Module -name foo {
    function get-foo {
        [CmdletBinding()]
        param (
            [string]$name
        )
        if ([String]::IsNullOrEmpty($name)) {
            Write-Error -Message "Name is required"
        } else {
            Write-Host "Hello, $name!"
        }
        if ($VerbosePreference -eq 'Continue') {
            Write-Host 'Verbose is enabled'
        }
    }
} | Import-Module
Get-foo wooo # fine
Get-foo -ea 0 -verbose # suppress error
$VerbosePreference = 'continue'
$ErrorActionPreference = 'ignore'
Get-foo # also fine.. prints verbose.
next cedar
#

Not through two modules

#

That is, if you call command A from module mA and it calls B in module mB,

finite juniper
#

Yeah, the problem is with propagating from a caller's scope to descendant scopes, with global one being an exception. Doesn't even have to be a different module. It's enough to call it from a wrapper script.
When you call it casually from VS Code, it gets dot-sourced, so your script scope functions as a global one. However, if you have a wrapper around the module function call, the global scope is now different.

So, coming back to your example, @astral knot , let's call it Test-Module.ps1 and comment out all the function calls, except for the last one, Get-foo # also fine.. prints verbose.
Now create another script in the same folder (let's call it Test-ModuleWrapper.ps1) and put in just one line: .\Test-Module.ps1. These two ways of calling our new script behave differently:

Preference Variables (works as expected)

$VerbosePreference = 'Continue'
$ErrorActionPreference = 'Ignore'
./Test-ModuleWrapper.ps1

Common Parameters (doesn't work as expected)

$VerbosePreference = 'SilentlyContinue'  # Back to default
$ErrorActionPreference = 'Continue'      # Back to default
./Test-ModuleWrapper.ps1 -ErrorAction Ignore -Verbose

Most common use case is when you want to create a mini-environment or a temporary script to call the functions you're testing from within it and, not wanting to mess with global preference vars, you add common parameters now and then.
I think everyone working with PowerShell encounters this issue sooner or later, and it might take quite some time figuring out what is wrong and why. And it's not even immediately obvious what exactly you need to google in this case, especially for beginners.

So for now using the Set-Variable cmdlet is the only viable workaround I know.

slim oar
#

Is this a Web API? Sometimes pwsh classes are useful for web APIs.
Warning: They have quirks if you try to go into OOP like other languages.
It's best to keep it simple, and keep them in one script/module. (Dotsourcing across files makes it break )

Why use a class verses a hashtable or PSCO?
Imagine a a PSCO with fixed property names and types

  • you determine which property names are valid.
  • they always exist. you can set a default otherwise it's null
  • if passed, type constraints have some validation
  • for simple types it can directly convert to json

======

This next part, is part powershell 5+ ( screenshot 2 )
It feels like splatting, except it's constructing a new object instance.

How it relates to classes:

  • if you don't declare a constructor, you're able to cast from hashtables

So

using namespace System.Collections.Generic
class User { 
   [string] $Letters
   [double[]] $Numbers
   [string[]] $Colors = @('red')
}

Now you can use the [class] hashtable syntax

[List[Object]] $records = @(
    [User]@{} 

    [User]@{ Letters = 'abcd' } 

    [User]@{ Letters = 'should fail'; Numbers = (get-date)}

    [User]@{ 
       Letters = 'bar' 
       Numbers = 34.5, 36.3
    }
    [User]@{ 
       Letters = 'bar' 
       Numbers = -10
       Colors = 'red', 'blue', 'green'
    }
)

$records.count # 4
$records | ConvertTo-Json -Depth 9