Search code examples
powershellenvironment-variablesrestore

In Powershell how to store the current environment variables and restore them as they were


I am looking for a method to save all environment variables of the currently running process to a variable and restore them back from that saved state.

This is how far I got:

$OldEnvironment = Get-ChildItem env:
try
{
    # Manipulate a variable
    $env:PATH="C:\My\Utilities\Path;$env:PATH"
    # Remove some
    Remove-Item -Path Env:ALLUSERSPROFILE
    Remove-Item -Path Env:APPDATA
    Remove-Item -Path Env:LOCALAPPDATA
    # Set a new one which we presume did not exist before
    Set-Item -Path env:NEWSHINYVARIABLE "FOOBAR"
    # Output current environment
    Get-ChildItem env:|ft
}
finally
{
    # Restore ...
    $OldEnvironment|%{ Set-Item -Path "Env:$($_.Name)" "$($_.Value)" }
    # ... alas there's a glitch here:
    Get-ChildItem -Path env:NEWSHINYVARIABLE|ft
    # NEWSHINYVARIABLE _still_ exists
}

Now my issue is to make sure that variables set inside the try block don't leak out (like NEWSHINYVARIABLE does). Removed variables aren't an issue, because they will simply get created anew.

How can also remove variables currently contained in env: but not stored in $OldEnvironment with a concise one-liner? (Please note: I do not want to resort to modules and background jobs which - as I understand - contain their own private environment. I want to stay with the try .. finally method structurally.)


Solution

  • Use Compare-Object to compare the content of the Env: PowerShell drive before and after, and take appropriate actions to restore the values of preexisting or remove newly added environment variables:

    # Set some sample env. vars.
    $env:foo='original foo'
    $env:bar='original bar'
    
    # Get the current list of env. vars. and their values
    $envVarsBefore = Get-ChildItem Env:
    
    try {
      # Perform sample operations that add, update, and delete environment variables.
      $env:foo='changed foo'
      $env:bar=$null # Remove $env:bar
      $env:goaway='yes' # define a new var.
    } finally {
      # Restore the original environment, including removal of newly added variables.
      Compare-Object -PassThru $envVarsBefore (Get-ChildItem Env:) -Property { '[{0}, {1}]' -f $_.Key, $_.Value } | 
        ForEach-Object {
          # A newly added or updated variable: remove it.
          # (If it was updated, it'll be restored with its original value below.)
          if ($_.SideIndicator -eq '=>') {
            Remove-Item Env:$($_.Name)
          }
          else { # $_.SideIndicator -eq '<='
            # Restore a deleted variable or a modified's variable original value.
            Set-Item Env:$($_.Name) $($_.Value)
          }
        }
    }
    
    # Check if the original environment was restored:
    Get-Item Env:foo, Env:bar, Env:goaway
    

    The last command in the code above prints the original (restored) values of $env:foo and $env:bar and - as expected - reports an error for the no-longer-extant $env:goaway variable.

    Note:

    • Get-ChildItem Env: emits System.Collections.DictionaryEntry instances, each representing an env. var. as a key(name)-value pair.

    • When Compare-Object processes objects that do not implement the System.IComparable interface, it falls back to comparing objects by their .ToString() values.

      • In PowerShell (Core) 7, fortunately, System.Collections.DictionaryEntry instances have meaningful string representations that reflect both the entry key (the env. var name) and value (the env. var. value); e.g.:

         # -> e.g. (on Windows): '[OS, Windows_NT]'
         (Get-Item Env:OS).ToString()
        
      • In Windows PowerShell (the legacy, ships-with-Windows, Windows-only edition of PowerShell whose latest and last version is 5.1), such instances - unhelpfully - stringify to the full type name only, which the -Property { '[{0}, {1}]' -f $_.Key, $_.Value } argument passed to Compare-Object compensates for, which is an instance of a calculated property.

    • In other words:

      • The code above works in both PowerShell editions.
      • If your code is designed to run in PowerShell 7 only, you can improve performance by removing the -Property { '[{0}, {1}]' -f $_.Key, $_.Value } argument.