PowerShell Function parameters - by reference or by value? - function

So, I tried looking up the answer to this question, and found the generally available answer is that PowerShell passes parameters by value. These generally accepted solutions all post sample code to prove their assertions, similar to the following:
Function add1 ($parameter)
{
Write-Host " In Function: `$parameter = $parameter"
Write-Host " In Function: `$parameter += 1"
$parameter += 1
Write-Host " In Function: `$parameter = $parameter"
}
cls
$a = 1
Write-Host "Before function: `$a = $a"
add1 $a
Write-Host " After function: `$a = $a"
This gives the results:
Before function: Run Command: $a = 1
In Function: $parameter: 1
In Function: Run Command: $parameter += 1
In Function: $parameter: 2
After function: $a: 1
Thus proving that parameters are passed by value, right? Well, I was having a heck of a time troubleshooting a function I was writing. The function added a couple of additional NoteProperty items to a PSCustomObject I pass in to the function, and my program would throw all sorts of errors saying that the NoteProperty already existed, even though I had not modified the original object in the parent scope, only inside the function.
So, I set up a version of the above code to test using parameter of type [PSCustomObject], like so:
Function F1($Obj)
{
'Function F1: Run command: $Obj.FirstValue = 11'
$Obj.FirstValue = 11
" `$Obj.Name: $($StartObject.Name)"
" `$Obj.FirstValue: $($StartObject.FirstValue)"
" `$Obj.SecondValue: $($StartObject.SecondValue)"
}
Function F2($Obj)
{
'Function F2: Run command: $Obj | Add-Member -MemberType NoteProperty -Name SecondValue -Value 33'
$obj | Add-Member -MemberType NoteProperty -Name SecondValue -Value 33
" `$Obj.Name: $($StartObject.Name)"
" `$Obj.FirstValue: $($StartObject.FirstValue)"
" `$Obj.SecondValue: $($StartObject.SecondValue)"
}
cls
Remove-Variable StartObject
"Main script: Run command: `$StartObject = [PSCustomObject]#{Name='Original';FirstValue=22}"
$StartObject = [PSCustomObject]#{Name='Original';FirstValue=22}
" `$StartObject.Name: $($StartObject.Name)"
" `$StartObject.FirstValue: $($StartObject.FirstValue)"
" `$StartObject.SecondValue: $($StartObject.SecondValue)"
'Run command: F1 $StartObject'
" "
F1 $StartObject
" "
"Main script: `$StartObject.Name: $($StartObject.Name)"
" `$StartObject.FirstValue: $($StartObject.FirstValue)"
" `$StartObject.SecondValue: $($StartObject.SecondValue)"
"Run command: F2 $StartObject"
" "
F2 $StartObject
" "
"Main script: `$StartObject.Name = $($StartObject.Name)"
" `$StartObject.FirstValue = $($StartObject.FirstValue)"
" `$StartObject.SecondValue = $($StartObject.SecondValue)"
This messy piece of programming produces the following output:
Main script: Run command: $StartObject = [PSCustomObject]#{Name='Original';FirstValue=22}
$StartObject.Name: Original
$StartObject.FirstValue: 22
$StartObject.SecondValue:
Run command: F1 $StartObject
Function F1: Run command: $Obj.FirstValue = 11
$Obj.Name: Original
$Obj.FirstValue: 11
$Obj.SecondValue:
Main script: $StartObject.Name: Original
$StartObject.FirstValue: 11
$StartObject.SecondValue:
Run command: F2 #{Name=Original; FirstValue=11}
Function F2: Run command: $Obj | Add-Member -MemberType NoteProperty -Name SecondValue -Value 33
$Obj.Name: Original
$Obj.FirstValue: 11
$Obj.SecondValue: 33
Main script: $StartObject.Name = Original
$StartObject.FirstValue = 11
$StartObject.SecondValue = 33
These results clearly show that when [PSCustomObject] parameters are used, any modifications within the function take place on the passed object, thus pass by reference. This behavior happens regardless of defining my parameters as [PSCustomObject]$Obj, or leaving them untyped. This is not a huge problem in and of itself, but the problem is that I was unable to find this little gem of information in any of the documentation I looked through. I checked a few tutorial sites and Microsoft's own documentation on Function Parameters, but did not see this exception.
So, my question boils down to this: Has anyone found any documentation to support my theory that while most parameters default to passing by value, they are passed by reference when objects are concerned?
I am perfectly willing to believe that I missed some documentation somewhere, so please...point it out and show me the error of my ways! :)
Thanks very much

