Save off env: drive and later revert - powershell

I am running multiple scripts from the powershell console. These scripts add/modify variables in the env: drive. Between running each script, I would like to reset the env: drive to what it was when I opened up the console. Is there a way to save off/copy the env: drive, and then copy it back at a later point?

The easiest way is to just start another process for each script. Instead of
.\foo.ps1
just use
powershell -file .\foo.ps1
Then each script gets its own process and thus its own environment to mess with. This doesn't work, of course, if those scripts also modify things like global variables you'd rather have retained in between.
On the other hand, saving the state of the environment is fairly simple:
$savedState = Get-ChildItem Env:
And restoring it again:
Remove-Item Env:*
$savedState | ForEach-Object { Set-Content Env:$($_.Name) $_.Value }

You might want to take a look at the PowerShell Community Extensions module. There commands that make managing environment variables easier to do. In particular, Push-EnvironmentBlock and Pop-EnvironmentBlock are useful here. "Push" saves the current environment variables and pushes it onto a stack. "Pop" removes the previous state from the stack and restores it. So you would probably do this
Push-EnvironmentBlock -Description "Before running script.ps1"
.\script.ps1
Pop-EnvrionmentBlock

Related

In PowerShell, how can I list all environmental variables without using Get-ChildItem or Get_Item?

I'm writing a simple PowerShell script and want to dump all environmental variables/values. Something simple like
gci env:* | sort-object name
seemed liked a good start. But this didn't work for me.
Where things seem to get wacky is that my script is called from a job run by scheduler, both of which set environmental variables configured by other developers.
So, when I use Get-ChildItem as shown above, I get:
gci : An item with the same key has already been added.
Finally, my question: How can I get the environmental variables, ideally both names and values, to see which one(s) have been added incorrectly?
A simple-to-remember command is:
dir Env:
There are 3 scopes of what is called Environment Variables:
[System.EnvironmentVariableTarget]::Machine
[System.EnvironmentVariableTarget]::User
[System.EnvironmentVariableTarget]::Process
To get list of variables, you can use
[System.Environment]::GetEnvironmentVariables($scope)
[System.Environment]::GetEnvironmentVariables() # This will mix all scopes in one output
To set variable, you can use
[System.Environment]::SetEnvironmentVariable($varName, $varValue, $scope)
If $scope is Machine or User, it will try to store data, otherwise it will trow an exception.
$Env: is actually a virtual PowerShell drive and environment variables are items on it. There is a special provider Get-PSProvider -PSProvider Environment that implements this method of accessing to environment in powershell.
You can run Get-ChildItem -Path 'Env:\' and this is exactly the same as [System.Environment]::GetEnvironmentVariables() without specifying scope.
For Powershell version 5.1.19041.906, below commands works fine.
gci env:* | sort-object name
Alternatively you can use below command for your requirement.
[System.Environment]::GetEnvironmentVariables()

Add environment variables with PowerShell and bat files

