I'm fairly new to powershell, and I'm just not getting how to modify a variable in a parent scope:
$val = 0
function foo()
{
$val = 10
}
foo
write "The number is: $val"
When I run it I get:
The number is: 0
I would like it to be 10. But powershell is creating a new variable that hides the one in the parent scope.
I've tried these, with no success (as per the documentation):
$script:$val = 10
$global:$val = 10
$script:$val = 10
But these don't even 'compile' so to speak.
What am I missing?
You don't need to use the global scope. A variable with the same name could have been already exist in the shell console and you may update it instead. Use the script scope modifier. When using a scope modifier you don't include the $ sign in the variable name.
$script:val=10
The parent scope can actually be modified directly with Set-Variable -Scope 1 without the need for Script or Global scope usage. Example:
$val = 0
function foo {
Set-Variable -scope 1 -Name "Val" -Value "10"
}
foo
write "The number is: $val"
Returns:
The number is: 10
More information can be found in the Microsoft Docs article About Scopes. The critical excerpt from that doc:
Note: For the cmdlets that use the Scope parameter, you can also refer to scopes by number. The number describes the relative position of one scope to another. Scope 0 represents the current, or local, scope. Scope 1 indicates the immediate parent scope. Scope 2 indicates the parent of the parent scope, and so on. Numbered scopes are useful if you have created many recursive scopes.
Be aware that recursive functions require the scope to be adjusted accordingly:
$val = ,0
function foo {
$b = $val.Count
Set-Variable -Name 'val' -Value ($val + ,$b) -Scope $b
if ($b -lt 10) {
foo
}
}
Let me point out a third alternative, even though the answer has already been made. If you want to change a variable, don't be afraid to pass it by reference and work with it that way.
$val=1
function bar ($lcl)
{
write "In bar(), `$lcl.Value starts as $($lcl.Value)"
$lcl.Value += 9
write "In bar(), `$lcl.Value ends as $($lcl.Value)"
}
$val
bar([REF]$val)
$val
That returns:
1
In bar(), $lcl.Value starts as 1
In bar(), $lcl.Value ends as 10
10
If you want to use this then you could do something like this:
$global:val=0
function foo()
{
$global:val=10
}
foo
write "The number is: $val"
Perhaps the easiest way is to dot source the function:
$val = 0
function foo()
{
$val = 10
}
. foo
write "The number is: $val"
The difference here being that you call foo via . foo.
Dot sourcing runs the function as normal, but it runs inside the parent scope, so there is no child scope. This basically removes scoping. It's only an issue if you start setting or overwriting variables/definitions in the parent scope unintentionally. For small scripts this isn't usually the case, which makes dot sourcing really easy to work with.
If you only want a single variable, then you can use a return value, e.g.,
$val = 0
function foo()
{
return 10
}
$val = foo
write "The number is: $val"
(Or without the return, as it's not necessary in a function)
You can also return multiple values to set multiple variables in this manner, e.g., $a, $b, $c = foo if foo returned 1,2,3.
The above approaches are my preferred way to handle cross-scope variables.
As a last alternative, you can also place the write in the function itself, so it's writing the variable in the same scope as it's being defined.
Of course, you can also use the scope namespace solutions, Set-Variable by scope, or pass it via [ref] as demonstrated by others here. There are many solutions in PowerShell.
Related
This question already has answers here:
Powershell Join-Path showing 2 dirs in result instead of 1 - accidental script/function output
(1 answer)
Why does Range.BorderAround emit "True" to the console?
(1 answer)
Create a Single-Element Json Array Object Using PowerShell
(2 answers)
Closed 1 year ago.
I am new to PowerShell and there is a weird behavior I cannot explain. I call a function that returns a [System.Collections.ArrayList] but when I print my variable that receives the content of the array, if I have one value(for example: logXXX_20210222_075234355.txt), then I get 0 logXXX_20210222_075234355.txt. The value 0 gets added for some reason as if it has the index of the value.
If I have 4 values, it will look like this:
0 1 2 3 logXXX_20210222_075234315.txt logXXX_20210225_090407364.txt
logXXX_20210204_120318221.txt logXXX_20210129_122737751.txt
Can anyone help?
Here is a simple code that does that:
function returnAnArray{
$arrayToReturn =[System.Collections.ArrayList]::new()
$arrayToReturn.Add('logICM_20210222_075234315.txt')
return $arrayToReturn
}
$fileNames = returnAnArray
Write-Host $fileNames
0 logICM_20210222_075234315.txt
It's characteristic of the ArrayList class to output the index on .Add(...). However, PowerShell returns all output, which will cause it to intermingle the index numbers with the true or other intended output.
My favorite solution is to simply cast the the output from the .Add(...) method to [Void]:
function returnAnArray{
$arrayToReturn = [System.Collections.ArrayList]::new()
[Void]$arrayToReturn.Add('logICM_20210222_075234315.txt')
return $arrayToReturn
}
You can also use Out-Null for this purpose but in many cases it doesn't perform as well.
Another method is to assign it to $null like:
function returnAnArray{
$arrayToReturn = [System.Collections.ArrayList]::new()
$null = $arrayToReturn.Add('logICM_20210222_075234315.txt')
return $arrayToReturn
}
In some cases this can be marginally faster. However, I prefer the [Void] syntax and haven't observed whatever minor performance differential there may be.
Note: $null = ... works in all cases, while there are some cases where [Void] will not; See this answer (thanks again mklement0) for more information.
An aside, you can use casting to establish the list:
$arrayToReturn = [System.Collections.ArrayList]#()
Update Incorporating Important Comments from #mklement0:
return $arrayToReturn may not behave as intended. PowerShell's output behavior is to enumerate (stream) arrays down the pipeline. In such cases a 1 element array will end up returning a scalar. A multi-element array will return a typical object array [Object[]], not [Collection.ArrayList] as seems to be the intention.
The comma operator can be used to guarantee the return type by making the ArrayList the first element of another array. See this answer for more information.
Example without ,:
Function Return-ArrayList { [Collections.ArrayList]#(1,2,3,4,5,6) }
$ArrReturn = Return-ArrayList
$ArrReturn.gettype().FullName
Returns: System.Object[]
Example with ,:
Function Return-ArrayList { , [Collections.ArrayList]#(1,2,3,4,5,6) }
$ArrReturn = Return-ArrayList
$ArrReturn.gettype().FullName
Returns: System.Collections.ArrayList
Of course, this can also be handled by the calling code. Most commonly by wrapping the call in an array subexpression #(...). a call like: $filenames = #(returnAnArray) will force $filenames to be a typical object array ([Object[]]). Casting like $filenames = [Collections.ArrayList]#(returnArray) will make it an ArrayList.
For the latter approach, I always question if it's really needed. The typical use case for an ArrayList is to work around poor performance associated with using += to increment arrays. Often this can be accomplished by allowing PowerShell to return the array for you (see below). But, even if you're forced to use it inside the function, it doesn't mean you need it elsewhere in the code.
For Example:
$array = 1..10 | ForEach-Object{ $_ }
Is preferred over:
$array = [Collections.ArrayList]#()
1..10 | ForEach-Object{ [Void]$array.Add( $_ ) }
Persisting the ArrayList type beyond the function and through to the caller should be based on a persistent need. For example, if there's a need easily add/remove elements further along in the program.
Still More Information:
Notice the Return statement isn't needed either. This very much ties back to why you were getting extra output. Anything a function outputs is returned to the caller. Return isn't explicitly needed for this case. More commonly, Return can be used to exit a function at desired points...
A function like:
Function Demo-Return {
1
return
2
}
This will return 1 but not 2 because Return exited the function beforehand. However, if the function were:
Function Demo-Return
{
1
return 2
}
This returns 1, 2.
However, that's equivalent to Return 1,2 OR just 1,2 without Return
Update based on comments from #zett42:
You could avoid the ArrayList behavior altogether by using a different collection type. Most commonly a generic list, [Collections.Generic.List[object]]. Technically [ArrayList] is deprecated already making generic lists a better option. Furthermore, the .Add() method doesn't output anything, thus you do not need [Void] or any other nullification method. Generic lists are slightly faster than ArrayLists, and saving the nullification operation a further, albeit still small performance advantage.
ArrayList appears to store alternating indexes and values:
PS /home/alistair> $filenames[0]
0
PS /home/alistair> $filenames[1]
logICM_20210222_075234315.txt
Got a question why logical function is not executed or evaluated at all when called from if-statement. tried with powershell version 4 and 7 in windows, both failed.
i have already worked around on this, but curious how could this happen.
can anyone please help? not sure whether this is any environment setup related, or powershell grammar issue. any opinions would be appreciated.
# example 1:
function isValidN{
echo “isValidN is called”
return 0 -lt 1;
}
function Main {
if(isValidN){
echo “t”
}else{
echo “f”
}
}
Main;
#result: it will not execute isValidN at all, it did not printout “isValidN is called”, Why?
#Example 2:
function isValidN{
echo “isValidN is called”
return 0 -lt 1;
}
function Main {
isValidN;
if(isValidN){
echo “t”
}else{
echo “f”
}
}
Main;
#result: the first isValidN call before if-statement would execute and printout “isValidN is called.” However, the 2nd one in the if-statement NOT. Why?
The crucial information is in the helpful comments by Santiago Squarzon and Mathias R. Jessen, but let me break it down conceptually:
As will become clear later, your isValidN function was called in the if statement, but produced no display output.
PowerShell functions and scripts do not have return values - they produce output that is typically written to the success output stream, PowerShell's analog to stdout (standard output).
In that sense, PowerShell functions more like a traditional shell rather than a typical programming language.
Unlike traditional shells, however, PowerShell offers six output streams, and targeting these streams is a way to output information that isn't data, such as debugging messages. Dedicated cmdlets exist to target each streams, as detailed in the conceptual about_Output_Streams help topic.
Any statement in a function or script may produce output - either implicitly, if output from a command call or the value of an expression isn't captured or redirected, or explicitly, with the - rarely necessary - Write-Output cmdlet, for which echo is a built-in alias.
The implication is that any script or function can have an open-ended number (multiple) "return values".
By contrast, Write-Host is meant for printing information to the host (display, terminal), and bypasses the success output stream (since version 5 of PowerShell, it writes to the information stream, which - as all streams do - goes to the host by default). As such, it is sometimes used as a quick way to provide debugging output, though it's generally preferable to use Write-Debug (for debugging only) or Write-Verbose (to provide extended information to the user on demand), but note that making their output visible requires opt-in.
Unlike in other languages, return is not needed to output data - unless there is a need to exit the scope at that point, its use is optional - and may be used independently of output statements; as syntactic sugar, data to output may be passed to return; e.g., return 0 -lt 1 is short for the following two separate statements 0 -lt 1; return: 0 -lt 1 outputs the value of the -lt operation, and return then exits the scope.
Applying the above to what you tried:
function isValidN {
echo "isValidN is called"
return 0 -lt 1
}
is the equivalent of:
function isValidN {
Write-Output "isValidN is called"
Write-Output (0 -lt 1)
return
}
Or, using implicit output, and omitting the redundant return call, given that the scope is implicitly exited at the end of the function:
function isValidN {
"isValidN is called"
0 -lt 1
}
Thus, your function outputs two objects: String "isValidN is called", and the evaluation of 0 -lt 1, which is $true
Using a command call (including scripts and functions) in an if conditional - captures all output from that command, so that:
if (isValidN) # ...
effectively becomes:
if (#("isValidN is called", $true)) # ...
That is, the two-object output from function isValidN, when captured, turned into a two-element array (of type [object[]]), which was implicitly evaluated as a Boolean ([bool]) due to its use with if.
PowerShell allows any object to be converted to [bool], and a 2+-element array always evaluates to $true, irrespective of its content - see the bottom section of this answer for a summary of PowerShell's to-Boolean conversion rules.
To summarize:
What you meant to print to the display, "isValidN is called", became part of the function's output ("return value"), "polluting" the function's output with incidental data, making the if conditional evaluate to $true always.
Because "isValidN is called" was part of the output, and that output was captured (and consumed) by the if conditional, it never printed to the display.
What I assume you meant do to:
function isValidN {
Write-Debug 'isValidN is called'
0 -lt 1
}
# Make Write-Debug output visible.
# To turn it back off:
# $DebugPreference = 'SilentlyContinue'
$DebugPreference = 'Continue'
# Call the function
if (isValidN) { 't' } else { 'f' }
Output:
DEBUG: isValidN is called
t
Note:
If you make a function or script an advanced one, you can turn debugging output on on a per-command call basis, by passing the common -Debug parameter, as an alternative to setting the $DebugPreference preference variable, which affects all commands in the current scope and descendant scopes.
I have multiple functions. By the end of the function they all need to go through exactly the same couple of lines of code, doing some logistic work. How can I define these couple of lines of code into a script block outside these functions and be able to use for all functions, either global reference or passed in as an argument(less preferred)? These lines of code will involve both global and local variables. The ideal is whenever I need to update the logistic work content, I can update it in one place, like script block definition, instead of using a function, which seems overkill for a few lines of codes. Thanks.
function A {
...
$var1 = $global:x + 1
Write-Host var1 value is $var1
}
function B {
...
$var1 = $global:x + 1
Write-Host var1 value is $var1
}
$global:x = 0
A
B
I am new to Powershell, but i am curious what is the best practice for creating default variable in Powershell. This is an example, which i am referencing. In case if you just want to initialize default variables, without intention to pass any parameters to function. Which is better way number 1 or 2 or none of them. :)
1.
function test
{
param ([int]$x = 5,[int]$y = 14)
$x * $y
}
2.
function test
{
[int]$x = 5
[int]$y = 14
$x * $y
}
It just depends on your use-case. If you truly never intend to change the variables, #2 is correct.
I think you just need to ask yourself what future use-cases might be. Would changing the values break your function? The ability to supply parameters is very useful, if not now, perhaps in the future.
Basically, if you're using the variables as FINAL, #2 is fine, but in all other cases I would say #1 is more correct.
If your intention is to take parameters to your function, use Param(). By default, undefined values will be $Null
Function Test
{
Param(
[Int]
$X = 5,
[Int]
$Y = 14
)
Return $X * $Y
}
Function Test2
{
$X=5; $Y=14
$X * $Y
}
> Test2
>> 70
> Test
>> 70
> Test 5 20
>> 100
I wrote this code:
$var = 0
$array= #(1,2,3)
function changeData(){
$var=1
$array[0]=100
}
changeData
Write-host $var
Write-host $array
When I run it I get:
0
100 2 3
I understand why the variable $var remains 0.
We cannot modify a variable in a parent scope unless we add global keyword as below:
function changeData(){
$global:var=1
$array[0]=100
}
However, I cannot understand why the $array changed into #(100,2,3).
The $array is in a parent scope too. But somehow it can be changed in function changeData().
Could you tell me why this difference occurs?
When you do: $array[0] = 100, it's equivalent to invoking this method to the object: $array.SetValue(100,0). Invoking a method on an object will not be restricted by the scope, so this will take effect.
Note that $array doesn't change after the invocation, and of course, doing this inside changeData() won't work either: $array = #(4, 5, 6)