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
Related
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.
What is the simplest way to check if invalid arguments are passed to the constructor method new?
use v6;
unit class Abc;
has Int $.a;
my $new = Abc.new( :b(4) );
The ClassX::StrictConstructor module should help. Install it with zef install ClassX::StrictConstructor and use it like so:
use ClassX::StrictConstructor;
class Stricter does ClassX::StrictConstructor {
has $.thing;
}
throws-like { Stricter.new(thing => 1, bad => 99) }, X::UnknownAttribute;
TLDR; If you are just worried about someone accidently mis-typing :a(4) as
:b(4), it might be better to just mark $.a as required.
class ABC {
has Int $.a is required;
}
ABC.new( :b(4) ); # error
# The attribute '$!a' is required, but you did not provide a value for it.
A quick hack would be to add a submethod TWEAK that makes sure any named values that you don't specify aren't there. It doesn't interfere with the normal workings of new and BUILD, so the normal type checks work without having to re-implement them.
class ABC {
has Int $.a;
submethod TWEAK (
:a($), # capture :a so the next won't capture it
*% # capture remaining named
() # make sure it is empty
) {}
}
A slightly more involved (but still hacky) way that should continue to work for subclasses, and that doesn't need to be updated with the addition of more attributes:
class ABC {
has Int $.a;
submethod TWEAK (
*%_ # capture all named
) {
# get the attributes that are known about
# (should remove any private attributes from this list)
my \accepted = say self.^attributes».name».subst(/.'!'/,'');
# ignore any that we know about
%_{|accepted}:delete;
# fail if there are any left
fail "invalid attributes %_.keys.List()" if %_
}
}
Write a custom new
Add this method declaration to your class:
method new ( :$a is required ) { callsame }
The :$a binds to a named argument named a (i.e. a key/value pair whose key is 'a', eg. a => 4).
The is required that follows the parameter name makes the a argument mandatory.
Now calls that do not pass a named argument named a will be rejected:
Abc.new( :b(4) ) ; # Error: Required named parameter 'a' not passed
The body of your new new method calls callsame. It calls the new that your class inherits, namely Mu's new. This routine walks through your class and its ancestors initializing attributes whose names correspond to named arguments:
Abc.new( :a(4) ) ; # OK. Initializes new object's `$!a` to `4`
Abc.new( :a(4) ).a.say ; # Displays `4`
UPD: See Brad's answer for a simpler approach that just adds the is required directly to the existing attribute declaration in the class:
has Int $.a is required; # now there's no need for a custom `new`
ABC.new( :b(4) ); # The attribute '$!a' is required...
Note the shift in the error message from Required named parameter 'a' not passed to attribute '$!a' is required.... This reflects the shift from adding a custom new with a required routine parameter that then gets automatically bound to the attribute, to just adding the is required directly to the attribute.
If that's not enough, read on.
The default new
Accepts any and all named arguments (pairs). It then passes them on to other routines that are automatically called during object construction. You passed the named argument :b(4) in your example code. It was accepted and passed on.
Rejects any and all positional arguments (not pairs).
The new call in your original code was accepted because you only passed a named argument, so no positional arguments, thus satisfying the argument validation directly done by the default new.
Rejecting unknown named arguments (as well as any positional arguments)
method new ( :$a!, *%rest ) { %rest and die 'nope'; callsame }
The *%rest "slurps" all named arguments other than one named a into a slurpy hash. If it is not empty then the die triggers.
See also timotimo's answer.
Requiring positional arguments instead of named
It's almost always both simpler and better to use named parameters/arguments to automatically initialize corresponding object attributes as shown above. This is especially true if you want to make it easy for folk to both inherit from your class and add attributes they also want initialized during new.
But if you wish to over-rule this rule-of-thumb, you can require one or more positional parameters/arguments and call methods on the new object to initialize it from the passed argument(s).
Perhaps the simplest way to do this is to alter the attribute so that it's publicly writable:
has Int $.a is rw;
and add something like:
method new ( $a ) { with callwith() { .a = $a; $_ } }
The callwith() routine calls the inherited new with no arguments, positional or named. The default (Mu) new returns a newly constructed object. .a = $a sets its a attribute and the $_ returns it. So:
my $new = Abc.new( 4 );
say $new.a ; # Displays `4`
If you don't want a publicly writable attribute, then leave the has as it was and instead write something like:
method !a ( $a ) { $!a = $a } # Methods starting `!` are private
method new ( $a ) { with callwith() { $_!a($a); $_ } }
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.
Im trying to modify a variable using upvar (in an upward stack), but the value of the variable is passed to the procedure and not the variable name.
I cannot change what is passed, since it is already implemented widely on the program.
Is there a way to modify the file name in some way ?
proc check_file_exists {name} {
upvar $name newName
check_exists $name #Do all checks that file is there
set newName $name_1
}
check_file_exists $name
puts $name
this code will print the old name of the file and not the new one.
What I think you should do is bite the bullet and change the calls. It's a fairly simple search-and-replace, after all. The code will be more sane than if you use any of the other solutions.
check_file_exists name
Or, you could add another parameter to the parameter list and use that to pass the name, making the first argument a dummy argument.
check_file_exists $name name
Or, if you're not using the return value, you could return the new value and assign it back:
set name [check_file_exists $name]
Or, you could assign the new value to a global variable (e.g. theValue) inside the procedure, and assign that back:
check_file_exists $name
# don't need this if you're in global scope
global theValue
set name $theValue
Or, you could assign the name to a global variable (e.g. theName) and access that inside the procedure: the procedure will be able to update name directly.
# don't need this if you're in global scope
global theName
set theName name
check_file_exists $name
(There are some variations on this f.i. using upvar.)
None of the alternatives are pretty, and all of them still require you to make a change at the call (except the last, if you only ever use one variable for this value). If you're adamant about not doing that, there's always Donal's info frame solution, which only requires the procedure itself to be changed.
Let me know if you want help with the procedure code for any of these alternatives.
This is quite difficult; it's really not the way you're supposed to work. But you can do it using info frame -1 (a facility usually used for debugging) to find out exactly how the current procedure was called. However, you need to be careful as the caller might be using the result of a command: this is an unsafe hack.
proc check_file_exists {name} {
set caller [dict get [info frame -1] cmd]
if {[regexp {^check_file_exists +\$(\w+)} $caller -> varName]} {
# OK, we were called with a simple variable name
puts "Called with variable $varName"
} else {
# Complicated case! Help...
return -code error "must be called with a simple variable's contents"
}
upvar 1 $varName newName
check_exists $name
set newName $name_1
}
Can someone tell me, why this function call does not work and why the argument is always empty ?
function check([string]$input){
Write-Host $input #empty line
$count = $input.Length #always 0
$test = ([ADSI]::Exists('WinNT://./'+$input)) #exception (empty string)
return $test
}
check 'test'
Trying to get the info if an user or usergroup exists..
Best regards
$input is an automatic variable.
https://technet.microsoft.com/ru-ru/library/hh847768.aspx
$Input
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.
Perhaps use a param block for parameters.
https://technet.microsoft.com/en-us/magazine/jj554301.aspx
Update: the problem seems to be fixed if you don't use $input as a parameter name, maybe not a bad thing to have proper variable names ;)
Also Powershell doesn't have return keyword, you just push the object as a statement by itself, this will be returned by function:
function Get-ADObjectExists
{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[string]
$ObjectName
)
#return result by just calling the object (no return statement in powershell)
([ADSI]::Exists('WinNT://./'+$ObjectName))
}
Get-ADObjectExists -ObjectName'test'