As part of a project, I need to run two bat files with a PowerShell script. These bat files will perform several operations including the creation of environment variables.
But problem. The first environment variables are created (those created with the first bat file) but not the second ones (which must be created with the execution of the second bat file). The execution of the second one went "well" because the rest of the operations it has to perform are well done.
I use the same function for the execution of these .bat files.
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine")
$argList = "/c '"$batFile'""
$process = Start-Process "cmd.exe" -ArgumentList $argList -Wait -PassThru -Verb runAd
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine")
I use the line
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine")
to reload the environment variables. But that didn't solve the problem. Without this line I have the environment variables corresponding to the second bat file (the one running second) being created. With it, I have only those of the first one.
I also noticed that if I re-run my PowerShell program (so re-run the batch files), I had all the environment variables created.
Well 1st off:
It looks like you have an error in your code for executing your 2nd batch file so I suspect you re-wrote your code to go here and it's not at all in the original form, as how it's written you would never get anything further.
You know what TL;DR: Try this
I've been writing a lot, and its a rabbit hole considering the snippet of code isn't enough of the process, the code is obviously re-written as it introduces a clearly different bug, and your description leaves something to be desired.
i'll leave some of the other points below re-ordered, and you can feel free to read/ignore whatever.
But here, is the long and short of it.
if you need to run this CMD scripts, and get some stuff out of them to ad to path, have them run normally and echo the path they create into stdout, then capture it in a powershell variable, dedupe it in powershell and set the path directly for your existing powershell environment.
Amend both of your CMD Scripts AKA Batch Files to add this to the very top before any existing lines.
#(SETLOCAL
ECHO OFF )
CALL :ORIGINAL_SCRIPT_STARTS_HERE >NUL 2>NUL
ECHO=%PATH%
( ENDLOCAL
EXIT /B )
:ORIGINAL_SCRIPT_STARTS_HERE
REM All your original script lines should be present below this line!!
PowerShell code basically will be
$batfile_1 = "C:\Admin\SE\Batfile_1.cmd"
$batfile_2 = "C:\Admin\SE\Batfile_2.cmd"
$Path_New = $($env:path) -split ";" | select -unique
$Path_New += $(&$batFile_1) -split ";" | ? {$_ -notin $Path_New}
$Path_New += $(&$batFile_2) -split ";" | ? {$_ -notin $Path_New}
$env:path = $($Path_New | select -unique) -join ";"
Although if you don't need the separate testable steps you could make it more concise as:
$batfile_1 = "C:\Admin\SE\Batfile_1.cmd"
$batfile_2 = "C:\Admin\SE\Batfile_2.cmd"
$env:path = $(
$( $env:path
&$batFile_1
&$batFile_2
) -split ";" | select -unique
) -join ";"
Okay leaving the mostly done stuff where I quit-off trying to amend my points as I followed the rabbit hole tracking pieces around, as it will give some light on some aspects here
2nd off: You do not need to start-process to run a CMD script, you can run a cmd script natively it will automatically instantiate a cmd instance.
Start-Process will spawn it as a separate process sure, but you wait for it, and although you use -PassThru and are saving that as a variable, you don't do anything with it to try to check it's status or error code so you may as well just run the CMD script directly and see it's StdOut in your powershell window, or save it in a variable to log it if needed.
3rd off: Why not just set the environment variables directly using Powershell?
I'm guessing these scripts do other stuff but might be that you should just echo what they want to set Path to back to the PowerShell script and then dedupe it and set the path when done.
4th off: $env:Path is your current environment's path, this includes all pathtext that is from the System AND the currentuser profile (HKLM: and HKCU: registry keys for environment), while $( [System.Environment]::GetEnvironmentVariable("Path","Machine") ) is your System (Local machine) pat has derived from your registry.
5th off: The operational Environment is specific to each shell, when you start a new instance of CMD /c it inherits the environment of the previous cmd instance that spawned it.
6th off: Changes made to environmental variables do not 'persist' ie: you can't open a new CMD / Powershell instance and see them, and once you close that original cmd window they're gone, unless you edit the registry values of these items directoy or use SET X in a cmd session (Which is problematic and should be avoided!) and also ONLY affects the USER variables not the system/local machine variables.
Thus, any changes made to the environment in one CMD instance only operate within that existing shell unless they are changes that persist in the registry, in which case they will only affect new shells that are launched.
7th off: When you launch powershell, it is a cmd shell running powershell, and so powershell inherits whatever the local machine and current user's variables are at that moment when the interpreter is started. This will be what is in $env:xxx
8th off: Setting $env:Path = $([System.Environment]::GetEnvironmentVariable("Path","Machine")) will always change the current powershell environment to whatever is stored in the [HKLM:\] registry key for environmental variables.
Now given that we don't have all of your code and only a description of what is happening.
It appears you have one script Lets call it batFile_1.cmd that is setting some variables in your
For each batch file you run whether spawned implicitly or explicitly you will inherit the previous shell's command environment.
However
Each instance of CMD which you spawn with your batch files within them, spawns a separate cmd shell instance and he Instance of CMD that powershell.exe is running inside of, and thus your script was running in.
Now I'm just supposing what is happening since you only give a small snippet, which is not enough to actually reproduce your real issue.
But it seems like you spawn a cmd script,
So it's hard to know exactly is happening without the full context instead of the snippet, although I'll go into one scenario that might be happening below.
A note on Each CMD instance only inherits the values of it's parent.
(I feel this is much clearer to explain in opening an actual CMD window and test how the shell works by spawning another instance of CMD.
When the new instance is exited the variables revert to the previous instance because they only move from parent to child)
eg:
C:\WINDOWS\system32>set "a=hello"
C:\WINDOWS\system32>echo=%a%
hello
C:\WINDOWS\system32>CMD /V
Microsoft Windows [Version 10.0.18362.1082]
(c) 2019 Microsoft Corporation. All rights reserved.
C:\WINDOWS\system32>(SET "a= there!"
More? SET "b=%a%!a!"
More? )
C:\WINDOWS\system32>echo=%b%
hello there!
C:\WINDOWS\system32>echo=%a%
there!
C:\WINDOWS\system32>exit
C:\WINDOWS\system32>echo=%a%
hello
C:\WINDOWS\system32>echo=%b%
%b%
C:\WINDOWS\system32>
But that isn't really what is happening here you seem to be updating the command environment back to the Local machine

Powershell Dot Slash .\ Starts at the root of a drive