Note: The following also applies to assigning one variable to another: $b = $a ...
* makes $b reference the very same object that $a does if $a's value is an instance of a reference type,
* makes $b receive an independent copy of $a's value if the latter is an instance of a value type.
PowerShell uses by-(variable)-value passing by default; that is, the content of a variable is passed, not a reference to the variable itself.
Extra effort is needed if you want by-(variable)-reference passing, i.e. if you want to pass a reference to a variable itself, allowing the callee to both get the variable's content and to assign new content; in the simplest form, you can use a [ref]-typed parameter (akin to ref parameters in C#). However, note that this technique is rarely necessary in PowerShell.
Whether that content is a copy of what the caller sees or a reference to the very same object depends on the data type of the content:
If the content happens to be an instance of a .NET reference type - as [pscustomobject] is - that content is an object reference, and the callee can therefore potentially modify that object, by virtue of seeing the very same object as the caller.
If you want to pass a copy (clone) of a reference-type instance, note that there is no universal mechanism for creating one:
You can create copies of instances of types if they implement the System.ICloneable interface by calling their .Clone() method, but note that it is up to the implementing type whether to perform shallow or deep cloning[1]; it is for that reason that use of this interface is discouraged; in practice, types that do implement it typically perform shallow cloning, notably arrays, array lists (System.Collections.ArrayList) and hashtables (but note that an [ordered] hashtable (System.Collections.Specialized.OrderedDictionary) doesn't implement ICloneable at all.
Additionally, in PowerShell, you can call .psobject.Copy() on instances of type [pscustomobject] to create a shallow copy. (Do not use this method on objects of any other type, where it will effectively be a no-op.) Similarly, individual .NET types may implement custom cloning methods.
If, by contrast, that content is an instance of a .NET value type - e.g., [int] - or a
string[2], an independent copy of that instance is passed.
This distinction is fundamental to .NET, not specific to PowerShell; it is also how arguments are passed in C#, for instance.
To determine whether a given variable's value is an instance of a value type or a reference type, use something like the following:
1, (Get-Date), (Get-Item /) | # sample values
foreach {
'{0} is of type {1}; is it a value type? {2}' -f $_,
$_.GetType(),
$_.GetType().IsValueType
}
You'll see something like:
1 is of type System.Int32; is it a value type? True
4/30/2020 12:37:01 PM is of type System.DateTime; is it a value type? True
/ is of type System.IO.DirectoryInfo; is it a value type? False
If you look up the documentation for a given .NET type, say System.DateTime, the inheritance information will start with Object -> ValueType for value types; in C# terms, a value type is either a struct or an enum, whereas a reference type is a class.
Terminology and concepts:
There are two unrelated concepts at play here, and the fact that they both use the terms (by-)value and (by-)reference can get confusing:
By-(variable)-value vs. by-(variable)-reference parameter-passing is a data-holder (variable) concept:
It describes whether, on parameter passing, a variable's value is passed (by value) or a reference to the variable itself[3] (by reference).
Reference types vs. value types is purely a data concept:
That is, for technical reasons, any object in .NET is either an instance of a value type (stored on the stack) or a reference type (stored on the heap). Instances of the former are directly stored in variables, whereas the latter are stored by way of a reference. Therefore, copying a variable value - e.g., in the context of by-value parameter-passing - means:
either: making a copy of a value-type instance itself, resulting in an independent data copy.
or: making a copy of a reference-type instance reference; a copy of a reference still points to the same object, however, which is why even by-variable-value passed reference-type instances are directly seen by the callee (by way of their reference copy).
[1] Shallow cloning means that property values that are reference-type instances are copied as-is - as references - which means that the clone's property value again references the very same object as the original. Deep cloning means that such property values are cloned themselves, recursively. Deep cloning is expensive and isn't always possible.
[2] A string ([string]) is technically also an instance of a reference type, but, as an exception, it is treated like a value type; see this answer for the rationale behind this exception.
[3] Another way of thinking about it: a reference (pointer) to the location where the variable stores its value is passed. This allows the callee to not only access the variable's value, but also to assign a (new) value.

Related

Problem using Powershell to extract a value from a Json structure

Using Powershell, I get a json snippet returned into my First variable (this always works fine);
# Initialise variables...
$nMessage_id = "";
$whatStatusJsonContent = "";
# Function https://abc.googleapis.com/abc/load call here and returns...
$whatStatusJsonContent = '{"message_id":9093813071099257562}'
Then I call the convert function into a temp variable like this;
$ResponseBody = ConvertFrom-Json $whatStatusJsonContent;
which puts the Json into a nice little data structure like this;
message_id
----------
9093813071099257562
From which I can select the value I want by calling this;
$nMessage_id = $ResponseBody.message_id;
Usually, this works fine and I get the value into my second variable;
$nMessage_id = 9093813071099257562
The problem is: Sometimes I get nothing in $nMessage_id, even though $whatStatusJsonContent is definitely logged as having the Json returned correctly from the function.
My question is: Do I have to ConvertFrom-Json, or can I read it raw from the First variable..?
COMBINED SOLUTION: Thanks to #mklement() and #Bernard-Moeskops
# Initialise variables...
$nMessage_id = "";
$whatStatusJsonContent = "";
# Function https://abc.googleapis.com/abc/load call here and returns...
$whatStatusJsonContent = '{"message_id":9093813071099257562}'
$ResponseBody = ConvertFrom-Json $whatStatusJsonContent;
if($ResponseBody.message_id){
# ConvertFrom-Json got the value!
$nMessage_id = $ResponseBody.message_id
}else{
# ConvertFrom-Json didn't work!
$nMessage_id = = ($whatStatusJsonContent -split '[:}]')[1]
}
There's nothing overtly wrong with your code.
ConvertFrom-Json should work as expected and return a [pscustomobject] instance with a .message_id property.
In your example, the message_id JSON property value is a number that is an integer, for which ConvertTo-Json automatically chooses a suitable integer data type as follows: the smallest signed type >= [int] (System.Int32)[1] that can accommodate the value ([int] -> [long] (System.Int64) -> [decimal] (System.Decimal)); the caveat is that if the value can't even fit into a [decimal], an - inexact - [double] is used.[2]
With the sample JSON in your question, [long] is chosen.
In a follow-up comment you state:
The routine makes over 1000 calls/hour and for most of them the Json comes back and the $nMessage_id is yielded perfectly. Then, suddenly, the $nMessage_id is empty, even though the Json is logged as coming back fine. So, somewhere in the ConvertFrom-Json or $ResponseBody.message_id the value is going missing...
I have no explanation, but if - for whatever reason - ConvertFrom-Json is the culprit, you can try string manipulation as a workaround to extract the message ID and see if that helps:
$whatStatusJsonContent = '{"message_id":9093813071099257562}'
# Extract the message_id property value as a *string*
# (which you can cast to a numeric type if/as needed).
$message_id = ($whatStatusJsonContent -split '[:}]')[1]
The above stores a string with content 9093813071099257562 in $message_id; note that, as written, the input string must have the exact format as above with respect to whitespace; while it is possible to make the text parsing more robust, not having to worry about format variations is one good reason to use a dedicated parser such as ConvertFrom-Json.
Another option is to try a different JSON parser to see if that helps.
Json.NET is the preeminent JSON parser in the .NET world (which now underlies the JSON cmdlets in PowerShell Core):
$whatStatusJsonContent = '{"message_id":9093813071099257562}'
$message_id = [NewtonSoft.Json.Linq.JObject]::Parse($whatStatusJsonContent).message_id.Value
Note: Json.NET - like ConvetFrom-Json in PowerShell _Core - commendably uses the arbitrary large [bigint] type as well once a number is too large to fit into a [long].
Use of the Json.NET assembly has the added advantage of better performance than the ConvertFrom-Json cmdlet.
In PowerShell Core, you can run the above code as-is (the assembly is preloaded); in Windows PowerShell you'll have to download the package via the link above and add the assembly (NewtonSoft.Json.dll) to your session with Add-Type -LiteralPath.
[1] Curiously, in PowerShell Core, as of (at least) v6.2.0, the smallest type chosen is [long] (System.Int64).
[2] More helpfully, PowerShell Core, as of (at least) v6.2.0, creates an arbitrarily large [bigint] (System.Numerics.BigInteger) instance once a value doesn't fit into a [long] anymore; that is, the [decimal] type is skipped altogether.
You are going to have to convert it, so that PowerShell can understand it. It will convert from a string to a PSCustomObject. Just check by asking the type of the variable before and after.
$ResponseBody.message_id.GetType()
If sometimes the output is nothing, you could do something like:
if($ResponseBody.message_id){
$nMessage_id = $ResponseBody.message_id
}else{
throw "No message id found"
}
Hope this helps.

Can we declare a variable in tcl which behave kind of like instance variable in Java i.e. once declared can be accessed anywhere within a class?

I have tried this:
global svcCallWithOneChar ""
if {[catch {set "svcCallWithOneChar = [mimic.list :prefix \"l\"]"} errmsg]} {
puts $errmsg
} else {
puts "###### Svc Call With prefix set to Single Char \n $svcCallWithOneChar \n ##################"
}
but it produces this error
can't read "svcCallWithOneChar =
The reason is: It goes to else condition and then can't access the result stored in svcCallWithOneChar.
If you are doing object-oriented programming in Tcl, there are several options. In TclOO, you define an instance variable like this:
oo::class create Foo {
variable thevariable
...
}
The variable thevariable is now accessible in all methods in all instances of the Foo class.
As for your example code, I can't really figure out what you are trying to do. A couple of notes, though:
The arguments to the global command are variable names to be linked between the global scope and the current scope. Your invocation, if it is inside a procedure, creates two local variables: svcCallWithOneChar and "" (yes, that is a legal variable name).
The invocation set "svcCallWithOneChar = [mimic.list :prefix \"l\"]" is not an assignment. It's the single-argument form of set, so it tries to access the value of a variable named svcCallWithOneChar = foo (if foo is the return value of the mimic.list :prefix \"l\" invocation). If the latter invocation results in the empty string, the variable name becomes svcCallWithOneChar =, which is exactly what your error message says.
Also, even though mimic.list :prefix \"l\" is invoked within a double-quoted string, the text within brackets is not part of the string (the result of the invocation is embedded verbatim in the string, though). So escaping the double quotes in the second argument to mimic.list means that the command gets the argument "l", not l.

Pass strings by reference in Powershell?

How can I pass strings by reference to the parent scope?
This doesn't work since strings are not acceptable "values".
function Submit([ref]$firstName){
$firstName.value = $txtFirstName.Text
}
$firstName = $null
Submit([ref]$firstName)
$firstName
Error: "Property 'value' cannot be found on this object; make sure it exists and is settable"
Doing this doesn't give an error but it doesn't change the variable either:
$firstName = "nothing"
function Submit([ref]$firstName){
$firstName = $txtFirstName.Text
}
Submit([ref]$firstName)
$firstName
Edit:
Doing the first code block by itself works. However when trying to do it in my script it returns the error again. I fixed it enough for it to assign the variable and do what I want but it still throws up an error and I was wondering how to fix that. I think it's because it doesn't like variable;es changing during a running session. Here is a link to my script
https://github.com/InconspicuousIntern/Form/blob/master/Form.ps1
Your first snippet is conceptually correct and works as intended - by itself it does not produce the "Property 'Value' cannot be found on this object" error.
You're seeing the error only as part of the full script you link to, because of the following line:
$btnSubmit.Add_Click({ Submit })
This line causes your Submit function to be called without arguments, which in turn causes the $firstName parameter value to be $null, which in turn causes the error quoted above when you assign to $firstName.Value.
By contrast, the following invocation of Submit, as in your first snippet, is correct:
Submit ([ref] $firstName) # Note the recommended space after 'Submit' - see below.
[ref] $firstName creates a (transient) reference to the caller's $firstName variable, which inside Submit binds to (local) parameter variable $firstName (the two may, but needn't and perhaps better not have the same name), where $firstName.Value can then be used to modify the caller's $firstName variable.
Syntax note: I've intentionally placed a space between Submit and ([ref] $firstName) to make one thing clearer:
The (...) (parentheses) here do not enclose the entire argument list, as they would in a method call, they enclose the single argument [ref] $firstName - of necessity, because that expression wouldn't be recognized as such otherwise.
Function calls in PowerShell are parsed in so-called argument mode, whose syntax is more like that of invoking console applications: arguments are space-separated, and generally only need quoting if they contain special characters.
For instance, if you also wanted to pass string 'foo', as the 2nd positional parameter, to Submit:
Submit ([ref] $firstName) foo
Note how the two arguments are space-separated and how foo needn't be quoted.
As for an alternative approach:
[ref]'s primary purpose is to enable .NET method calls that have ref / out parameters, and, as shown above, using [ref] is nontrivial.
For calls to PowerShell functions there are generally simpler solutions.
For instance, you can pass a custom object to your function and let the function update its properties with the values you want to return, which naturally allows multiple values to be "returned"; e.g.:
function Submit($outObj){
$outObj.firstName = 'a first name'
}
# Initialize the custom object that will receive values inside
# the Submit function.
$obj = [pscustomobject] #{ firstName = $null }
# Pass the custom object to Submit.
# Since a custom object is a reference type, a *reference* to it
# is bound to the $outObj parameter variable.
Submit $obj
$obj.firstName # -> 'a first name'
Alternatively, you can just let Submit construct the custom object itself, and simply output it:
function Submit {
# Construct and (implicitly) output a custom
# object with all values of interest.
[pscustomobject] #{
firstName = 'a first name'
}
}
$obj = Submit
$obj.firstName # -> 'a first name'
Please try this out and see if you are getting the same results? It is working for me, and I really did not change much.
$txtFirstName = [PSCustomObject]#{
Text = "Something"
}
function Submit([ref]$var){
$var.value = $txtFirstName.Text
}
$firstName = $null
Submit([ref]$firstName)
$firstName

