Powershell check what environment variables have changed - powershell

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 }

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

Save off env: drive and later revert

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

Paths from environmental variables not available in Powershell

So I have installed the Team Foundation Server PowerShell Tools, and verified they exist, but the executables (TFPT.exe) do not appear to be available in powershell.
Looking at $env:path (and looking at the path variable through System Properties) I see that the end of the path variable looks like this:
$env:Path = {other variables};%TFSPowerToolDir%;%BPADir%;.;
System Properties Enviromental Variables = {other variables};%TFSPowerToolDir%;%BPADir%
When I look at $env:TFSPowerToolDir I get C:\Program Files (x86)\Microsoft Team Foundation Server 2013 Power Tools\, so that seems correct.
But if I try to run tfpt I get the error "The term 'tfpt.exe' is not recognixed as the name of a cmdlet...
If I first do cd $env:TFSPowerToolDir and the run tfpt it works fine. So the environmental variable is correct. But it doesn't seem to get placed in the path.
Any ideas on how to fish this?
Can't replicate the problem here, actually. The problem seems to be that other environment variables are not expanded in $Env:PATH here, but in a quick test PowerShell did so for me reliably.
You could try to work around the problem by manually expanding environment variables in your profile script. E.g. with something like the following:
$Env:PATH = [regex]::Replace($Env:PATH, '%([^%]+)%', {
param($m)
$n = $m.Groups[1].Value
Get-Content -Raw Env:\$n
})

How to give the path as a parameter or a variable in powershell

I have written a powershell script. the code has paths related to only my PC.
Now the same code cannot be executed by another person on his machine because the path is diff. Therefore please let me know a way where my code can work on all machines.
It depends on the paths. If they're to programs in \Program Files perhaps you can use the environment variable $env:ProgramFiles in your path spec. You can also parameterize your script to take the path like so:
param($path)
# rest of script ...
Note that the param() statement must be the first non-comment line in your script.
You could also use the special $MyInvocation variable available to running scripts. It has access to the path the script was executed from, among other things.
For example a script I use has this line:
$InputCSV = (split-path $myinvocation.mycommand.path) + "\filename.csv"
Which means no matter where the script is run from it will know to grab the CSV file from the same place.