Note: I'm using the built-in PowerShell ISE as my environment
I got a funny issue with dot slash on Powershell. All of my scripts run from a certain folder and there are subfolders that contain data that is needed for them to run.
For example, my scripts are saved at c:\users\chris\posh
Most of the time, I will call input and send output to subfolders like this...
c:\users\chris\posh\inputs
c:\users\chris\posh\output
Therefore I'll have scripts examples that look like this for inputs and outputs:
$hbslist = Get-Content .\inputs\HBS-IP.txt
write-output "$($lat),$($long)" | Out-File .\Outputs\"LatLong.csv" -Append
Lately, when I run the scripts, it cannot locate my files or exe's that I call on. That's because it's trying to look at P:/ instead of c:\users\chris\posh when using .\
Powershell also starts in my P:\ (mapped share drive) for some reason and I cannot figure out as to why my PC is running this way.
It might be a policy on your machine which changes your home directory. You can check the home directory with:
echo $env:HOME
This happens often on corporate machines. If you want to set it back for your powershell environment, you can set it in your profile.ps1.
This is typically stored at:
c:\Users\<Name>\Documents\WindowsPowershell\profile.ps1

Powershell check what environment variables have changed

Is there a way to get a copy of all of the current environment variables in powershell? What I want to do is get a copy of $env at a particular point in time, run a batch script that does a bunch of set commands, and then look at $env again and check what has changed. I would then determine which environment variables had been newly set, changed or unset, and then run the appropriate setx commands (or [Environment]::SetEnvironmentVariable($NAME, $value, 'User')), to make the things that have changed in-process to persistent user changes.
I've tried calling .clone() on $env, but that didn't work. Any ideas on how to get a copy of $env or general ideas about how to accomplish what I describe above? Suggestions for a powershell newbie would be appreciated.
Just retrieve the Env: drive into a variable.
$envVars = Get-ChildItem Env:
I wrote some functions that might be of use to you as well, in this article:
Windows IT Pro: Take Charge of Environment Variables in PowerShell
I presented some functions in the article: Get-Environment (same as Get-ChildItem Env:, but included for completeness), Restore-Environment (restores a saved copy of the environment), and Invoke-CmdScript (runs a cmd.exe shell script [batch file] that adds environment variables and makes them available in PowerShell).
There may be a more appropriate way to do it but this should work.
$a = #{}
Get-ChildItem env: | % { $a[$_.Name] = $_.Value }

Why don't .NET objects in PowerShell use the current directory?

When you use a .NET object from PowerShell, and it takes a filename, it always seems to be relative to C:\Windows\System32.
For example:
[IO.File]::WriteAllText('hello.txt', 'Hello World')
...will write C:\Windows\System32\hello.txt, rather than C:\Current\Directory\hello.txt
Why does PowerShell do this? Can this behaviour be changed? If it can't be changed, how do I work around it?
I've tried Resolve-Path, but that only works with files that already exist, and it's far too verbose to be doing all the time.
You can change .net working dir to powershell working dir: [Environment]::CurrentDirectory = (Get-Location -PSProvider FileSystem).ProviderPath
After this line all .net methods like [io.path]::GetFullPath and [IO.File]::WriteAllText will work without problems
The reasons PowerShell doesn't keep the .NET notion of current working directory in sync with PowerShell's notion of the working dir are:
PowerShell working dirs can be in a provider that isn't even file system
based e.g. HKLM:\Software
A single PowerShell process can have
multiple runspaces. Each runspace can be cd`d into a different file
system location. However the .NET/process "working directory" is
essentially a global for the process and wouldn't work for a
scenario where there can be multiple working dirs (one per runspace).
For convenience, I added the following to my prompt function, so that it runs whenever a command finishes:
# Make .NET's current directory follow PowerShell's
# current directory, if possible.
if ($PWD.Provider.Name -eq 'FileSystem') {
[System.IO.Directory]::SetCurrentDirectory($PWD)
}
This is not necessarily a great idea, because it means that some scripts (that assume that the Win32 working directory tracks the PowerShell working directory) will work on my machine, but not necessarily on others.
When you use filenames in .Net methods, the best practice is to use fully-qualified path names. Or use
$pwd\foo.cer
If you do in powershell console from:
C:\> [Environment]::CurrentDirectory
C:\WINDOWS\system32\WindowsPowerShell\v1.0
you can see what folder .net use.
That's probably because PowerShell is running in System32. When you cd to a directory in PowerShell, it doesn't actually change the working directory of powershell.exe.
See:
PowerTip article on syncing the two directories
Channel9 forum thread
I ran into the same problem a long time ago and now I add the following to the beginning of my profile:
# Setup user environment when running session under alternate credentials and
# logged in as a normal user.
if ((Get-PSProvider FileSystem).Home -eq "")
{
Set-Variable HOME $env:USERPROFILE -Force
$env:HOMEDRIVE = Split-Path $HOME -Qualifier
$env:HOMEPATH = Split-Path $HOME -NoQualifier
(Get-PSProvider FileSystem).Home = $HOME
Set-Location $HOME
}