I am trying to figure out how to get the name of a powershell variable from the object, itself.
I'm doing this because I'm making changes to an object passed by reference into a function, so I don't know what the object will be and I am using the Set-Variable cmdlet to change that variable to read only.
# .__NEEDTOGETVARNAMEASSTRING is a placeholder because I don't know how to do that.
function Set-ToReadOnly{
param([ref]$inputVar)
$varName = $inputVar.__NEEDTOGETVARNAMEASSTRING
Set-Variable -Name $varName -Option ReadOnly
}
$testVar = 'foo'
Set-ToReadOnly $testVar
I've looked through a lot of similar questions and can't find anything that answers this specifically. I want to work with the variable entirely inside of the function--I don't want to rely on passing in additional information.
Also, while there may be easier/better ways of setting read-only, I have been wanting to know how to reliably pull the variable name from a variable for a long time, so please focus solving that problem, not my application of it in this example.
Mathias R. Jessen's helpful answer explains why the originating variable cannot be reliably determined if you only pass its value.
The only robust solution to your problem is to pass a variable object rather than its value as an argument:
function Set-ToReadOnly {
param([psvariable] $inputVar) # note the parameter type
$inputVar.Options += 'ReadOnly'
}
$testVar = 'foo'
Set-ToReadOnly (Get-Variable testVar) # pass the variable *object*
If your function is defined in the same scope as the calling code - which is not true if you the function is defined in a (different) module - you can more simply pass just the variable name and retrieve the variable from the parent / an ancestral scope:
# Works ONLY when called from the SAME SCOPE / MODULE
function Set-ToReadOnly {
param([string] $inputVarName)
# Retrieve the variable object via Get-Variable.
# This will implicitly look up the chain of ancestral scopes until
# a variable by that name is found.
$inputVar = Get-Variable $inputVarName
$inputVar.Options += 'ReadOnly'
}
$testVar = 'foo'
Set-ToReadOnly testVar # pass the variable *name*
As noted in this answer to a similar question, what you're asking (resolving the identity of a variable based on its value) can not be done reliably:
The simple reason being that contextual information about a variable
being referenced as a parameter argument will have been stripped away
by the time you can actually inspect the parameter value inside the
function.
Long before the function is actually called, the parser will have
evaluated the value of every single parameter argument, and
(optionally) coerced the type of said value to whatever type is
expected by the parameter it's bound to.
So the thing that is ultimately passed as an argument to the function
is not the variable $myVariable, but the (potentially coerced) value
of $myVariable.
What you could do for reference types is simply go through all variables in the calling scope and check if they have the same value:
function Set-ReadOnlyVariable {
param(
[Parameter(Mandatory=$true)]
[ValidateScript({ -not $_.GetType().IsValueType })]
$value
)
foreach($variable in Get-Variable -Scope 1 |Where-Object {$_.Value -ne $null -and $_.Value.Equals($value)}){
$variable.Options = $variable.Options -bor [System.Management.Automation.ScopedItemOptions]::ReadOnly
}
}
But this will set every single variable in the callers scope with that value to readonly, not just the variable you referenced, and I'd strongly recommend against this kind of thing - you're most likely doing something horribly wrong if you need to do this
Related
I've seen this, but neither answer actually answers the question using named argument in the call to the function
This is for Powershell 7. No workflow. And really Named Parameters this time! Also not covered in the standard documentation here .. Starting to think that this isn't possible.
this works, but it is requires brackets with positional arguments, not named arguments
Function MyFunction ([ref][string]$MyRefParam) {
$MyRefParam.Value = "newValue";
}
$myLocal = "oldValue"
Write-Host $myLocal # Outputs: oldValue
MyFunction ([ref]$myLocal);
Write-Host $myLocal # Outputs: newValue
Of course we all know that the best way to call a Powershell function is with named argument MyFunction -Arg1 23 rather than positional arguments MyFunction 23 or MyFunction(23) . These would not get through our Pull Requests!
But this doesn't work
Function MyFunction ([ref][string]$MyRefParam) {
$MyRefParam.Value = "newValue";
}
$myLocal = "oldValue"
Write-Host $myLocal # Outputs: oldValue
MyFunction -MyRefParam ([ref]$myLocal) # Outputs Cannot process argument transformation on parameter 'MyRefParam'. Reference type is expected in argument.
Write-Host $myLocal # Outputs: oldValue
Is there another way to provide the ref type in this syntax? So far I've tried combinations of [ref] and [ref][string] in both the param definition and in the call - I can't get anything to let Powershell see that I am really passing a Ref
Thanks for any help!
Use only a [ref] type constraint in your parameter declaration, i.e. remove the additional [string] type constraint (it doesn't do anything, and arguably shouldn't even be allowed - see bottom section):
Function MyFunction ([ref] $MyRefParam) {
$MyRefParam.Value = "newValue"
}
$myLocal = 'oldVaue'
MyFunction -MyRefParam ([ref] $myLocal)
$myLocal # -> 'newvalue'
You cannot type a [ref] instance: [ref] isn't a keyword that modifies a parameter declaration (as ref would be in C#), it is a type in its own right, System.Management.Automation.PSReference, and its value-holding property, .Value is of type object, i.e. it can hold any type of object.
The upshot:
You cannot enforce a specific data type when a value is passed via [ref].[1]
Conversely, you're free to assign a value of any type to the .Value property of the [ref] instance received.
Taking a step back:
[ref] is primarily intended for supporting calls to .NET APIs with ref / out parameters.
Its use in pure PowerShell code is unusual and best avoided, not least due to the awkward invocation syntax.
That said, the difference in behavior with your double type constraint between positional and named parameter binding is certainly curious - see Mathias R. Jessen's excellent explanation in the comments.
However, the real problem is that you're even allowed to define a multi-type constraint (unusual in itself) that involves [ref], because there appears to be an intentional check to prevent that, yet it doesn't surface in parameter declarations.
You can make it surface as follows, however (run this directly at the command line):
# ERROR, because [ref] cannot be combined with other type constraints.
PS> [ref] [string] $foo = 'bar'
Cannot use [ref] with other types in a type constraint
That the same check isn't enforced in parameter declarations has been reported in GitHub issue #16146.
[1] Behind the scenes, a generic type that is non-public is used when an actual value is passed with a [ref] cast (System.Management.Automation.PSReference<T>), which is instantiated with whatever type the value being cast is. However, this generic type's .Value property is still [object]-typed, allowing for modifications to assign any type.
I am familiar with creating read-only variables and constants in PowerShell by using the Set-Variable command with something like this:
Set-Variable -Option ReadOnly, AllScope -Name STANDARD_TOKEN_PARAMS -Force -Value #{
Username = 'username'
Password = 'password'
ClientId = 'clientID'
}
Or alternatively to sub ReadOnly with Constant for non-removable variables.
I assumed that with the ReadOnly option I would not be able to modify the collection, but this isn't the case. For example, $STANDARD_TOKEN_PARAMS.someNewEntry = 'newEntry' is valid and modifies the collection accordingly.
Are there similar commands that I could use to create a real 'ReadOnly' collection in PowerShell?
The options ReadOnly and Constant are variable (data-holder) concepts: they only prevent assigning a new value to the variable, they don't prevent modification of the value that a read-only/constant variable immutably stores.
To also prevent modification of the value (object) itself, you must additionally use a read-only data type[1] to store in the read-only/constant variable. To get a read-only hash table (dictionary), use the generic System.Collections.ObjectModel.ReadOnlyDictionary[TKey, TValue] type:
Set-Variable -Option ReadOnly, AllScope -Name STANDARD_TOKEN_PARAMS -Value $(
$dict = [System.Collections.Generic.Dictionary[string, string]]::new(
[System.StringComparer]::OrdinalIgnoreCase
)
$dict.Add('Username', 'username')
$dict.Add('Password', 'password')
$dict.Add('ClientId', 'clientID')
[System.Collections.ObjectModel.ReadOnlyDictionary[string, string]]::new($dict)
)
Note:
Unfortunately, you cannot directly initialize a ReadOnlyDictionary[TKey, TValue] instance from a PowerShell [hashtable], because a generic IDictionary-implementing instance with matching types is required; therefore, an auxiliary System.Collections.Generic.Dictionar[TKey, TValue] instance is used.
Note the [System.StringComparer]::OrdinalIgnoreCase argument passed to the aux. dictionary, which ensures that key lookups are case-insensitive, the way they are by default in PowerShell [hashtables].[2]
While the resulting $STANDARD_TOKEN_PARAMS read-only variable's value is then effectively also read-only, accidental attempts to modify the read-only dictionary result in error messages that aren't obvious, as of PowerShell Core 7.1:
$STANDARD_TOKEN_PARAMS['Username'] = 'foo' unhelpfully reports: Unable to index into an object of type "System.Collections.ObjectModel.ReadOnlyDictionary2[System.String,System.String]`
$STANDARD_TOKEN_PARAMS.Add('foo', 'bar') unhelpfully reports: Cannot find an overload for "Add" and the argument count: "2"
$STANDARD_TOKEN_PARAMS.Clear() unhelpfully reports: Cannot find an overload for "Clear" and the argument count: "0".
Only using dot notation provides a helpful error message: $STANDARD_TOKEN_PARAMS.Username = 'foo' reports Collection is read-only.
GitHub issue #15118 proposes improving these error messages.
[1] This isn't always necessary, namely not if the value is an instance of a by definition immutable .NET primitive type (a property-less type such as [int]) or a [string].
[2] Note that [hashtable] in Windows PowerShell and (obsolete) PowerShell Core versions 6.1 and below use [System.StringComparer]::CurrentCultureIgnoreCase, i.e, culture-sensitive lookups instead - see this GitHub issue for background information.
I've made a powershell script which validates some parameters. In the process of validation I need to create some strings. I also need these strings later in the script.
To avoid rebuilding the same strings again, can I reuse variables defined within validation blocks? Perhaps I can use functions in validation blocks somehow? Or maybe global variables? I'm not sure what's possible here, or what's good practice.
Example:
Test.ps1
Function Test {
param(
[string]
[Parameter(Mandatory=$true)]
$thing1
[string]
[Parameter(Mandatory=$true)]
$thing2
[string]
[Parameter(Mandatory=$true)]
[ValidateScript({
$a = Get-A $thing1
$b = Get-B $thing2
$c = $a + $b
$d = Get-D $c
if(-not($d -contains $_)) {
throw "$_ is not a valid value for the thing3 parameter."
}
return $true
})]
$thing3
)
# Here I'd like to use $c
# At worst, calling Get-A and Get-B again may be expensive
# Or it could just be annoying duplication of code
}
Bonus question, if this is possible, could I reuse those variables in a subsequent validation block?
You could use a byref varliable.
This will affect the variable being passed to it so you could both have a return value and a parameter affected by the execution of your function.
About Ref
You can pass variables to functions by reference or by value.
When you pass a variable by value, you are passing a copy of the data.
In the following example, the function changes the value of the
variable passed to it. In PowerShell, integers are value types so they
are passed by value. Therefore, the value of $var is unchanged outside
the scope of the function.
Function Test{
Param($thing1,$thing2,[ref]$c)
$c.Value = new-guid
return $true
}
#$ThisIsC = $null
test -c ([ref] $ThisIsC)
Write-Host $ThisIsC -ForegroundColor Green
Alternatively, you can use the $script or the $global scope.
For a simple script to quickly expose your variable, the $scriptscope will do just that. A byref parameter might be easier for the end-user if you intend to distribute your function by making it clear you need to pass a reference parameter.
See About Scopes documentation.
Scopes in PowerShell have both names and numbers. The named scopes
specify an absolute scope. The numbers are relative and reflect the
relationship between scopes.
Global: The scope that is in effect when PowerShell starts. Variables
and functions that are present when PowerShell starts have been
created in the global scope, such as automatic variables and
preference variables. The variables, aliases, and functions in your
PowerShell profiles are also created in the global scope.
Local: The current scope. The local scope can be the global scope or
any other scope.
Script: The scope that is created while a script file runs. Only the
commands in the script run in the script scope. To the commands in a
script, the script scope is the local scope.
Private: Items in private scope cannot be seen outside of the current
scope. You can use private scope to create a private version of an
item with the same name in another scope.
Numbered Scopes: You can refer to scopes by name or by a number that
describes the relative position of one scope to another. Scope 0
represents the current, or local, scope. Scope 1 indicates the
immediate parent scope. Scope 2 indicates the parent of the parent
scope, and so on. Numbered scopes are useful if you have created many
recursive scopes.
I've just noticed that if you are using ValidateSet for a parameter variable in a function you cannot within that function change the parameter variable to a value that is not in the set.
Here is a simple example to demonstrate:
Function Test {
[cmdletbinding()]
Param(
[ValidateSet(1,2,3)]
[int]$Number
)
$Number = 4
}
Test 3
Returns:
The variable cannot be validated because the value 4 is not a valid value for the Number variable.
I've used Get-Member to explore $Number and I can't see any indication of how (or why) it restricts the variable like this. I assume it's some sort of custom object or strong typing but the variable looks to be a System.Int32. Does anyone know how/why this happens?
This isn't unique to Int variables, this is just a simple example. I have found the same true for a String Array parameter.
I discovered from this related question: Find the values in ValidateSet that if I do the following inside my function:
(Get-Variable 'Number').Attributes.ValidValues
this lists the defined ValidateSet values. I therefore assume setting this attribute is how ValidateSet works, with the side effect being that it is then in effect throughout the life of the variable.
I am sure I read somewhere that there is an easy way to pass named parameters from a calling function to a called function without explicitly naming and specifying each parameter.
This is more than just reusing the position; I'm interested in the case where the name of the passed parameters is the same in some cases, but not in others.
I also think there is a way that is not dependent on position.
function called-func {
param([string]$foo, [string]$baz, [string]$bar)
write-debug $baz
write-host $foo,$bar
}
function calling-func {
param([int]$rep = 1, [string]$foo, [string]$bar)
1..$rep | %{
called-func -foo $foo -bar $bar -baz $rep ## <---- Should this be simpler?
}
}
calling-func -rep 10 -foo "Hello" -bar "World"
What would the method be, and is there a link?
I thought it might have been Jeffrey Snover, but I'm not sure.
In PowerShell v2 (which admittedly you may not be ready to move to yet) allows you to pass along parameters without knowing about them ahead of time:
called-func $PSBoundParameters
PSBoundParameters is a dictionary of all the parameters that were actually provided to your function. You can remove parameters you don't want (or add I suppose).
Well, I think I was confusing a blog post I read about switch parameters. As far as I can tell the best way is to just reuse the parameters like so:
called-func -foo:$foo -bar:$bar
How about
called-func $foo $bar