Powershell global variable is not inherited by child scripts - powershell

I have a bunch of powershell scripts, broken down to keep things modular. However, all or most of these scripts rely on certain common variables, like the server they talk to, etc. I'm defining these variable in the global scope and accessing them in the scripts using $global:{Variable} syntax.
This works fine in the script which serves as the entry point. However when the main (entry point) script executes a child script using the following syntax, I no longer can read the value of the same global variable. All I get is empty string.
Powershell.exe -File .\Child-Script.ps1
My understanding is:
Parent scopes in powershell are inherited by child scopes
Global scope should be available throughout all scripts getting executes in the current shell context.
What am I missing here? Please help.
Example:
Parent Script (Main-Script.ps1)
$global:ServerUrl="http://myserver.com"
Write-Host "ServerUrl (Main-Script): $global:ServerUrl"
Powershell.exe -File .\Child-Script.ps1
Child-Script (Child-Script.ps1)
Write-Host "ServerUrl (Child-Script): $global:ServerUrl"
Output
ServerUrl (Main-Script): http://myserver.com
ServerUrl (Child-Script):

When you launch Powershell.exe you are creating a new powershell session with its own global context. So this:
Powershell.exe -File .\Child-Script.ps1
Will cause the child script to run in a different context. Instead you can 'Dot Source' the child script which will cause it to be executed in the current context:
. .\Child-Script.ps1

Powershell.exe -File .\Child-Script.ps1 = Creates a new session with its own global scope indeed
. .\Child-Script.ps1 = it imports the script in the global scope of the current session . The script can then have access to the global scope but also the global scope to the script. So every function variable or class declared in the child class will be available in the global scope also.
Let's say that your Child-Script.ps1 contains the following line:
$MyVar = "Hello"
And Parent-Script.ps1 :
$MyVar = "GoodBye"
# Dot Sourcing
. .\Child-Script
return $MyVar
In this example $MyVar will return "Hello" because the script was
imported to the global scope , overriding it. This is useful for
importing classes and functions into the global scope so that they are
available in all scopes of the current session.
You Can even dot surce a function like so : . My-Func .
By doing so , all the variables inside the function will be available in the global scope
In your example , just executing the script in its own scope is enough
for this you just need to specify the full path or use the call
operator ( & ) Like so:
c:\path\to\Child-script.ps1 OR .\Child-Script.ps1 ( Relative Path )
With ampersand (&) :
& c:\path\to\Child-script.ps1 OR & .\Child-Script.ps1
And if we now consider this as the new Parent-Script.ps1 :
$MyVar = "GoodBye"
# Call
& .\Child-Script
return $MyVar
In this case $MyVar will return "GoodBye"
Useful post : What is the difference between dot (.) and ampersand (&) in PowerShell?

Related

Global vs script variable

I have defined and assigned both global and script variables. But then when i call global variable, it gets overriden with script variable
$global:myvar = 'global'
$myvar = 'script'
$global:myvar #I expect here 'global', but it prints 'script'
$myvar
This is how PowerShell Variables are designed to work. Variables your scripts or functions set only last as long as they're running, when they end, their values go away.
In what you're doing today, you're changing the $global scope variable but not running a script or function. You're effectively in the global scope already.
In order to use those nested scopes, you need to run a script or function, like this script below, called scratch.ps1
#script inherited the previous value
"script: current favorite animal is $MyFavoriteAnimal, inherited"
#now setting a script level variable, which lasts till the script ends
$MyFavoriteAnimal = "fox"
"script: current favorite animal is $MyFavoriteAnimal"
function GetAnimal(){
#this function will inherit the variable value already set in the script scope
"function: my favorite animal is currently $MyFavoriteAnimal"
#now the function sets its own value for this variable, in the function scope
$MyFavoriteAnimal = "dog"
#the value remains changed until the function ends
"function: my favorite animal is currently $MyFavoriteAnimal"
}
getAnimal
#the function will have ended, so now the script scope value is 'back'
"script: My favorite animal is now $MyFavoriteAnimal"
To access this functionality, you'll need to use scripts or functions.

What is the difference between dot (.) and ampersand (&) in PowerShell?

