Search code examples
bashpowershellvariablesscopeenvironment-variables

PowerShell flexibility in scope of environment variables via scripts


In bash, if I define a variable inside a script (say, set_var.sh), I can choose whether those definitions persist after "running" the script. This will depend on how I "run" the script, the options being:

  1. sh set_var.sh (variables do not persist)
  2. ./set_var.sh (variables do not persist, same as point 1)
  3. source set_var.sh (variables persist)

This is irrespective from the variable being exported to the environment or not in set_var.sh.

Can the same as items 1 and 3 above be achieved with PowerShell 5.1 scripts, for both PS and $env variables?

For an illustration see (1) below.


EDIT:

As per answer by Mathias R. Jessen, the equivalent to item #3 above is "dot sourcing". There is also an "intermediate" case (not identified above, perhaps there is also a way to get this in bash), where environment variables persist but PS variables don't.

My script:

# set_var.ps1
$env:TEST_VAR = 'test_var'
$TEST_VAR2 = 'test_var2'

What I checked:

> $env:TEST_VAR ; $TEST_VAR2 ;
> . .\set_var.ps1
> $env:TEST_VAR ; $TEST_VAR2 ;
test_var
test_var2
> Remove-Variable TEST_VAR2 ; Remove-Item env:TEST_VAR ;
> $env:TEST_VAR ; $TEST_VAR2 ;
> .\set_var.ps1
> $env:TEST_VAR ; $TEST_VAR2 ;
test_var
> Remove-Item env:TEST_VAR ;
> $env:TEST_VAR ; $TEST_VAR2 ;
> & .\set_var.ps1
> $env:TEST_VAR ; $TEST_VAR2 ;
test_var

(1) Example of persistent / non-persistent variables

I have script set_var.sh with the following contents:

#!/bin/bash

export TEST_VAR=test_var
TEST_VAR2=test_var2

Then the following commands prove my point:

$ echo $TEST_VAR ; echo $TEST_VAR2


$ sh set_var.sh ; echo $TEST_VAR ; echo $TEST_VAR2


$ source set_var.sh ; echo $TEST_VAR ; echo $TEST_VAR2
test_var
test_var2
$ echo $TEST_VAR ; echo $TEST_VAR2
test_var
test_var2
$ env | grep TEST_VAR
TEST_VAR=test_var
$ unset TEST_VAR ; unset TEST_VAR2
$ echo $TEST_VAR ; echo $TEST_VAR2


$ ./set_var.sh ; echo $TEST_VAR ; echo $TEST_VAR2


$ echo $TEST_VAR ; echo $TEST_VAR2



Solution

  • To complement Mathias' helpful answer:

    Regarding your edit:

    It is a script's direct invocation / invocation via & - which, unlike in sh / bash, runs in-process - preserves changes to environment variables, but not to regular variables in the script.[1]

    A succinct demonstration, using & with a script block ({ ... }) in lieu of a script file:

    PS> & { $foo = 'bar'; $env:foo = 'bar-env' }; "[$foo]", "[$env:foo]"
    []        # regular variable in the script [block] scope went out of scope
    [bar-env] # environment variable is visible to caller (the whole *process*)
    

    As for the PowerShell equivalent of 1. (sh set_var.sh) and 2. (./set_var.sh):

    An explicit call to the PowerShell CLI (powershell.exe in Windows PowerShell, pwsh in PowerShell [Core], v6+): is needed to run a script in a child process.

    Note: The following examples use pwsh, but the same applies to powershell.exe (whose CLI is a mostly identical to pwsh's, with only a few parameters having been added to the latter).

    In the simplest case, use -File (which is the implied parameter in PowerShell [Core], but is required in Windows PowerShell):

    pwsh -File set_var.ps1  # PowerShell v6+: same as: pwsh set_var.ps1
    

    Note, however, that you'll only get textual output (strings) from the script this way rather than the rich objects that PowerShell normally supports.

    The simplest way to receive objects instead - available from inside PowerShell only[2] - is to pass a script block in which the script is called instead, which automatically makes the CLI output XML-serialized representations of the script's output objects, and causes these representations to be automatically deserialized by the calling session:

    pwsh { .\set_var.ps1 } 
    

    Note:

    • The .\ prefix is necessary, because PowerShell - by design, as a security feature - does not permit running scripts by mere file name from the current directory.

    • The XML-based serialization using the CLIXML format - which is also used for PowerShell's remoting and background jobs - has limits with respect to type fidelity; except for .NET primitive types and a handful well-known types, you get [pscustomobject]-based approximations of the original objects - see this answer.


    [1] However, PowerShell does offer the ability to set variables across in-session scope boundaries, e.g. $global:foo = 'bar' to set a session-global variable or Set-Variable foo bar -Scope 1 to set a variable in the caller's (parent) scope (which when called from a script's top-level scope would also be the global scope).

    [2] From outside of PowerShell, you can explicitly request output in CLIXML format (the XML-based object-serialization format), namely with the -of Xml CLI parameter, but you'll have to parse the XML yourself and reconstruct objects from it.