Why Changing local variable value affect the global copy - powershell

I have a function that returns the value of a global variable. When I assign this result to a local variable, and changing the local value the another variable is changing too.
Example:
function setGlobal
{
$temp = #{}
$temp.id = 50;
$Global:global1 = $temp;
return $Global:global1;
}
then I call this function, and set the result value:
$result = setGlobal
$result.id = 80
now both variables has the same value.
$Global:global1 # id = 60
$result # id = 60
How can I prevent this from happening? And why does changing the local value will affect the global copy?

The reason is, of course, that you don't have two copies of the object. You have two references to the same object, a hashtable. The reference is copied, not the object. The object is like a house. The variables (global1 and result) are like pieces of paper. I write the address of the house on my piece of paper (global1) and then I copy the address onto your piece of paper (result). Then you go to the address you have and paint the door red. Now when I go to the address I have, and my house now has a red door.
Making a deep copy of an object is slightly easier than making a copy of a house. For an arbitrary object use PSObject.Copy():
function setGlobal
{
$temp = #{}
$temp.id = 50;
$Global:global1 = $temp;
return $Global:global1.PSObject.Copy();
}
In this case, this is exactly the same as Clone since PSObject.Copy uses Clone if it is available.

This is happening because powershell is implicitly using references. So $Global:global1 and $result end up pointing to the same place when you make the assignment.
To get a copy, use the .Clone() method:
$result = $Global:global1.Clone()
Or instead of doing that on assignment, do it in the return of the function:
function setGlobal
{
$temp = #{}
$temp.id = 50;
$Global:global1 = $temp;
return $Global:global1.Clone();
}

Related

Powershell pass complex object By Value, not By Reference

I am trying to process some data in an ordered dictionary, then add that to another ordered dictionary, and I can do that by reinitializing my temporary dictionary, like this...
$collection = [Collections.Specialized.OrderedDictionary]::new()
foreach ($id in 1..5) {
$tempCollection = [Collections.Specialized.OrderedDictionary]::new()
foreach ($char in [Char]'a'..[Char]'e') {
$letter = ([Char]$char).ToString()
if ($id % 2 -eq 0) {
$letter = $letter.ToUpper()
}
$int = [Int][Char]$letter
$tempCollection.Add($letter, $int)
}
$collection.Add($id, $tempCollection)
}
foreach ($id in $collection.Keys) {
Write-Host "$id"
foreach ($key in $collection.$id.Keys) {
Write-Host " $key : $($collection.$id.$key)"
}
}
However, I feel like reinitializing is a bit inefficient/inelegant, and I would rather just .Clear() that temporary variable. Which leads to this...
$collection = [Collections.Specialized.OrderedDictionary]::new()
$tempCollection = [Collections.Specialized.OrderedDictionary]::new()
foreach ($id in 1..5) {
foreach ($char in [Char]'a'..[Char]'e') {
$letter = ([Char]$char).ToString()
if ($id % 2 -eq 0) {
$letter = $letter.ToUpper()
}
$int = [Int][Char]$letter
$tempCollection.Add($letter, $int)
}
$collection.Add($id, $tempCollection)
$tempCollection.Clear()
}
foreach ($id in $collection.Keys) {
Write-Host "$id"
foreach ($key in $collection.$id.Keys) {
Write-Host " $key : $($collection.$id.$key)"
}
}
The problem is that while simple objects like string, int, char, etc are passed by value, all complex objects like a dictionary are passed by reference. So I pass the SAME dictionary in every iteration of $collection.Add($id, $tempCollection) and the final state of $tempCollection is cleared, so the result is 5 empty members of $collection.
I know I can force something that is normally passed By Value to be By Reference using [Ref] as outlined here. And [Ref] is just an accelerator for System.Management.Automation.PSReference. So what I need is a way to force an argument By Value, but neither [Val] nor [ByVal] works, and searching for System.Management.Automation.PSValue doesn't seem to return anything useful either. The PSReference doco linked above says
This class is used to describe both kinds of references:
a. reference to a value: _value will be holding the value being referenced.
b. reference to a variable: _value will be holding a PSVariable
instance for the variable to be referenced.
which makes me think I can get to the Value somehow, but for the life of me I can't grok HOW. Am I on the right track, and just missing something, or am I misunderstanding this documentation completely?
Cloning also seems like a potential solution, i.e. $collection.Add($id, $tempCollection.Clone()), but Ordered Dictionaries don't implement ICloneable. .CopyTo() also isn't an option, since it doesn't necessarily maintain the order of the elements. Nor does .AsReadOnly() since
The AsReadOnly method creates a read-only wrapper around the current
OrderedDictionary collection. Changes made to the OrderedDictionary
collection are reflected in the read-only copy. Nor does OrderedDictionary implement .copy() as PSObject does.
I also tried making a new variable, like this...
$newCollection = $tempCollection
$collection.Add($id, $newCollection)
$tempCollection.Clear()
And that doesn't work either. So it seems that complex objects by reference seems to apply to more than just passed arguments.
It seems almost like my Ordered Dictionary choice/need is the root of the problem, but it seems like needing a unconnected copy of an Ordered Dictionary would not be such an edge case that it isn't supported.

