Get-Variable defined in a scriptblock from a psm function - powershell

I have the following piece of code:
$x = 'xyz'
& {
$y = 'abc'
foo
}
The foo function is defined in the foo.psm1 module which is imported before the script block is started.
Inside the foo function, I call Get-Variable which shows me x but it doesn't show y. I tried playing with the -Scope parameter: Local, Script, Global, 0 - which is the local scope from what I understood from the docs, 1 - which is the parent scope.
How could I get the y variable inside the foo function?
I'm not looking for a solution such as passing it as an argument. I want something as Get-Variable but sadly it doesn't see it for some reason.
UP
Based on the comments received, probably more context is needed.
Say that foo receives a ScriptBlock which is using the $using: syntax.
$x = 'xyz'
& {
$y = 'abc'
foo -ScriptBlock {
Write-Host $using:x
Write-Host $using:y
}
}
I'm 'mining' these variables as follows:
$usingAsts = $ScriptBlock.Ast.FindAll( { param($ast) $ast -is [System.Management.Automation.Language.UsingExpressionAst] }, $true) | ForEach-Object { $_ -as [System.Management.Automation.Language.UsingExpressionAst] }
foreach ($usingAst in $usingAsts) {
$varAst = $usingAst.SubExpression -as [System.Management.Automation.Language.VariableExpressionAst]
$var = Get-Variable -Name $varAst.VariablePath.UserPath -ErrorAction SilentlyContinue
}
This is how I'm using Get-Variable and in the case presented above, y cannot be found.

Modules run in their own scope domain (aka session state), which means they generally do not see the caller's variables - unless (a module-external) caller runs directly in the global scope.
For an overview of scopes in PowerShell, see the bottom section of this answer.
However, assuming that you define the function in your module as an advanced one, there is a way to access the caller's state, namely via the automatic $PSCmdlet variable.
Here's a simplified example, using a dynamic module created via the New-Module cmdlet:
# Create a dynamic module that defines function 'foo'
$null = New-Module {
function foo {
# Make the function and advanced (cmdlet-like) one, via
# [CmdletBinding()].
[CmdletBinding()] param()
# Access the value of variable $bar in the
# (module-external) caller's scope.
# To get the variable *object*, use:
# $PSCmdlet.SessionState.PSVariable.Get('bar')
$PSCmdlet.GetVariableValue('bar')
}
}
& {
$bar = 'abc'
foo
}
The above outputs verbatim abc, as desired.

Related

Why does the scope of variables change depending on if it's a .ps1 or .psm1 file, and how can this be mitigated?