Declaring parameters does not work if anything is above param

I have a script with parameters. In order to ease the debug of the script I create a small function I found on the net to list all my variables. In order to do so, I start by getting all existing variables at the top of the script, then I create a function which compares recorded variables before and after getting parameters
Problem is when I put the $AutomaticVariables and the function before param declaration, PowerShell gives me the following error for any parameter where I set a default value. Is there anyway to workaround this … bug? If it's not a bug, why the hell this behavior. I don't see the point.
The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept assignments, such as a
variable or a property.
# Array and function to debug script variable content
$AutomaticVariables = Get-Variable
function check_variables {
Compare-Object (Get-Variable) $AutomaticVariables -Property Name -PassThru |
Where -Property Name -ne "AutomaticVariables"
}
param(
[String]$hostname,
[String]$jobdesc,
[String]$type = "standard",
[String]$repo,
[String]$ocred,
[String]$site,
[String]$cred = "SRC-$($site)-adm",
[String]$sitetype,
[String]$room,
[String]$chsite = "chub"
)
# TEST - Display variables
check_variables
As mentioned in the comments, you should gather the variables you want to exclude in the calling scope:
Define function (could as well be a script), notice the $DebugFunc parameter I've added at the end:
function Do-Stuff
{
param(
[String]$hostname,
[String]$jobdesc,
[String]$type = "standard",
[String]$repo,
[String]$ocred,
[String]$site,
[String]$cred = "SRC-$($site)-adm",
[String]$sitetype,
[String]$room,
[String]$chsite = "chub",
[scriptblock]$DebugFunc
)
if($PSBoundParameters.ContainsKey('DebugFunc')){
. $DebugFunc
}
}
Now, gather the variables and define your function, then inject it into Do-Stuff:
# Array and function to debug script variable content
$AutomaticVariables = Get-Variable
function check_variables {
Compare-Object (Get-Variable) $AutomaticVariables -Property Name -PassThru | Where -Property Name -ne "AutomaticVariables"
}
Do-Stuff -DebugFunc $Function:check_variables
It's not a bug. The param section defines the input parameter of your script thus has to be the first statement (same as with functions). There is no need to perform any action before the param block.
If you explain what you want to achieve with your check_variables (not what it does). We probably can show you how to do it right.