Explicit Return in Powershell

I can write the following code in javascript:
function sum(num1, num2) {
return num1 + num2;
}
and then get a value
var someNum = sum(2,5);
I would like to do the same thing in Powershell, but I read the following guide:
PowerShell also knows the return keyword; however, it follows a
different logic. In general, the purpose of return is to end the
execution of a code section and to give the control back to the parent
block.
If you add a parameter to the return statement, the value will indeed
be returned to the calling subroutine. However, this also applies for
all other statements with an output. This means that any output
produced in the function will be stored in the variable together with
the return parameter.
I want to do this for the sake of having pure functions. However, it seems doing
var someNum = sum(2,5);
is entirely redundant, when I can just call the function above, define someNum inside of it, and it will be available in the global scope.
Am I missing something or is it possible to write pure functions in Powershell that don't return everything inside the function?
A bit tangential, but here is my actual code:
function GetPreviousKeyMD5Hashes() {
$query = "SELECT Name, MD5, executed FROM [AMagicDb].[dbo].cr_Scripts";
$command = New-Object System.Data.SQLClient.SQLCommand;
$command.Connection = $connection;
$command.CommandText = $query;
try {
$reader = $command.ExecuteReader();
while ($reader.Read()) {
$key = $reader.GetString(1)
$previousScripts.Add($key) | Out-Null
}
$reader.Close();
Write-Output "$(Get-Date) Finished querying previous scripts"
}
catch {
$exceptionMessage = $_.Exception.Message;
Write-Output "$(Get-Date) Error running SQL at with exception $exceptionMessage"
}
}
and then:
$previousScripts = New-Object Collections.Generic.HashSet[string];
GetPreviousKeyMD5Hashes;
This code isn't clear to me at all - running GetPreviousKeyMD5Hashes does set $previousScripts, but this is entirely unclear to whoever modifies this after me. My only other alternative (afaik) is to have all this in line, which also isn't readable.
is entirely redundant, when I can just call the function above, define someNum inside of it, and it will be available in the global scope.
No: functions execute in a child scope (unless you dot-source them with .), so variables created or assigned to inside a function are local to it.
Am I missing something or is it possible to write pure functions in Powershell that don't return everything inside the function?
Yes: The implicit output behavior only applies to statements whose output is neither captured - $var = ... - nor redirected - ... > foo.txt
If there are statements that happen to produce output that you'd like to discard, use $null = ... or ... > $null
Note: ... | Out-Null works in principle too, but will generally perform worse, especially in earlier PowerShell versions - thanks, TheIncorrigible1.
If there are status messages that you'd like to write without their becoming part of the output, use Write-Host or, preferably Write-Verbose or, in PSv5+, Write-Information, though note that the latter two require opt-in for their output to be visible in the console.
Do NOT use Write-Output to write status messages, as it writes to the success output stream, whose purpose is to output data ("return values").
See this answer of mine for more information about PowerShell's output streams.
The equivalent of your JavaScript code is therefore:
function sum($num1, $num2) {
Write-Host "Adding $num1 and $num2..." # print status message to host (console)
$num1 + $num2 # perform the addition and implicitly output result
}
PS> $someNum = sum 1 2 # NOTE: arguments are whitespace-separated, without (...)
Adding 1 and 2... # Write-Host output was passed through to console
PS> $someNum # $someNum captured the success output stream of sum()
3
Am I missing something or is it possible to write pure functions in Powershell that don't return everything inside the function?
You can't have your cake and eat it too...
If you have no out put in your function, then it is "pure" like you desire. If you have output, that also becomes part of the return.
You can use [ref] params. See below for example.
function DoStuff([ref]$refObj)
{
Write-Output "DoStuff: Enter"
$refObj.Value += $(1 + 2)
$refObj.Value += "more strings"
Write-Output "DoStuff: Exit"
}
$refRet = #()
$allRet = DoStuff([ref]$refRet)
"allRet"
$allRet
"refRet"
$refRet
"`n`nagain"
$allRet = DoStuff([ref]$refRet)
"allRet"
$allRet
"refRet"
$refRet
Note: Powershell doesn't need semicolons at the end of each statement; only for separating multiple statements on the same line.
Whenever possible, it's a good idea to avoid changing global state within a function. Pass input as parameters, and return the output, so you aren't tied to using the function in only one way. Your sample could look like this:
function sum
{
param($num1,$num2)
return $num1+$num2
}
$somenum=sum 2 5
Now, with Powershell, the return statement isn't needed. The result of every statement that isn't otherwise assigned, captured, redirected, or otherwise used, is just thrown in with the return value. So we could replace the return statement above with simply
$num1+$num2
You're already making use of this in your code with:
$previousScripts.Add($key) | Out-Null
where you are discarding the result of .Add(). Otherwise it would be included in the return value.
Personally, I find using return to explicitly mark the return value makes it easier to read. Powershell's way of putting all if the output in the return caused a lot of trouble for me as I was learning.
So, the only fixes to your code I would make are:
Move $previousScripts = New-Object Collections.Generic.HashSet[string] to inside the function, making it local.
Add return $previousScripts to the end of the function.

