I would need to make some decisions in PowerShell based on values in a hashtable.
Basically I need to assign users' UPN in Active Directory according to an attribute, the company one, and thought to create a hashtable containing keys and values like this
Key Value
Company1 #Company1.com
Company2 #Company2.com
The issue I am facing is I don't know how to tell PowerShell to use a value rather than another based on the company attribute, basically I don't know how to cycle/check the company attribute against the key in the hashtable.
I've tried to use switches like
$a -match $hashTable
or
$a -contains $hashtable
with little success.
I think a hashtable is what I need here, but I am of course open to any suggestion like using external files for the match as the number of the companies I need to match is rather high.
While Mathias already presented a proper solution I'd like to add some more explanation.
Hashtables and switch statements are essentially two different solutions to the same problem: transform an input value to a corresponding output.
A → "foo"
B → "bar"
C → "baz"
...
Hashtables are the simpler (and faster) approach, where you look up the result to the input value in a pre-defined table. Their advantage is that you can implement the associations in one place (e.g. a declaration section of your code where you define all your (static) data) and use them in a very simple manner anywhere else in your code:
$domains = #{
'Company1' = '#Company1.com'
'Company2' = '#Company2.com'
}
$name = 'foo'
$company = 'Company1'
$addr = $name + $domains[$company]
switch statements are more complicated in their handling, but also a lot more versatile. For instance, they allow different kinds of comparisons (wildcard, regular expression) in addition to simple lookups. They also allow providing default values/actions if a value is not listed. With hashtables you need to handle those cases with an additional if/else statement like Mathias showed, unless you're fine with the empty value that hashtables return when the lookup doesn't find a match.
$name = 'foo'
$company = 'Company1'
$domain = switch ($company) {
'Company1' { '#Company1.com' }
'Company2' { '#Company2.com' }
default { throw 'Unknown company' }
}
$addr = $name + $domain
-match and -contains are not "switches" but operators.
The -match operator takes a string as its left hand side argument and compares it to a regular expression pattern, which your $hashtable is a poor substitute for.
The -contains operator takes a collection (an array or list) as its left hand side argument and compares it to a scalar value on the right, to see if the collection, well contains the scalar value. Also not immediately applicable to your hashtable.
You can use the ContainsKey() method to test whether a given key exists, then use an indexer ($table[$key]) to extract the value:
$UPNSuffix = if($HashTable.ContainsKey($User.Company)){
# Company name exists in hashtable extract UPN suffix
$HashTable[$User.Company]
}
else {
# Not found, return default UPN suffix
"#default.company1.com"
}
Alternatively use the -contains operator on the Keys of the hashtable in place of ContainsKey():
$UPNSuffix = if($HashTable.Keys -contains $User.Company){
# Company name exists in hashtable extract UPN suffix
$HashTable[$User.Company]
}
else {
# Not found, return default UPN suffix
"#default.company1.com"
}
Related
Is there a way to add a method to built-in/native powershell object (or type?/class¿)?
In particular, I'm looking at you [hashtable], but I suppose my question is also a general one... for instance, I could see wanting to add functionality to all my arrays...
For instance, I would like all my [hashtable] objects to have the method: .AddAndOverwrite(..) which would replace the value of the key if it exists; otherwise it creates a new key.
The only way I seem to be able to do this is to:
create an empty hashtable, $HashTableNew
add the ScriptMethod(s) to $HashTableNew (i.e. .AddAndOverwrite(..))
then when I use it, make a copy of $HashTableNew
$SomeOtherHashTable = $HashTableNew.PSObject.Copy()
This just seems like not "the way"...
Note: I will admit, this is not the best example use of a data type extension method (as #SantiagoSquarzon points out)... but it is a simple one, and it allows for a simple example in the accepted answer; so I'm intentionally leaving it as is, rather than changing question / the extension method to .foo() returning widgets...
There is indeed a better and easier way to update a type as a whole by using Update-TypeData.
Here is an example that add an .AddOrOverwrite method to the hashtable.
$TypeParam = #{
TypeName = 'System.Collections.Hashtable'
MemberType = 'ScriptMethod'
MemberName = 'AddOrOverwrite'
Value = { Param($Key, $Value) $this.$key = $Value }
}
Update-TypeData #TypeParam -Force
$SomeHashTable.AddOrOverwrite('aaa','2222222')
$this, in the scriptblock of the method definition, correspond to the object reference that is targeted, in this case, the hashtable.
-Force will overwrite the definition every time without error stating the type was already added.
That method is not super useful as it does something that the hashtable manage pretty well on its own by just using assignment but it demonstrates how to do it.
Bonus example
Here's an example on how you would apply this principle and create 2 script properties (readonly) for a string so you can convert to base 64 back and forth.
$TypeParam = #{
TypeName = 'System.String'
MemberType = 'ScriptProperty'
Force = $true
}
Update-TypeData #TypeParam -MemberName 'Base64' -Value { [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($this)) }
Update-TypeData #TypeParam -MemberName 'Base64Decoded' -Value { [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($this)) }
# Encode the string to base 64 (Output: U29tZVN0cmluZw==)
"SomeString".Base64
# Decode the string from Base64 (Output: SomeString)
"U29tZVN0cmluZw==".Base64Decoded
References
Msdocs - About-Types
Dr Scripto - Easily Update Powershell Type Data by Using a Cmdlet
I have a function that can either initialize itself and return an ordered dictionary with initial values, or if a collection is provided as an argument, it manipulates that collection. And that collection is a key within a parent collection.
In my actual code I am seeing an odd behavior, where a key in the initial collection is initially $Null or has a specified value, but when I try to revise that value I do NOT get an error, but I also do not get a changed value. However, when I try creating a minimally functional example to post here, it does work correctly, both in the console and the IDE.
So, given this code
function Write-Data {
param (
[System.Collections.Specialized.OrderedDictionary]$Collection
)
foreach ($key in $Collection.Keys) {
try {
$type = $Collection.$key.GetType().FullName
} catch {
$type = 'NULL'
}
Write-Host "$key $type $($Collection.$key)"
}
Write-Host
}
function Manage-Data {
param (
[System.Collections.Specialized.OrderedDictionary]$Collection
)
if (-not $Collection) {
[System.Collections.Specialized.OrderedDictionary]$initialCollection = [Ordered]#{
initialNull = $null
initialString = 'initial string'
}
return $initialCollection
} else {
$Collection.initialNull = 'No longer null'
$Collection.initialString = 'New String'
}
}
CLS
$parentContainer = New-Object System.Collections.Specialized.OrderedDictionary
$parentContainer.Add('data', (Manage-Data))
Write-Data $parentContainer.data
Manage-Data -Collection $parentContainer.data
Write-Data $parentContainer.data
is there any obvious scenario where either of the lines revising values would not throw an error, but would also not change the value? For example, if there are actually more functions doing other things with that initialized collection object before the attempt to revise data? Or perhaps more generally, since I am depending on the default byReference behavior of complex objects, is there some situation where this behavior breaks down and I am effectively modifying a new complex object when I think I am modifying the original? Or is the fact that I am having problems with a simple data type within the complex type potentially the issue?
For what it is worth, the idea here is to basically be able to use Dependency Injection, but with functions rather than classes, and also mimic to some extent the the concept of a constructor and a method in a class, but again in a function. And that has generally been working well, if a little messy, which has reenforced in my mind that I need to move to classes eventually. But this particular issue has me worried that I will see the same problem in classes, and unless I can understand it now I will have issues. But since I can't seem to recreate the issue in a simplified example, I seem to be unable to figure anything out.
It occurs to me that one thing I haven't tried is to actually get the memory address of the collection I think I am modifying, or even of the individual key, so I can verify I actually am changing the same data that I initialized. But HOW to get the memory address of a variable escapes me, and is maybe not possible in PowerShell or .NET?
"the default byReference behavior of complex objects" concerns the properties of the object not the object itself:
The difference between (if):
[System.Collections.Specialized.OrderedDictionary]$initialCollection = [Ordered]#{
initialNull = $null
initialString = 'initial string'
}
and (else)
$Collection.initialNull = 'No longer null'
$Collection.initialString = 'New String'
Is that the later (else) statements indeed change the values of the parent values as $Collection refers to the same object as $parentContainer.data but the former (if) statement creates a new $initialCollection in the scope of the Manage-Data function which isn't visible in the parent (even if you assign it to $Collection, it would create a new object reference in the scope of the Manage-Data function).
You might return $initialCollection but then, how are you handling the different returns (either a $initialCollection or enumerable null ) in your parent function? Therefore I would just return $initialCollection for both conditions and reassign the object (where the properties are still by reference and only the $parentContainer.data reference will change/reset):
$parentContainer.data = Manage-Data -Collection $parentContainer.data
Potential problems
In other words, the potential issue in your Manage-Data function lies in the fact that parent function needs a different approach in calling it based on the condition if (-not $Collection) which is actually defined within the function. (What will be the value of this condition, as the caller already need to act differently on the condition?)
This leaves two pitfalls:
You call the function in the assumption that the argument is not a collection but it actually is:
$parentContainer = [Ordered]#{ data = [Ordered]#{} }
$parentContainer.Add('data', (Manage-Data))
In this case you get an error:
MethodInvocationException: Exception calling "Add" with "2" argument(s): "Item has already been added. Key in dictionary: 'data' Key being added: 'data'"
And the opposite (which is less obvious): you call the function in the assumption that the argument is a collection but it is actually not:
$parentContainer = [Ordered]#{}
Manage-Data $ParentContainer.Data
This will leave an unexpected object on the pipeline:
(See: PowerShell Pipeline Pollution)
Name Value
---- -----
initialNull
initialString initial string
And doesn't add anything to the $parentContainer object:
$parentContainer # doesn't return anything as it doesn't contain anything
Suggestions
See about scopes
Enable Set-StrictMode -Version Latest.
This will show you that a property is potentially empty.
Use ([ref]$MyVar).Value = 'new value' to replace a value in a parent scope.
(not related to the question) Use the IDictionary interface: [Collections.IDictionary]$Collection to accept a more general collection type in your functions.
I want to check the count of the items in a variable and change the value of the variable depending upon the count. Further, I want to use this function to validate other variables as well. Below mentioned is the code.
$c=#()
function Status($s)
{
if($s.Count -eq "0"){
$s = "Fail"
}else{
$s="success"
}
}
Status $c
Here I expected the value of $c would be "Fail". But instead the value remains to be null.
Without fiddling around with scopes or sending variables by reference, why don't you simply have the function return 'Fail' or 'Success'?
BTW The .Count property is of type Int32, so you should not surround the value 0 with quotes, making it a string.
function Get-Status($s) {
if($s.Count -eq 0) { 'Fail' } else { 'Success' }
}
Now, if you want to overwrite the variable $c with the outcome of whatever the function returns, simply do:
$c = #()
$c = Get-Status $c # will reset variable $c to the string value of 'Fail'
P.S. I renamed the function so it conforms to the Verb-Noun naming convention in PowerShell
If you want to change multiple variables in one function you need references or scopes. Scopes will change the variable with the same name inside the function and globally. Calling a variable by reference is indifferent to the variable names outside the function.
Reference:
Working with references your variable in the function needs to be of type [ref] ( or System.Management.Automation.PSReference ). In that case the argument you use must be cast to [ref] and to this before calling the function enclose the var with brackets ([ref]$c). When using references, you can't just change the variable itself, but you need to work with its .value. The value of your reference represents your original variable. ([ref]$s.Value -eq $c)
Using that your code would look like this:
$c=#()
function Status([ref]$s) #Define $s as [ref]. only arguments of type [ref] are valid
{
if($s.value.Count -eq 0)
{
$s.Value = "Fail" #This will change the type of $c from array to string
}
else
{
$s.Value += "success" #This will recreate the array with an additional object (string)
}
}
Status ([ref]$c) #The variable must be cast to [ref] to be valid
$c
Scopes:
Normally a function is executed in a lower scope than the rest of the script. That means variables only exist in their scope and in lower scopes and changes in lower scopes won't reflect to higher scopes. However, you can directly address a variable in another scope using $<scope>:var ($script:s). The downside is you work with the variable itself. The name of the variable inside the function and outside must be the same. (reading the help for scopes is highly recommended)
Here is your code with scopes:
$s=#() #var needs to have the same name
function Status #No parameter here
{
if($script:s.Count -eq "0")
{
$script:s = "Fail" #This will change the type of $s from array to string
}
else
{
$script:s += "success" #This will recreate the array with an additional object (string)
}
}
Status
$s
For a more global function, here is a “get the value of the specified variable” function.
# PowerShell
function getVar($name){
return (Get-Variable -Name $name).Value
}
The only problem with this is that if you have two variables with different scopes, the function may return the wrong variable, therefore the wrong value.
Here is a function to set a variable. It suffers from the same cons as above though.
# PowerShell
function setVar($name, $value){
Set-Variable -Name $name -Value $value
}
You can use the -Scope $scope part to help if you need to.
Happy coding!
I have a Hash Table like this:
$Path = #{
"BM" = "\\srv\xy"
"BB4-L" = "\\srv\xy"
"BB4-R" = "\\srv\xy"
"HSB" = "\\srv\xy"
"IB" = "\\srv\xy"
"LM1" = "\\srv\xy"
"LM2" = "\\srv\xy"
"sis" = "\\srv\xy"
}
my $env:username is sis. Why does .contains() and -contains something different?
PS Z:\Powershell-Scripts\Functions> $Path -contains $env:username
False
PS Z:\Powershell-Scripts\Functions> $Path.contains($env:username)
True
I always like to go with the PowerShell Syntax if possible, but I can't in this case, since -contains would return false.
How are .contains() and -contains different?
$Path is a System.Collections.Hashtable. You can also read in documentation that:
When the test value is a collection, the Contains operator uses
reference equality. It returns TRUE only when one of the reference
values is the same instance of the test value object.
Each item in hashtable is System.Collections.DictionaryEntry. You are comparing it to string. Since types do not match, references do not match as well. Contains(System.Object key) and ContainsKey(System.Object key) use keys to test. To be consistent in comparisons you should write:
$Path.Keys -contains $env:username
From MS documentation:
-Contains
Description: Containment operator. Tells whether a collection of reference
values includes a single test value. Always returns a Boolean value. Returns TRUE
only when the test value exactly matches at least one of the reference values.
.Contains() Method is one of the methods of a String object that supports substring hence why you get
True when you run $Path.Contains($env:username)
I got the code to work by changing from hashtable.containskey to -contains. In the following code snippit, i commented our the line containing .containsKey($UserIDFromAD) and added the line containing $UserIDsFromFile.keys -contains $UserIDFromAD. The code now works as expected.
So, whats the difference between .containskey and -contains?
#if ($UserIDsFromFile.containsKey($UserIDFromAD)){ #If the UserID is in Yesterdays list ignore it.
if ($UserIDsFromFile.keys -contains $UserIDFromAD){ #If the UserID is in Yesterdays list ignore it.
how does putting a comma before a list affect its type?
Have a look at the following code:
function StartProgram
{
$firstList = getListMethodOne
Write-Host "firstList is of type $($firstList.gettype())"
$secondList = getListMethodTwo
Write-Host "secondList is of type $($secondList.gettype())"
}
function getListMethodOne
{
$list = new-object system.collections.generic.list[string]
$list.Add("foo") #If there is one element, $list is of type String
$list.Add("bar") #If there is more than one element, $list is of type System.Object[]
return $list
}
function getListMethodTwo
{
$list = new-object system.collections.generic.list[string]
$list.Add("foo")
$list.Add("bar")
return ,$list #This is always of type List[string]
}
StartProgram
Why is it, if you don't use a comma before returning $list in getListMethodOne it returns as type System.Object[], whereas if you do use a comma as in getListMethodTwo, it is of type List[string] as expected?
PS: I'm using PSVersion 4.0
When you return collection, than PowerShell is kind enough to unravel it for you.
Unary comma creates collection with single element, so "external" collection gets unraveled and collection you want to return is kept.
I blogged about it a while ago.
Two more things:
return is used in PowerShell to leave function early, it's not needed to return something from function (any not captured output is returned)
in PowerShell 4.0 you can use Write-Output -NoEnumerate $collection to prevent unraveling your collection.
I don't have a full answer, but I would bet this relates to PowerShell's 'flattening' behaviour.
By using the unary operator ',' you are creating a new collection wrapper around the $list object. After PowerShell 'flattens' this, you see the object that was inside the wrapper.
A fuller explaination here: http://rkeithhill.wordpress.com/2007/09/24/effective-powershell-item-8-output-cardinality-scalars-collections-and-empty-sets-oh-my/