I have a function that executes a script block. For convenience, the script block does not need to have explicitly defined parameters, but instead can use $_ and $A to refer to the inputs.
In the code, this is done as such:
$_ = $Value
$A = $Value2
& $ScriptBlock
This whole thing is wrapped in a function. Minimal example:
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
[Object]$Value2
)
$_ = $Value
$A = $Value2
& $ScriptBlock
}
If this function is written in a PowerShell script file (.ps1), but imported using Import-Module, the behaviour of F is as expected:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
15
PS>
However, when the function is written in a PowerShell module file (.psm1) and imported using Import-Module, the behaviour is unexpected:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
PS>
Using {$_ + 1} instead gives 1. It seems that $_ has a value of $null instead. Presumably, some security measure restricts the scope of the $_ variable or otherwise protects it. Or, possibly, the $_ variable is assigned by some automatic process. Regardless, if only the $_ variable was affected, the first unsuccessful example would return 1.
Ideally, the solution would involve the ability to explicitly specify the environment in which a script block is run. Something like:
Invoke-ScriptBlock -Variables #{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
In conclusion, the questions are:
Why can't script blocks in module files access variables defined in functions from which they were called?
Is there a method for explicitly specifying the variables accessible by a script block when invoking it?
Is there some other way of solving this that does not involve including an explicit parameter declaration in the script block?
Out of order:
Is there some other way of solving this that does not involve including an explicit parameter declaration in the script block?
Yes, if you just want to populate $_, use ForEach-Object!
ForEach-Object executes in the caller's local scope, which helps you work around the issue - except you won't have to, because it also automatically binds input to $_/$PSItem:
# this will work both in module-exported commands and standalone functions
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
)
ForEach-Object -InputObject $Value -Process $ScriptBlock
}
Now F will work as expected:
PS C:\> F -Value 7 -ScriptBlock {$_ * 2}
Ideally, the solution would involve the ability to explicitly specify the environment in which a script block is run. Something like:
Invoke-ScriptBlock -Variables #{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
Execute the scripblock using ScriptBlock.InvokeWithContext():
$functionsToDefine = #{
'Do-Stuff' = {
param($a,$b)
Write-Host "$a - $b"
}
}
$variablesToDefine = #(
[PSVariable]::new("var1", "one")
[PSVariable]::new("var2", "two")
)
$argumentList = #()
{Do-Stuff -a $var1 -b two}.InvokeWithContext($functionsToDefine, $variablesToDefine, $argumentList)
Or, wrapped in a function like your original example:
function F
{
param(
[scriptblock]$ScriptBlock
[object]$Value
)
$ScriptBlock.InvokeWithContext(#{},#([PSVariable]::new('_',$Value)),#())
}
Now you know how to solve your problem, let's get back to the question(s) about module scoping.
At first, it's worth noting that you could actually achieve the above using modules, but sort of in reverse.
(In the following, I use in-memory modules defined with New-Module, but the module scope resolution behavior describe is the same as when you import a script module from disk)
While module scoping "bypasses" normal scope resolution rules (see below for explanation), PowerShell actually supports the inverse - explicit execution in a specific module's scope.
Simply pass a module reference as the first argument to the & call operator, and PowerShell will treat the subsequent arguments as a command to be invoked in said module:
# Our non-module test function
$twoPlusTwo = { return $two + $two }
$two = 2
& $twoPlusTwo # yields 4
# let's try it with explicit module-scoped execution
$myEnv = New-Module {
$two = 2.5
}
& $myEnv $twoPlusTwo # Hell froze over, 2+2=5 (returns 5)
Why can't script blocks in module files access variables defined in functions from which they were called?
If they can, why can't the $_ automatic variable?
Because loaded modules maintain state, and the implementers of PowerShell wanted to isolate module state from the caller's environment.
Why might that be useful, and why might one preclude the other, you ask?
Consider the following example, a non-module function to test for odd numbers:
$two = 2
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
If we run the above statements in a script or an interactive prompt, subsequently invocating Test-IsOdd should yield the expected result:
PS C:\> Test-IsOdd 123
True
So far, so great, but relying on the non-local $two variable bears a flaw in this scenario - if, somewhere in our script or in the shell we accidentally reassign the local variable $two, we might break Test-IsOdd completely:
PS C:\> $two = 1 # oops!
PS C:\> Test-IsOdd 123
False
This is expected since, by default, variable scope resolution just wanders up the call stack until it reaches the global scope.
But sometimes you might require state to be kept across executions of one or more functions, like in our example above.
Modules solve this by following slightly different scope resolution rules - module-exported functions defer to something we call module scope (before reaching the global scope).
To illustrate how this solves our problem from before, considering this module-exported version of the same function:
$oddModule = New-Module {
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
$two = 2
}
Now, if we invoke our new module-exported Test-IsOdd, we predictably get the expected result, regardless of "contamination" in the callers scope:
PS C:\> Test-IsOdd 123
True
PS C:\> $two = 1
PS C:\> Test-IsOdd 123 # still works
True
This behavior, while maybe surprising, basicly serves to solidify the implicit contract between the module author and the user - the module author doesn't need to worry too much about what's "out there" (the callers session state), and the user can expect whatever going on "in there" (the loaded module's state) to work correctly without worrying about what they assign to variables in the local scope.
Module scoping behavior poorly documented in the help files, but is explained in some depth in chapter 8 of Bruce Payette's "PowerShell In Action" (ISBN:9781633430297)

Delay-bind script block does not work when function is exported from module

I have following function:
function PipeScript {
param(
[Parameter(ValueFromPipeline)]
[Object] $InputObject,
[Object] $ScriptBlock
)
process {
$value = Invoke-Command -ScriptBlock $ScriptBlock
Write-Host "Script: $value"
}
}
When I define this function directly in script and pipe input into it I get following result which is expected:
#{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name }
# Outputs: "Script: Test"
But when I define this function inside module and export it with Export-ModuleMember -Function PipeScript then pipeline variable $_ inside script block is always null:
Import-Module PipeModule
#{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name }
# Outputs: "Script: "
Full repro is available at: https://github.com/lpatalas/DelayBindScriptBlock
Can someone explain this behaviour?
Tip of the hat to PetSerAl for all his help.
Here's a simple solution, but note that it runs the script block directly in the caller's scope, i.e. it effectively "dot-sources", which allows modification of the caller's variables.
By contrast, your use of Invoke-Command runs the script block in a child scope of the caller's scope - if that is truly the intent, see the variant solution below.
"Dot-sourcing" the script block is also what standard cmdlets such as Where-Object and ForEach-Object do.
# Define the function in an (in-memory) module.
# An in-memory module is automatically imported.
$null = New-Module {
function PipeScript {
param(
[Parameter(ValueFromPipeline)]
[Object] $InputObject
,
[scriptblock] $ScriptBlock
)
process {
# Use ForEach-Object to create the automatic $_ variable
# in the script block's origin scope.
$value = ForEach-Object -Process $ScriptBlock -InputObject $InputObject
# Output the value
"Script: $value"
}
}
}
# Test the function:
$var = 42; #{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name; ++$var }
$var # -> 43 - the script block ran in the caller's scope.
The above outputs string Script: Test and 43 afterwards, proving that the input object was seen as $_ and that dot-sourcing worked ($var was successfully incremented in the caller's scope).
Here's a variant, via the PowerShell SDK, that runs the script block in a child scope of the caller's scope.
This can be helpful if you don't want the execution of the script block to accidentally modify the caller's variables.
It is the same behavior you get with the engine-level delay-bind script-block and calculated-property features - though it's unclear whether that behavior was chosen intentionally.
$null = New-Module {
function PipeScript {
param(
[Parameter(ValueFromPipeline)]
[Object] $InputObject
,
[scriptblock] $ScriptBlock
)
process {
# Use ScriptBlock.InvokeContext() to inject a $_ variable
# into the child scope that the script block runs in:
# Creating a custom version of what is normally an *automatic* variable
# seems hacky, but the docs do state:
# "The list of variables may include the special variables
# $input, $_ and $this." - see https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.invokewithcontext
$value = $ScriptBlock.InvokeWithContext(
$null, # extra functions to define (none here)
[psvariable]::new('_', $InputObject) # actual parameter type is List<PSVariable>
)
# Output the value
"Script: $value"
}
}
}
# Test the function:
$var = 42
#{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name; ++$var }
$var # -> 42 - unaltered, because the script block ran in a child scope.
The above outputs string Script: Test, followed by 42, proving that the script block saw the input object as $_ and that variable $var - although seen in the script block, was not modified, due to running in a child scope.
The ScriptBlock.InvokeWithContext() method is documented here.
As for why your attempt didn't work:
Generally, script blocks are bound to the scope and scope domain in which they are created (except if they're created expressly as unbound script blocks, with [scriptblock]::Create('...')).
A scope outside of a module is part of the default scope domain. Every module has its own scope domain, and except for the global scope, which all scopes across all scope domains see, scopes in different scope domains do not see one another.
Your script block is created in the default scope domain, and when the module-defined function invokes it, the $_ is looked for in the scope of origin, i.e., in the (non-module) caller scope, where it isn't defined, because the automatic $_ variable is created by PowerShell on demand in the local scope, which is in the enclosing module's scope domain.
By using .InvokeWithContext(), the script block runs in a child scope of the caller's scope (as would be the case with .Invoke() and Invoke-Command by default), into which the above code injects a custom $_ variable so that the script block can reference it.
Providing better SDK support for these scenarios is being discussed in GitHub issue #3581.

How do I implement output variables in powershell?

I realize "output variables" is probably the wrong terminology here which is why my google searching has failed me. I'm guessing it revolves around explicitly setting variable scope, but I've tried reading the about_Scopes doc and it's just not clicking for me.
Essentially what I'm trying to do is implement the equivalent of the -SessionVariable argument from Invoke-RestMethod in my own module's function. In other words, I need a string parameter that turns into a variable in the caller's scope.
function MyTest
{
param([string]$outvar)
# caller scoped variable magic goes here
set-variable -Name $outvar -value "hello world"
# normal function output to pipeline here
Write-Output "my return value"
}
# calling the function initially outputs "my return value"
MyTest -outvar myvar
# referencing the variable outputs "hello world"
$myvar
For bonus points, how do things change (if at all) if I'm wrapping an existing function that has its own output variable and I want to effectively pass through the output variable name?
function MyWrapper
{
param([string]$SessionVariable)
Invoke-RestMethod -Uri "http://myhost" -SessionVariable $SessionVariable
# caller scoped variable magic goes here
set-variable -Name $SessionVariable -value $SessionVariable
}
# calling the wrapper outputs the normal results from Invoke-RestMethod
MyWrapper -SessionVariable myvar
# calling the variable outputs the WebRequestSession object from the inner Invoke-RestMethod call
$myvar
P.S. If it matters, I'm trying to keep the module compatible with Powershell v3+.
There's no need to attempt to implement this yourself, -OutVariable is a Common Parameter.
Add a CmdletBinding attribute to your param block to get these for free:
function MyTest {
[CmdletBinding()]
param()
return "hello world"
}
MyTest -OutVariable testvar
$testvar now holds the string value "hello world"
For your second example, where you need to set a value in the caller scope in addition to pipeline output, use the -Scope option with Set-Variable:
function MyTest {
[CmdletBinding()]
param([string]$AnotherVariable)
if([string]::IsNullOrWhiteSpace($AnotherVariable)){
Set-Variable -Name $AnotherVariable -Value 'more data' -Scope 1
}
return "hello world"
}
MyTest -AnotherVariable myvar
$myvar in the caller scope now contains the string value "more data". The 1 value passed to the Scope parameter means "1 level up"
#Mathias R. Jessen propose solution, which work well, when not defined in module. But not, if you put it in module. So, I provide another solution, which works, when put in module.
In advanced function (one which use [CmdletBinding()] attribute) you can use $PSCmdlet.SessionState to refer to SessionState of a caller. Thus, you can use $PSCmdlet.SessionState.PSVariable.Set('Name' ,'Value') to set variables in caller's SessionState.
Note: this solution will not work if not defined in module or if called from the same module, where it defined.
New-Module {
function MyTest1 {
[CmdletBinding()]
param([string]$outvar)
$PSCmdlet.SessionState.PSVariable.Set($outvar, 'Some value')
}
function MyTest2 {
[CmdletBinding()]
param([string]$outvar)
# -Scope 1 not work, because it executed from module SessionState,
# and thus have separate hierarchy of scopes
Set-Variable -Name $outvar -Value 'Some other value' -Scope 1
}
function MyTest3 {
[CmdletBinding()]
param([string]$outvar)
# -Scope 2 will refer to global scope, not the caller scope,
# so variable with same name in caller scope will hide global variable
Set-Variable -Name $outvar -Value 'Some other value' -Scope 2
}
} | Out-Null
& {
$global:a = 'Global value'
MyTest1 a
$a
$global:a
''
MyTest2 a
$a
$global:a
''
MyTest3 a
$a
$global:a
}

Cannot modify a script-scoped variable from inside a function

I am currently making a script which is supposed to connect to 42 different local servers and getting the Users of a specific group (fjärrskrivbordsanvändare(Remote desktop users in swedish :D)) from active directory. After it has gotten all the users from the server it has to export the users to a file on MY desktop
The csv file has to look like this:
Company;Users
LawyerSweden;Mike
LawyerSweden;Jennifer
Stockholm Candymakers;Pedro
(Examples)
etc.
Here's the code as of now:
cls
$MolnGroup = 'fjärrskrivbordsanvändare'
$ActiveDirectory = 'activedirectory'
$script:CloudArray
Set-Variable -Name OutputAnvandare -Value ($null) -Scope Script
Set-Variable -Name OutputDomain -Value ($null) -Scope Script
function ReadInfo {
Write-Host("A")
Get-Variable -Exclude PWD,*Preference | Remove-Variable -EA 0
if (Test-Path "C:\file\frickin\path.txt") {
Write-Host("File found")
}else {
Write-Host("Error: File not found, filepath might be invalid.")
Exit
}
$filename = "C:\File\Freakin'\path\super.txt"
$Headers = "IPAddress", "Username", "Password", "Cloud"
$Importedcsv = Import-csv $filename -Delimiter ";" -Header $Headers
$PasswordsArray += #($Importedcsv.password)
$AddressArray = #($Importedcsv | ForEach-Object { $_.IPAddress } )
$UsernamesArray += #($Importedcsv.username)
$CloudArray += #($Importedcsv.cloud)
GetData
}
function GetData([int]$p) {
Write-Host("B")
for ($row = 1; $row -le $UsernamesArray.Length; $row++)
{
# (If the customer has cloud-service on server, proceed)
if($CloudArray[$row] -eq 1)
{
# Code below uses the information read in from a file to connect pc to server(s)
$secstr = New-Object -TypeName System.Security.SecureString
$PasswordsArray[$row].ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $UsernamesArray[$row], $secstr
# Runs command on server
$OutputAnvandare = Invoke-Command -computername $AddressArray[$row] -credential $cred -ScriptBlock {
Import-Module Activedirectory
foreach ($Anvandare in (Get-ADGroupMember fjärrskrivbordsanvändare))
{
$Anvandare.Name
}
}
$OutputDomain = Invoke-Command -computername $AddressArray[$row] -credential $cred -ScriptBlock {
Import-Module Activedirectory
foreach ($Anvandare in (Get-ADGroupMember fjärrskrivbordsanvändare))
{
gc env:UserDomain
}
}
$OutputDomain + $OutputAnvandare
}
}
}
function Export {
Write-Host("C")
# Variabler för att bygga up en CSV-fil genom Out-File
$filsökväg = "C:\my\file\path\Coolkids.csv"
$ColForetag = "Company"
$ColAnvandare = "Users"
$Emptyline = "`n"
$delimiter = ";"
for ($p = 1; $p -le $AA.Length; $p++) {
# writes out columns in the csv file
$ColForetag + $delimiter + $ColAnvandare | Out-File $filsökväg
# Writes out the domain name and the users
$OutputDomain + $delimiter + $OutputAnvandare | Out-File $filsökväg -Append
}
}
ReadInfo
Export
My problem is, I can't export the users or the domain. As you can see i tried to make the variables global to the whole script, but $outputanvandare and $outputdomain only contains the information i need inside of the foreach loop. If I try to print them out anywhere else, they're empty?!
This answer focuses on variable scoping, because it is the immediate cause of the problem.
However, it is worth mentioning that modifying variables across scopes is best avoided to begin with; instead, pass values via the success stream (or, less typically, via by-reference variables and parameters ([ref]).
To expound on PetSerAl's helpful comment on the question: The perhaps counter-intuitive thing about PowerShell variable scoping is that:
while you can see (read) variables from ancestral (higher-up) scopes (such as the parent scope) by referring to them by their mere name (e.g., $OutputDomain),
you cannot modify them by name only - to modify them you must explicitly refer to the scope that they were defined in.
Without scope qualification, assigning to a variable defined in an ancestral scope implicitly creates a new variable with the same name in the current scope.
Example that demonstrates the issue:
# Create empty script-level var.
Set-Variable -Scope Script -Name OutputDomain -Value 'original'
# This is the same as:
# $script:OutputDomain = 'original'
# Declare a function that reads and modifies $OutputDomain
function func {
# $OutputDomain from the script scope can be READ
# without scope qualification:
$OutputDomain # -> 'original'
# Try to modify $OutputDomain.
# !! Because $OutputDomain is ASSIGNED TO WITHOUT SCOPE QUALIFICATION
# !! a NEW variable in the scope of the FUNCTION is created, and that
# !! new variable goes out of scope when the function returns.
# !! The SCRIPT-LEVEL $OutputDomain is left UNTOUCHED.
$OutputDomain = 'new'
# !! Now that a local variable has been created, $OutputDomain refers to the LOCAL one.
# !! Without scope qualification, you cannot see the script-level variable
# !! anymore.
$OutputDomain # -> 'new'
}
# Invoke the function.
func
# Print the now current value of $OutputDomain at the script level:
$OutputDomain # !! -> 'original', because the script-level variable was never modified.
Solution:
There are several ways to add scope qualification to a variable reference:
Use a scope modifier, such as script in $script:OutputDomain.
In the case at hand, this is the simplest solution:
$script:OutputDomain = 'new'
Note that this only works with absolute scopes global, script, and local (the default).
A caveat re global variables: they are session-global, so a script assigning to a global variable could inadvertently modify a preexisting global variable, and, conversely, global variables created inside a script continue to exist after the script terminates.
Use Get/Set-Variable -Scope, which - in addition to supporting the absolute scope modifiers - supports relative scope references by 0-based index, where 0 represents the current scope, 1 the parent scope, and so on.
In the case at hand, since the script scope is the next higher scope,
Get-Variable -Scope 1 OutputDomain is the same as $script:OutputDomain, and
Set-Variable -Scope 1 OutputDomain 'new' equals $script:OutputDomain = 'new'.
(A rarely used alternative available inside functions and trap handlers is to use [ref], which allows modifying the variable in the most immediate ancestral scope in which it is defined: ([ref] $OutputDomain).Value = 'new', which, as PetSerAl points out in a comment, is the same as (Get-Variable OutputDomain).Value = 'new')
For more information, see:
Get-Help about_Variables
Get-Help about_Scopes
Finally, for the sake of completeness, Set-Variable -Option AllScope is a way to avoid having to use scope qualification at all (in all descendent scopes), because effectively then only a single variable by that name exists, which can be read and modified without scope qualification from any (descendent) scope.
# By defining $OutputDomain this way, all descendent scopes
# can both read and assign to $OutpuDomain without scope qualification
# (because the variable is effectively a singleton).
Set-Variable -Scope Script -Option AllScope -Name OutputDomain
However, I would not recommend it (at least not without adopting a naming convention), as it obscures the distinction between modifying local variables and all-scope variables:
in the absence of scope qualification, looking at a statement such as $OutputDomain = 'new' in isolation, you cannot tell if a local or an all-scope variable is being modified.
Since you've mentioned that you want to learn, I hope you'll pardon my answer, which is a bit longer than normal.
The issue that's impacting you here is PowerShell Variable Scoping. When you're commiting the values of $outputAvandare and $outputDomain, they only exist for as long as that function is running.
Function variables last until the function ends.
Script variables last until the script ends.
Session/global variables last until the session ends.
Environmental variable persist forever.
If you want to get the values out of them, you could make them Global variables instead, using this syntax:
$global:OutputAnvandare = blahblahblah
While that would be the easiest fix for your code, Global variables are frowned upon in PowerShell, since they subvert the normal PowerShell expectations of variable scopes.
Much better solution :)
Don't be dismayed, you're actually almost there with a really good solution that conforms to PowerShell design rules.
Today, your GetData function grabs the values that we want, but it only emits them to the console. You can see this in this line on GetData:
$OutputDomain + $OutputAnvandare
This is what we'd call emitting an object, or emiting data to the console. We need to STORE this data instead of just writing it. So instead of simply calling the function, as you do today, do this instead:
$Output = GetData
Then your function will run and grab all the AD Users, etc, and we'll grab the results and stuff them in $output. Then you can export the contents of $output later on.

Setting a global PowerShell variable from a function where the global variable name is a variable passed to the function

I need to set a global variable from a function and am not quite sure how to do it.
# Set variables
$global:var1
$global:var2
$global:var3
function foo ($a, $b, $c)
{
# Add $a and $b and set the requested global variable to equal to it
$c = $a + $b
}
Call the function:
foo 1 2 $global:var3
End result:
$global:var3 is set to 3
Or if I called the function like this:
foo 1 2 $global:var2
End result:
$global:var2 is set to 3
I hope this example makes sense. The third variable passed to the function is the name of the variable it is to set.
You can use the Set-Variable cmdlet. Passing $global:var3 sends the value of $var3, which is not what you want. You want to send the name.
$global:var1 = $null
function foo ($a, $b, $varName)
{
Set-Variable -Name $varName -Value ($a + $b) -Scope Global
}
foo 1 2 var1
This is not very good programming practice, though. Below would be much more straightforward, and less likely to introduce bugs later:
$global:var1 = $null
function ComputeNewValue ($a, $b)
{
$a + $b
}
$global:var1 = ComputeNewValue 1 2
As simple as:
$A="1"
function changeA2 () { $global:A="0"}
changeA2
$A
I ran across this question while troubleshooting my own code.
So this does NOT work...
$myLogText = ""
function AddLog ($Message)
{
$myLogText += ($Message)
}
AddLog ("Hello")
Write-Host $myLogText
This APPEARS to work, but only in the PowerShell ISE:
$myLogText = ""
function AddLog ($Message)
{
$global:myLogText += ($Message)
}
AddLog ("Hello")
Write-Host $myLogText
This is actually what works in both ISE and command line:
$global:myLogText = ""
function AddLog ($Message)
{
$global:myLogText += ($Message)
}
AddLog ("Hello")
Write-Host $global:myLogText
You'll have to pass your arguments as reference types.
#First create the variables (note you have to set them to something)
$global:var1 = $null
$global:var2 = $null
$global:var3 = $null
#The type of the reference argument should be of type [REF]
function foo ($a, $b, [REF]$c)
{
# add $a and $b and set the requested global variable to equal to it
# Note how you modify the value.
$c.Value = $a + $b
}
#You can then call it like this:
foo 1 2 [REF]$global:var3
The first suggestion in latkin's answer seems good, although I would suggest the less long-winded way below.
PS c:\temp> $global:test="one"
PS c:\temp> $test
one
PS c:\temp> function changet() {$global:test="two"}
PS c:\temp> changet
PS c:\temp> $test
two
His second suggestion however about being bad programming practice, is fair enough in a simple computation like this one, but what if you want to return a more complicated output from your variable? For example, what if you wanted the function to return an array or an object? That's where, for me, PowerShell functions seem to fail woefully. Meaning you have no choice other than to pass it back from the function using a global variable. For example:
PS c:\temp> function changet([byte]$a,[byte]$b,[byte]$c) {$global:test=#(($a+$b),$c,($a+$c))}
PS c:\temp> changet 1 2 3
PS c:\temp> $test
3
3
4
PS C:\nb> $test[2]
4
I know this might feel like a bit of a digression, but I feel in order to answer the original question we need to establish whether global variables are bad programming practice and whether, in more complex functions, there is a better way. (If there is one I'd be interested to here it.)
#zdan. Good answer. I'd improve it like this...
I think that the closest you can come to a true return value in PowerShell is to use a local variable to pass the value and never to use return as it may be 'corrupted' by any manner of output situations
function CheckRestart([REF]$retval)
{
# Some logic
$retval.Value = $true
}
[bool]$restart = $false
CheckRestart( [REF]$restart)
if ( $restart )
{
Restart-Computer -Force
}
The $restart variable is used either side of the call to the function CheckRestart making clear the scope of the variable. The return value can by convention be either the first or last parameter declared. I prefer last.
Set the variable as a global variable outside of the function and then set the value inside of the function.