In PowerShell, what is the difference between using dot (.) and ampersand (&) when invoking a cmdlet, function, script file, or operable program?
For example:
. foo.sh1
& foo.sh1
There is a very similar question which has been incorrectly closed as a duplicate: Differences between ampersand (&) and dot (.) while invoking a PowerShell scriptblock. The questions are different and have completely different keywords and search rankings. The answer on What is the `.` shorthand for in a PowerShell pipeline? only answers half the question.
The difference between the . and & operators matters only when calling PowerShell scripts or functions (or their aliases) - for cmdlets and external programs, they act the same.
For scripts and functions, . and & differ with respect to scoping of the definition of functions, aliases, and variables:
&, the call operator, executes scripts and functions in a child scope, which is the typical use case: functions and scripts are typically expected to execute without side effects:
The variables, (nested) functions, aliases defined in the script / function invoked are local to the invocation and go out of scope when the script exits / function returns.
Note, however, that even a script run in a child scope can affect the caller's environment, such as by using Set-Location to change the current location, explicitly modifying the parent scope (Set-Variable -Scope 1 ...) or the global scope ($global:...) or defining process-level environment variables.
., the dot-sourcing operator, executes scripts and functions in the current scope and is typically used to modify the caller's scope by adding functions, aliases, and possibly variables for later use. For instance, this mechanism is used to load the $PROFILE file that initializes an interactive session.
The caveat is that for functions (as opposed to scripts) the reference scope for child vs. current is not necessarily the caller's scope: if the function was defined in a module, the reference scope is that module's scope domain:
In other words: trying to use . with a module-originated function is virtually pointless, because the scope getting modified is the module's.
That said, functions defined in modules aren't usually designed with dot-sourcing in mind anyway.
You can also run things inside a module scope with the call operator, from my notes from Windows Powershell in Action.
# get and variable in module scope
$m = get-module counter
& $m Get-Variable count
& $m Set-Variable count 33
# see func def
& $m Get-Item function:Get-Count
# redefine func in memory
& $m {
function script:Get-Count
{
return $script:count += $increment * 2
}
}
# get original func def on disk
Import-Module .\counter.psm1 -Force
A few other things:
# run with commandinfo object
$d = get-command get-date
& $d
# call anonymous function
& {param($x,$y) $x+$y} 2 5
# same with dot operator
. {param($x,$y) $x+$y} 2 5
Like Mathias mentioned as a comment in this thread. & is used to invoke the expression whatever comes after the & and . is used to invoke it in the current scope and is normally used to dot source a helper file which contains functions to make it available in callers scope.
See https://mcpmag.com/articles/2017/02/02/exploring-dot-sourcing-in-powershell.aspx for more dot sourcing.
To segregate and give a clear understanding, I am explaining a scenario.
Imagine you have function named MyFunction in a source.ps1. And you wish to use that function in another script(MyCustomScript.ps1)
Put a line in MyCustomScript.ps1 like the below and you should be able to use it.
. path\of\the\source.ps1
MyFunction
So you are using the function which is present in source.ps1 in your custom script.
Whereas, & is the call operator in Powershell which will help you to call any of the outside executable like psexec and others.
Invoking a command (either directly or with the call operator) will create another scope known as child scope and will be gone once the command been executed. If the command is changing any of the values in a global variable then in that case the changes will be lost when the scope ends as well.
To avoid this drawback and to keep any changes made to global variables you can dot the script which will always execute the script in your current scope.
Dot sourcing will only run the function or script within the current scope and call operator (&) which will run a function or script as usual; but it will never add to the current scope.
Hope this gives an idea on when to use what.

will the global variable inside a script visible to another script which share same environment

so, I have two scripts (a.ps1 and b.ps1) inside another scripts (c.ps1). c.ps1 will only do the calling of other two scripts (a.ps1 & b.ps1). If I declare a global variable inside of a.ps1 and I want to use that global variable inside of b.ps1. Is it going to work since both scripts (a.ps1 & b.ps1) are in the same environment (c.ps1)?
As I understand you well, actually you have only one script "c" and "a" and "b" are acting as functions inside "c".
In this case the answer is: Yes, if you declare variable in first function "a" you can use it in the second function "c".
Okay, I did the tests myself and can say: it depends on how you start one script from another. If you use something like Start-Process powershell -args "-file a.ps1" a global variable will not be passed. But if you use & .\a.ps1 it will be. For example:
c.ps1:
& .\a.ps1
a.ps1:
$global:TestVariable = "TEST"
& .\b.ps1
b.ps1:
Write-Host "Test variable: [$global:TestVariable]."
Read-Host # for pause
In this case the output will be:
D:\TEST_123>powershell -file c.ps1
Test variable: [TEST].
And as mentioned by #David Brabant you should read about_scopes man.