Powershell: Turn period delimited string into object properties

I have a string that looks something like this:
$string = "property1.property2.property3"
And I have an object, we'll call $object. If I try to do $object.$string it doesn't interpret it that I want property3 of property2 of property1 of $object, it thinks I want $object."property1.property2.property3".
Obviously, using split('.') is where I need to be looking, but I don't know how to do it if I have an unknown amount of properties. I can't statically do:
$split = $string.split('.')
$object.$split[0].$split[1].$split[2]
That doesn't work because I don't know how many properties are going to be in the string. So how do I stitch it together off of n amounts of properties in the string?
A simple cheater way to do this would be to use Invoke-Expression. It will build the string and execute it in the same way as if you typed it yourself.
$string = "property1.property2.property3"
Invoke-Expression "`$object.$string"
You need to escape the first $ since we don't want that expanded at the same time as $string. Typical warning: Beware of malicious code execution when using Invoke-Expression since it can do anything you want it to.
In order to avoid this you would have to build a recursive function that would take the current position in the object and pass it the next breadcrumb.
Function Get-NestedObject{
param(
# The object we are going to return a propery from
$object,
# The property we are going to return
$property,
# The root object we are starting from.
$rootObject
)
# If the object passed is null then it means we are on the first pass so
# return the $property of the $rootObject.
if($object){
return $object.$property
} else {
return $rootObject.$property
}
}
# The property breadcrumbs
$string = '"Directory Mappings"."SSRS Reports"'
# sp
$delimetedString = $String.Split(".")
$nestedObject = $null
Foreach($breadCrumb in $delimetedString){
$nestedObject = Get-NestedObject $nestedObject $breadcrumb $settings
}
$nestedObject
There are some obvious places where that function could be hardened and documented better but that should give you an idea of what you could do.
What's the use case here? You can split the string as you've described. This will create an array, and you can count the number of elements in the array so that n is known.
$string = "property1.property2.property3"
$split = $string.split('.')
foreach($i in 0..($split.Count -1)){
Write-Host "Element $i is equal to $($split[$i])"
$myString += $split[$i]
}

Convert data type of property for all objects in an array

Say I have an array of objects:
$a = #(
#{ Name = "A"; Value = "2016-01-02" },
#{ Name = "B"; Value = "2016-01-03" },
#{ Name = "C"; Value = "2016-01-04" }
)
The Value property is currently a String. I want to convert the Value property of each object to a DateTime. I could accomplish this with a for loop, but I was wondering if there is a more direct way to do it.
Yes, calculated properties.
$a | select #{N='Name';E={$_.Name}}, #{N='Value';E={ [datetime]$_.Value }}
This will change data type of the first object Value to datetime.
$a[0].value = [datetime]::ParseExact($a[0].value,'yyyy-MM-dd',$null)
And the loop from here :
foreach ($Obj in $a)
{
$Obj.Value = [datetime]::ParseExact($Obj.value,'yyyy-MM-dd',$null)
}
The #TessellatingHeckler 's answer, which you accepted, is not correct for your question! It is only change the representation of the data in the object, but not converted it as you ask. Even if you save it as a new object (or overwrite the original) this will change the object itself. You can see the difference if you run flowing code:
$b=$a | select #{N='Name';E={$_.Name}}, #{N='Value';E={ [datetime]$_.Value }}
#check the original and converted object
$a|gm
$b|gm

Powershell initializing global to $null is failing

I am running a GUI that takes user input but can be changed before finalizing if they make a mistake. When the form opens, I am trying to initialize the globals to null. This is failing. I put a breakpoint on the code and looked at the value before and then stepped into it. The value does not change.
So for example, if I run the form and enter "Foo" as my global variable, exit the form, then run the form again, even after the line in question executes, the value of the global is still "Foo". What is going on? I have used this exact code with other GUIs and it never failed (but the values were generated automatically rather than based on user input).
# Define and initialize global variables
$global:ServerName = $null # <-- This fails to reset the variable from the previous run of the form
function ValidateChoices(){
$OKToGo = $true
$TempServerName = $null
try {
# Only Allow Valid NETBios Name with AlphaNumberic and - up to 15 characters
[ValidatePattern("^[A-Za-z\d-]{1,15}$")]$TempServerName = $ServerNameTextbox.Text
$ServerNameTitle.BackColor = ""
$ServerNameTextbox.BackColor = ""
$global:ServerName = $TempServerName
} catch {
$OKToGo = $false
$ServerNameTitle.BackColor = "Pink"
$ServerNameTextbox.BackColor = "Pink"
}
...
if ( $OKToGo ){
"ServerName=" + $global:ServerName | Out-File c:\debug.txt
}
}
Here is the answer: When ValidatePattern is run against a variable, those restrictions are kept and re-evaluated anytime an attempt to change the variable is made. This holds true even if ValidatePattern was not explicitly called. And because it was a global variable, those restrictions rode through multiple iterations of the form. Because $null does not conform to my listed ValidatePattern parameters, the call to change the value was ignored