Is it possible to create a non-modifiable hashtable in PowerShell? - powershell

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.

Related

powershell, how to safely store variable type for future use

I load some config from a JSON file and I want to be able to check what I've just loaded and potentially set some default values.
for now, I create a hash table containing info then load and check this way
$config_info=#{
foo=#{default='' ;type=[string]}
bar=#{default=#();type=[array] }
}
$config = Get-Content $configPath -Raw | ConvertFrom-Json
if(-not [Bool]$config.PSObject.Properties['foo']) {
$config | Add-Member -MemberType NoteProperty -Name 'foo' -Value $config_info.foo.default
}
if($config.foo -isnot $config_info.foo.type) { ... }
but I'm wondering how safe is the notation type=[string] and if I should rather use type=''.getType() witch looks less natural.
Update 1
in the past I used Export-Clixml/Import-Clixml for my config files but the resulting files are hardly human readable/ editable.
I also sometimes used dot-sourcing PS1 files but for the current project, config has to be maintain by non-powershell people.
What I'm wondering is, is type=[string] a safe/ lasting syntax to store ''.getType() in a variable, or not.
What I'm wondering is, is type=[string] a safe/ lasting syntax to store ''.getType() in a variable, or not.
Yes, assuming that the .NET [string] type (System.String) isn't being shadowed by a custom class definition of the same name (which would be unwise to do).
[string] is a PowerShell type literal, and referring to a .NET type this way works predictably, assuming the type has been loaded into the session, which is by definition true for built-in types such as System.String. You can refer to a .NET type:
by its accelerator name, if defined; such as [string] or [regex]; use the .FullName property to see the type's full name; e.g. [regex].FullName
by its full name, though note that you're free to omit the System. part of the namespace, e.g., you can refer to System.Text.Encoding as [System.Text.Encoding] or [Text.Encoding]
by its assembly-qualified name; e.g. (obtained with
[string].AssemblyQualifiedName):
[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]`
However:
This doesn't work across PowerShell editions, at least not for built-in types.
It is rarely necessary and can give the false impression that a given assembly version is being referenced (.NET seemingly loads whatever version is available)
However, in .NET (Core) / PowerShell (Core) only, you may omit the Version field; similarly, Culture and PublicKeyToken are optional.
While I doubt the need will arise, especially with respect to built-in types, at least hypothetically you can then disambiguate types with the same full name by their hosting assembly; e.g.:
# PS Core (v7+) only; the PublicKeyToken field may be omitted.
[System.String, System.Private.CoreLib, PublicKeyToken=7cec85d7bea7798e]
A simpler PowerShell (Core) 7+ solution:
In PowerShell (Core) 7+, ConvertFrom-Json has an -AsHashtable switch returns the parsing results as hashtables rather than as [pscustomobject] instances.
Given that you can cast from a hashtable to a custom class type literal, you can use strongly typed properties with default values that correspond to the properties in the input JSON data.
# Custom class that describes the structure of the config data,
# with strongly typed properties and default values.
class Config {
[string] $foo = '(none)'
[int[]] $bar = #()
}
# Sample JSON input; parse it into a hashtable.
$ht = #'
{
"bar": [1, 2]
}
'# | ConvertFrom-Json -AsHashtable
# Construct a [Config] instance from the values in the hashtable,
# enforcing data types, with on-demand conversion.
[Config] $ht
Output (note how foo has its default value):
foo bar
--- ---
(none) {1, 2}
Note:
PowerShell's flexible automatic type conversions are used when constructing the Config instance, and given that PowerShell's allows any data type to be converted to [string], non-string input for [string]-typed properties is still accepted (resulting in stringification).
When a value is of the wrong type and cannot be converted (e.g., a value of "abc" for an [int]-typed property), the [Config] cast results in a statement-terminating error.
If you need more stringent type-checking, consider using validation by a JSON schema, which Test-Json supports via the -Schema and -SchemaFile parameters, in PowerShell (Core) only, as zett42 suggests.

How to reference PSCredential.Empty in powershell script

I am trying to use empty credentials for anonymous access. How to I set a variable to empty credentials. I am trying to use PSCredential.Empty Property
Santiago Squarzon has provided the solution:
[pscredential]::Empty
is the PowerShell equivalent of C#'s PSCredential.Empty, i.e. access to the static Empty property of the System.Management.Automation.PSCredential type.
PowerShell's syntax for accessing .NET types and their - instance or static - member:
In order to refer to a .NET type by its literal name in PowerShell, you must specify it as a type literal, which is the type name enclosed in [...]
[pscredential] works as-is, even in the absence of a using namespace System.Management.Automation statement, because it happens to be a type accelerator, i.e. a frequently used type that you can access by a short, unqualified name (which can, but needn't be the same name as that of the type it references; e.g., [xml] is short for System.Xml.XmlDocument).
Generally, you either need a using namespace statement if you want to use the unqualified name only, or you need to use the namespace-qualified type name; e.g., [System.Management.Automation.PSCredential] or - given that the System. component is optional - [Management.Automation.PSCredential]
For a complete discussion of PowerShell's type literals, see this answer.
While instance members of .NET types are accessed with . (member-access operator), just like in C#, accessing static members requires a different operator: ::, the static member-access operator.
A distinct operator is necessary, because type literals are objects themselves, namely instances of System.Reflection.TypeInfo, so that using . accesses the latter's instance properties; e.g. [pscredential].FullName returns the type's namespace-qualified name.
Also note that, in keeping with PowerShell's general case-insensitive nature, neither type literals nor member names need be specified case-exactly.

Get-ItemProperty with Registry, returned object type

I can use Get-Item with folders, files and registry keys, and the type of the object I get back will make sense; [System.IO.DirectoryInfo], [System.IO.FileInfo] or [Microsoft.Win32.RegistryKey].
But with registry properties, what using Get-ItemProperty returns is a [System.Management.Automation.PSCustomObject]. Is this because there is no dedicated type for registry property? That seems odd. But my Google-Fu is not turning anything up.
My use case is this, I am doing a series of Copy and Move tasks, with all four item types potentially getting copied or moved, and I want to implement an option to rename an existing destination rather than overwriting or failing. And exactly what the rename options are depends on the object type. And from a readability standpoint, PSCustom Object or a simple else for RegistryProperty is a bit ugly. So, looking for a way to get the property back as a type with a more obvious name, so when I look at the code again in 12 months it makes some sense.
Get-ItemProperty returns what is conceptually a registry value object: a property of a registry key that has a name and a - uh... - value (the named value object's data).
The .NET registry API has no type to represent such a value object - instead, it allows access via the registry key type's .GetValue($valueName) (to get a specific value object's data[1]) and .GetValueNames() methods (to get the list of all value names).
The PowerShell implementers apparently chose not to implement their own .NET type, and chose to use PowerShell's general-purpose, dynamic "property-bag" type, [pscustomobject][2] to model these value objects.
If you want to avoid the [pscustomobject] instances that Get-ItemProperty returns, you can use Get-Item instead, which returns a Microsoft.Win32.RegistryKey instance, i.e. an instance of the .NET type representing a key, on which you can invoke the methods mentioned above.
As an aside: If you're just interested in a given value object's data, you can use the PSv5+
Get-ItemPropertyValue cmdlet (e.g.
Get-ItemPropertyValue HKCU:\Console -Name LineWrap directly returns the [int] data of the targeted value).
[1] Additionally, as js2010's answer shows, the .GetValueKind() method returns an enum value that identifies a given value object's registry-specific data type. These types imply what .NET types are used to represent them, as returned by .GetValue(), but in some cases have no direct equivalent (ExpandString, MultiString, Unknown) and require additional work to interpret them correctly.
[2] It is possible - but wasn't done in this case - to assign (one or more) self-chosen type names to [pscustomobject] instances, which PowerShell reflects as the type name in Get-Member output (only the first, if there are multiple) and which it respects for ETS type definitions and format-data definitions. However, such pseudo types are not accessible as type literals; e.g.: $obj = [pscustomobject] #{ PSTypeName = 'MyType'; prop = 'foo' } allows you test for this type name with $obj.pstypenames -contains 'MyType', but not with $obj -is [MyType]. That said, you can base parameter declarations on them, via the [PSTypeName()] attribute.
There is a way to get the type of the properties:
$key = get-item hkcu:\key1
$key.GetValueKind('value1')
DWord

How to define "named" parameter as [ref] in PowerShell (really this time)

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.

get powershell variable name from actual variable

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