How do I pass a collection of object types to a function as a parameter in PowerShell?

Here is my code:
function Get-OSInfo {
param([string]$Computer)
$OS = gwmi -class Win32_OperatingSystem -computer $Computer
$OS | Add-Member –MemberType NoteProperty –Name OSType –Value ""
$OS.OSType = Get-OSType -Input $OS
write $OS
}
function Get-OSType {
param([?]$Input)
if ($Input.ProductType -eq 1) {
write "Client OS"
}
}
$blah = Get-OSInfo -Computer mypc
$blah | fl *
I realize that this could be done with a single function (or in the body of the script itself), but I have simplified the functions to highlight the trouble I'm having. What I want to do is pass the gwmi dataset from the Get-OSInfo function as a parameter variable in the Get-OSType so I can reference all of its properties in the second function without passing them individually from the first. Clear as mud?
I have tried multiple parameter accelerator types, [ref], [array], [object[]], etc., but I haven't found anything that works as a parameter. The only thing that has proven to work is to change the second function to use args[0] for accepting input, but that is not as clean as using parameters, and since it works, I can't help but think there is a parameter that should work as well.
Avoid using $input as that has special meaning in functions (representing pipeline input). Just rename the parameter to something like $OS.
The docs (man about_automatic_variables) on $input say:
Contains an enumerator that enumerates all input that is
passed to a function. The $input variable is available only to
functions and script blocks (which are unnamed functions). In the
Process block of a function, the $input variable enumerates the
object that is currently in the pipeline. When the Process block
completes, there are no objects left in the pipeline, so the $input
variable enumerates an empty collection. If the function does not
have a Process block, then in the End block, the $input variable
enumerates the collection of all input to the function.