Executing a Powershell function (which takes parameters) from Powershell command shell

I have a powershell script which is a function and takes parameters. From with the powershell command shell, how do I execute a function? It seems like it works differently for different users.
Is your script, simply a script or does it contain a function? If it is a script and takes parameters it will look something like this:
-- top of file foo.ps1 --
param($param1, $param2)
<script here>
You invoke that just like a cmdlet excecpt that if you are running from the current dir you have to specify the path to the script like so:
.\foo.ps1 a b
Also note that you pass arguments to scripts (and functions) space separated just like you do with cmdlets.
You mentioned function, so if you script looks like this you have a couple of options:
-- top of file foo.ps1 --
function foo ($param1, $param2) {
<script here>
}
If you run foo.ps1 like above, nothing will happen other than you will define a function called foo in a temporary scope and that scope will go away when the script exits. You could add a line to the bottom of the script that actually calls the foo function. But perhaps you are intending on using this script more as a reusable function library. In that case you probably want to load the functions into the current scope. You can do that with the dot source operator . like so:
C:\PS> . .\foo.ps1
C:\PS> foo a b
Now function foo will be defined at the global level. Note that you could do the same thing within another script which will load that function into the script's scope.
Take a look at this post, maybe it is the right for you.
How to pass command-line arguments to a PowerShell ps1 file
Anyway, the built-in $args variable is an array that holds all the command line arguments.

PowerShell variable collisions

I have a variable that is common to most of my app called "emails". I also want to use "emails" as the name of a parameter in one of the scripts. I need to refer to the value of both variables in the same script. Ideally there would be a way to refer using module/namespace or something and perhaps there is but I don't know it. You can see how I hack around this but it is ugly and prone to error. Is there a better way?
# PowerShell v1
# Some variable names are very common.
param ($emails)
# My Hack
# We need to save current value so we have it after we source in variables below.
$emails0=$emails
# Below is going to load a variable called "emails" which will overwrite parm above.
. C:\load_a_bunch_of_global_variables.ps1
It is because as documentation says: (the dot sourcing operator) Runs a script so that the items in the script are part of the calling scope.
In this case I would convert C:\load_a_bunch_of_global_variables.ps1 to a module and pass $emails as parameter or export a function that sets the $script:emails variable in the module. Then the variable will not be in a conflict with the variable in the parent script.
For more information about modules you can use get-help about_modules.
I would avoid using global variables if possible in my scripts.
Why? Because it is a code smell (as programmers say). With one script there is no problem. If two scripts use the same global variable and only read, it is maybe acceptable. But if any of them changes the value, then there might be unpleasant conflicts.
In some cases Get-Variable -scope 1 -name myvariable would help, but I would use it only in closed pieces of code like modules or in short scripts (the same reason as with global variables).
While you can use Get-Variable -scope to get access to variables at arbitrary levels of the call stack, it is easier in this case to grab the top level (to the script) variable using the script: modifier e.g.
$script:emails
rerun and stej both helped me out.
I still want to source in the file using ". file.ps1" but changing "$emails=foo#yahoo.com" in my load_a_bunch_of...ps1 file to "$global:emails=foo#yahoo.com" solved the problem. I can now refer to the variable using global key word when I have a local and a global variable, and when there is only one variable to deal with I can leave out the global keyword.
You can alwways access your global variables from a script using $global:var name inside your script you have local scope and you won't get collisions. If you . source your script you will override the global var.
For Ex if a have a script
$Crap ="test"
$Crap
And you run the flowing commands you get what you want. In line 2 we run the script and the var doesn't get a conflict but if you run the script as in line 4 with a . source you get what you are discovering which due to the way the . operator works
1:PS C:\Users\Adam> $crap = "hi"
2:PS C:\Users\Adam> .\test.ps1
test
3:PS C:\Users\Adam> $crap
hi
4:PS C:\Users\Adam> . .\test.ps1
test
5:PS C:\Users\Adam> $crap
test
6:PS C:\Users\Adam>
if You add the following line to the script run it
$global:crap;
you will get
PS C:\Users\Adam> .\test.ps1
test
hi