What is the equivalent of setlocal / endlocal for PowerShell?

goal

Isolate changes to environment variables in a code block.

Background

If I want to create a script package to run a command that requires a set of environment variables, I know I can do this:

setlocal set MYLANG=EN my-cmd dostuff -o out.csv endlocal 

However, I prefer to use PowerShell when I need to use a shell scripting language. I know how to set an environment variable ( $env:TEST="EN" ), and of course this is just a simple example. However, I'm not sure how to achieve the same effect as with the script package. Surprisingly, I do not see any questions asking about it.

I know that setting something up with $env:TEST="EN" is process related, but this is not practical if I use scripts as small utilities in a single terminal session.

My current approaches:

  • Introduced by setlocal . But this was not a command post ... I was hoping.
  • Save the current variable in the temp variable, run my command, change it back ... it seems to be silly.
  • Functional level area (although I had doubts about success since $env: seems a little different from $global:

The scope did not exceed the link to $env:

 $env:TEST="EN" function tt { $env:TEST="GB" ($env:TEST) } ($env:TEST) tt ($env:TEST) 

Output:

 C:\Users\me> .\eg.ps1 EN GB GB 
+5
source share
4 answers

In batch files, all shell variables are also environment variables ; therefore setlocal ... endlocal also provides a local scope for environment variables.

Unlike PowerShell , shell variables (like $var ) are different from environment variables (like $env:PATH ) - this is a difference that is usually beneficial.

Given that the smallest area for setting environment variables is the current process - and therefore the entire PowerShell session , you have to manage the small custom area manually if you want to do this in-process (this is what setlocal ... endlocal does in cmd.exe , for which PowerShell does not have a built-in equivalent; for shell variables of a custom field, use & { $var = ...; ... } ):

In-process approach: manual control of a custom area:

To ease the pain a bit, you can use the slightly redesigned cmdlet ForEach-Object call (via its built-in single-character alias % ):

 % { $oldVal, $env:MYLANG = $env:MYLANG, 'EN' } { my-cmd dostuff -o out.csv } { $env:MYLANG=$oldVal } 

Simply put, if there is no existing MYLANG value that needs to be restored:

 % { $env:MYLANG='EN' } { my-cmd dostuff -o out.csv } { $env:MYLANG=$null } 

This uses:

  • ForEach-Object enters its process script block (medium) once once even if there is no pipeline input.

  • ForEach-Object offers initializing and clearing script blocks (first and last, respectively).

  • $oldVal, $env:MYLANG = $env:MYLANG, 'EN' saves the old value (if any) $env:MYLANG in $oldVal when changing the value to 'EN' ; this technique of assigning several variables at once (known as the assignment of destructuring in some languages) is explained in Get-Help about_Assignment_Operators , section "PURPOSE OF MULTIPLE OPTIONS".

  • Note that the helper variable $oldVal remains in scope after this call; if this is a concern, clear it / restore it to its previous value (or select a name that is unlikely to be accidentally used in the surrounding code).

A more correct and reliable, but more detailed solution is to use try { ... } finally { ... } :

 try { # Temporarily set/create $env:MYLANG to 'EN' $prevVal = $env:MYLANG; $env:MYLANG = 'EN' my-cmd dostuff -o out.csv # run the command(s) that should see $env:MYLANG as 'EN' } finally { # remove / restore the temporary value # Note: if $env:MYLANG didn't previously exist, $prevVal is $null, # and assigning that back to $env:MYLANG *removes* it, as desired. $env:MYLANG = $prevVal } 

To facilitate this approach, this answer on my question offers a convenience feature
Invoke-WithEnvironment , which allows you to write the same call as:

 # Define env. var. $env:MYLANG only for the duration of executing the commands # in { ... } Invoke-WithEnvironment @{ MYLANG = 'EN' } { my-cmd dostuff -o out.csv } 

Alternatives using the helper process:

Using the helper process and setting the environment variable here,

  • you avoid having to recover your environment after a call

  • but you pay a penalty for performance, and the complexity of the call increases.

Using aux. cmd.exe process:

 cmd /c "set `"MYLANG=EN`" & my-cmd dostuff -o out.csv" 

Note:

  • An external "..." clause has been chosen so that you can reference PowerShell variables in your command; built-in " must be escaped `"

  • In addition, the arguments of the target command must be passed in accordance with the rules of cmd.exe (it does not matter with a simple command).

Using aux. PowerShell child session:

 # In PowerShell *Core*, use `pwsh` in lieu of `powershell` powershell -nop -c { $env:MYLANG = 'EN'; my-cmd dostuff -o out.csv } 

Note:

  • Starting another PowerShell session is expensive.

  • Exiting the script block ( { ... } ) is serialized and then deserialized in the call area; it doesn't matter for string output, but complex objects like [System.IO.FileInfo] deserialized to emulate the originals (which may or may not be a problem).

+3
source

PowerShell has a way to achieve this:

Local area:

 & { [System.Environment]::SetEnvironmentVariable('TEST', 'WORK Local', [System.EnvironmentVariableTarget]::Process) [System.Environment]::GetEnvironmentVariable("TEST", [System.EnvironmentVariableTarget]::Process) } 

This creates an environment variable within the process as described above. Any appeal to it outside the scope will not return anything.

For the global version, you simply change the target to β€œMachine”:

 & { [System.Environment]::SetEnvironmentVariable('TEST', 'WORK Global', [System.EnvironmentVariableTarget]::Machine) } 

Any call to this outside the scope will return 'Work Global'

Putting it all together:

 ## create local variable and print & { [System.Environment]::SetEnvironmentVariable('TEST', 'WORK Local', [System.EnvironmentVariableTarget]::Process) [System.Environment]::GetEnvironmentVariable("TEST", [System.EnvironmentVariableTarget]::Process) } function tt { ($env:TEST) } & { $TEST="EN"; $env:TEST="EN"; tt } & { $TEST="change1"; $env:TEST="change1"; tt } & { $TEST="change1"; $env:TEST="change2"; tt } [System.Environment]::GetEnvironmentVariable("TEST", [System.EnvironmentVariableTarget]::Process) & { [System.Environment]::SetEnvironmentVariable('TEST', 'WORK Global', [System.EnvironmentVariableTarget]::Machine) } ## create global variable ## Create local variable and print ( overrides global ) & { [System.Environment]::SetEnvironmentVariable('TEST', 'WORK Local', [System.EnvironmentVariableTarget]::Process) [System.Environment]::GetEnvironmentVariable("TEST", [System.EnvironmentVariableTarget]::Process) } [System.Environment]::GetEnvironmentVariable("TEST", [System.EnvironmentVariableTarget]::Machine) ## get global variable [System.Environment]::SetEnvironmentVariable("TEST",$null,"USer") ## remove global variable 

This gives us the following result:

 WORK Local EN change1 change2 change2 WORK Local WORK Global 
+1
source

I would just use try { } finally { } :

 try { $OriginalValue = $env:MYLANG $env:MYLANG= 'GB' my-cmd dostuff -o out.csv } finally { $env:MYLANG = $OriginalValue } 

This should cause the values ​​to be returned to their original values, even if an error is detected in your script. It is not bulletproof, but most of the things that would break it would also be very obvious that something went wrong.

You can also do this:

 try { $env:MYLANG= 'GB' my-cmd dostuff -o out.csv } finally { $env:MYLANG = [System.Environment]::GetEnvironmentVariable('MYLANG', 'User') } 

This should get the value from HKEY_CURRENT_USER\Environment . You may need 'Machine' instead of 'User' , and it will pull from HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment . What you need depends on a user environment variable or a computer environment variable. This works because the Env: provider drive does not save changes to environment variables, so changes to these variables will not change the registry.

+1
source

Forgive me if I missed something, because there part of this message is a bit unclear to me.

I would use the modifier scope of the $local , $script and $global modifiers.

Example

 $env:TEST="EN" function tt { $local:env:TEST="GB" ($local:env:TEST) } $t = { $local:env:TEST="DE" ($local:env:TEST) } ($env:TEST) tt ($env:TEST) . $t ($env:TEST) 

Comment output

 EN # ($env:TEST) GB # tt EN # ($env:TEST) DE # . $t EN # ($env:TEST) 
-2
source

Source: https://habr.com/ru/post/1265914/


All Articles