I've a .bat file that starts a process, and I want to perform some automation by using a powershell script. The bat file must remain reasons that don't depend on me. The .bat file contains the definitions of some environment variable, something like:
#echo Opening solution.
set QTDIR=C:\Environment\Qt\5.15.2\msvc2019_64
set PHYSX_HEADER=C:\Environment\PhysX-3.3-3.3.4-1.3.4\PhysXSDK\Include
set PHYSX_LIB_DEBUG=C:\Environment\PhysX-3.3-3.3.4-1.3.4\PhysXSDK\Lib\vc16win64
set DDS_ROOT=C:\Environment\OpenDDS-DDS-3.16\
set ACE_ROOT=C:\Environment\OpenDDS-DDS-3.16\ACE_Wrappers\
set TAO_ROOT=C:\Environment\OpenDDS-DDS-3.16\ACE_Wrappers\TAO\
rem Setting doxygen
for /f "delims=" %%i in ('where doxygen') do set DOXYGEN_EXECUTABLE="%%i"
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\devenv.exe" /Log "%USERPROFILE%\MyVSLog.xml" MySolution.sln
exit
This due to the fact that different projects needs env variables with same name but different content.
Now I want to create in the powershell script context the same enviornment variables that I create in the .bat file, so they can be used by compilers and other processes that needs them defined.
In the powershell script I'm able to read the name and the value of environemnts variables stored in the bat file, with following code (verbose due to debugging purposes):
$startBatFile = "$rootPath/Start_Solution.bat"
# Read environment variables
Select-String '^set ([^=]*)=(.*)' $startBatFile | ForEach-Object {
"object is $_"
$splitted = $_.Line.Split("=")
"splitted is $splitted"
$nameSplitted = $splitted[0].Split(" ")
$variableName = $nameSplitted[1]
$variableValue = $splitted[1]
"- Set env variable $variableName to value $variableValue"
Set-Variable -Name "env:$variableName" -Value $variableValue
}
In the loop I can set, for example, $variableName to QTDIR and $variableValue to C:\Environment\Qt\5.15.2\msvc2019_64.
But I cannot set them as environment variables. I'd like to have the same behaviour as setting them manually:
$env:MyEnvVariable = $someValue
but using env does not seem to work with Set-Variable. I cannot assign them directly because I'd like to avoid to have paths defined in more than one place, and because I should be able to load in the script different .bat files that contains diffferent definitions for environment variables, so they are not fixed and I cannot write them directly in the powershell code.
How can I set environment variables in powershell script by reading them from the .bat file?
Use Set-Content, not Set-Variable:
# Note: Parameters -Path and -Value are implied.
Set-Content "env:$variableName" $variableValue
Set-Variable is only intended for PowerShell's own variables, not for environment variables.
PowerShell exposes environment variables via the provider model, in the form of the Environment provider, which exposes the Env: drive (try Get-ChildItem Env:).
The usual form of referring to environment variables, with verbatim names (e.g. $Env:USERNAME) is an instance of namespace variable notation, and the equivalent of a Get-Content call (e.g. Get-Content Env:USERNAME) or, in the case of assigning a value, Set-Content (as shown above); see this answer for more information.
Since you're providing the name of your environment variables indirectly - via the value of (regular) variable $variableValue - namespace notation is not an option, and an explicit Set-Content call is required.
Related
I am using EWDK and msbuild to build some drivers.
I have a build.cmd file that works.
call <EWDK root>/BuildEnv\SetupBuildEnv.cmd
cd <working directory>
msbuild /t:build /p:...
Calling this file from PowerShell works
& build.cmd
I want to write it natively in PowerShell so I won't have a mix of languages.
I tried running it with PowerShell
& <EWDK root>/BuildEnv\SetupBuildEnv.cmd
& msbuild /t:build /p:...
And the build failed because none of the environment variables set in SetupBuildEnv.cmd were kept in the PowerShell session that called the script.
I can't re-write SetupBuildEnv.cmd as it comes with the SDK package so this is the only .cmd script which PowerShell should call.
I'd like to have msbuild.exe called directly in PowerShell.
Is there anyway to make this work?
Batch/cmd scripts called from PowerShell run in a new process (cmd.exe). Environment variables set by a batch script (using the set command) don't persist when the cmd.exe process ends, so the PowerShell parent process can't access them.
As a workaround, create a helper batch script that calls SetupBuildEnv.cmd and then outputs the current values of all environment variables. This output can be captured by the PowerShell script which can then set the environment variables of the PowerShell process accordingly, so msbuild called from PowerShell will inherit them.
The helper script doesn't need to be a separate file, it can be a cmd.exe /c one-liner, as in the following code sample:
$envBlockFound = $false
cmd /c 'call "<EWDK root>\BuildEnv\SetupBuildEnv.cmd" & echo ###EnvVars### & set' | ForEach-Object {
if( $envBlockFound ) {
$name, $value = $_ -split '='
Set-Item env:$name $value
}
elseif( $_.StartsWith('###EnvVars###') ) {
$envBlockFound = $true
}
}
# uncomment to verify env vars
# Get-Item env:
msbuild /t:build /p:...
The cmd /c … line breaks down to:
call "<EWDK root>\BuildEnv\SetupBuildEnv.cmd" …runs the given script and waits until it has finished.
echo ###EnvVars### …outputs a delimiter line so the PowerShell script can ignore the stdout from SetupBuildEnv. Note that the space character before the next & ends up in the output as a trailing space, which the PowerShell script has to handle.
set …without arguments outputs all environment variables as key/value pairs separated by =.
Using ForEach-Object, we process each line ($_) from stdout of cmd.exe, which also includes any stdout lines of SetupBuildEnv.cmd.
Ignore all lines until the delimiter line '###EnvVars###' is found.
When the delimiter string has been found (comparing using .StartsWith() instead of -eq to ignore the trailing space), then for each line split the line on = and set an environment variable.
Finally call the msbuild process which now inherits the env vars.
I'm in the process of converting an old batch file to Powershell. In the batch file, the SET command is used to declare and set multiple variables, and then an executable is called that uses those variables, along with additional flags. How would I do this in Powershell?
Batch File code excerpt:
SET VAR1=VAL1
SET VAR2=VAL2
SET VAR3=VAL3
SET VAR4=VAL4
%DIRECTORY%\%SUBDIR%\EXECUTABLE.EXE -FLAG1 -FLAG2 -FLAG3
I first attempted to declare the Powershell variables and call the exe using Start-Process, but the executable is looking for specific variable names. I'm not sure if those variables can be seen by the exe in this scenario, but it doesn't work.
$VAR1 = VAL1
$VAR2 = VAL2
$VAR3 = VAL3
Start-Process "$DRIVE\DIR\EXECUTABLE.exe -FLAG1 -FLAG2 -FLAG3"
I've also unsuccessfully attempted passing the multiple line command to the command shell:
$Command = "CMD.exe /C
SET VAR1=VAL1
SET VAR2=VAL2
SET VAR3=VAL3
SET VAR4=VAL4
$DRIVE\$DIR\EXECUTABLE.exe -FLAG1 -FLAG2 -FLAG3
Invoke-Expression $Command
Note** The variables have to be set on multiple lines, even when I run the exe from a DOS prompt. Using the "&" (Batch) or ";" (Powershell) and passing all variables on one line doesn't work.
For external programs (child processes) to see variables, they must be environment variables: use $env:VAR1 = 'VAL1' rather than $VAR1 = 'VAL1' - also note how the values must be quoted.
In cmd.exe (batch files), all variables are invariably also environment variables; in PowerShell, regular variables such as $VAR1 are visible only to the PowerShell session itself.
Do not use Start-Process to invoke external console applications, invoke them directly (synchronously, with standard streams connected to PowerShell's streams), using &, if the executable name/path is quoted and/or contains variable references; do not quote the command line as a whole; specify and - if necessary - quote the executable name/path and arguments individually.
Similarly, Invoke-Expression should generally be avoided.
Therefore:
$env:VAR1 = 'VAL1'
$env:VAR2 = 'VAL2'
$env:VAR3 = 'VAL3'
& "$DRIVE\DIR\EXECUTABLE.exe" -FLAG1 -FLAG2 -FLAG3
I have a small "dev.bat" batch file on my PATH which I run to switch to my development project directory in W:\. This works fine from CMD but not when run from PowerShell (or PWSH).
I have no other problems running .bat files from PowerShell.
PS C:\> type C:\dev.bat
W:
CD W:\dev
PS C:\> dev.bat
me#computer C:\
> W:
me#computer W:\dev
> CD W:\dev
PS C:\> echo "Why did dev.bat not change directory??"
Why did dev.bat not change directory??
PS C:\> W:
PS W:\>
No, cmd /c dev.bat makes no difference.
When run from PowerShell, batch files invariably run in a (cmd.exe) child process[1], given that PowerShell itself doesn't understand the batch language.
Changing the working directory in a child process is limited to that child process (and its own children), and has no effect on the calling process; a child process cannot change the calling process' working directory.
Your only option is to:
have your batch file echo (print) the desired working directory
capture that path in PowerShell and pass it to Set-Location
If you don't want to change your batch file, use the following workaround:
Set-Location -LiteralPath (cmd /c 'dev.bat >NUL && cd')
# Or if you want to use the 'cd' alias for Set-Location and
# are confident that path never has "[" characters in it (so that
# it can't be mistaken for a wildcard expression):
cd (cmd /c 'dev.bat >NUL && cd')
If batch files needn't be involved at all, and you just want a convenient way to create custom functions that change to a predefined location (working directory), place the following function in your $PROFILE file:
# Helper function to place in $PROFILE, which generates custom quick-cd
# functions, based on a function name and target directory path.
function New-QuickCD ($Name, $LiteralPath) {
$funcDef = #"
function global:$Name { Push-Location -LiteralPath "$LiteralPath" } # quick-CD function
"#
Invoke-Expression $funcDef # define in current session too
$funcDef >> $PROFILE # append to $PROFILE
}
Note:
The generated functions use Push-Location rather than Set-Location to enable easy returning to the previous location with Pop-Location (popd).
For convenience, generated functions are also defined in the current session via Invoke-Expression[2] on creation, so you don't have to reload (dot-source) $PROFILE or open a new session before you can call the newly generated function.
Blindly appending to $PROFILE with >> means that if you redefine a function, the new definition will take effect, but the obsolete previous one will linger in the file, requiring manual cleanup; the comment # quick-CD function placed after each generated function is meant to facilitate that - see the bottom section for a more sophisticated version of New-QuickCD that updates old definitions in place.
You can make the function more robust and convenient in a variety of ways: making the parameters mandatory, verifying the path's existence (by default), resolving the path to an absolute one - again, see the bottom section.
E.g., to create a function named dev that switches to W:\dev, you'd then call:
# Generate function 'dev', which switches to 'W:\dev',
# append it to your $PROFILE file, and also define it in this session:
New-QuickCD dev W:\dev
# Call it:
dev # changes the current location to W:\dev; use 'popd' to return.
More robust, flexible New-QuickCD function:
It improves on the above version as follows:
It makes the parameters mandatory.
It verifies the existence of the target directory path.
It defines the functions with support for a -PrintOnly switch that merely prints the function's target directory, without changing to it.
It resolves a relative path to an absolute one first, so that you can run New-QuickCD foo . to define a function that switches to the absolute path of the current location.
When you redefine a function, the previous definition is automatically updated:
In order to enable this functionality $PROFILE is rewritten as a whole, using the > redirection operator.
To remove functions, you must still edit $PROFILE manually.
It comes with comment-based help; run help New-QuickCD -Examples, for instance.
function New-QuickCD {
<#
.SYNOPSIS
Creates a custom quick-CD function.
.DESCRIPTION
Creates a custom quick-CD function and appends it your $PROFILE file.
Such a function changes to a fixed location (directory) stored inside the
function, specified at creation time to allow for quickly changing to
frequently used directories using a short name.
For convenience, a newly created function is also defined for the running
session (not just for all future sessions).
The quick-CD functions use Push-Location to change location, which
enables you to easily return to the previously active location with
Pop-Location (popd).
To determine what location a given quick-CD function *would* change to,
invoke it with the -PrintOnly switch.
.PARAMETER FunctionName
The name of the quick-CD function to define.
.PARAMETER DirectoryPath
The literal path of the directory the quick-CD function should change to.
If given a relative path, it is resolved to an absolute one first.
For convenience, you may specify a *file* path, in which case that file's
parent path is used.
.NOTES
Your $PROFILE file is recreated every time you use this function, using the
> redirection operator, so as to support updating functions in place.
To *remove* a quick-CD function, edit $PROFILE manually.
.EXAMPLE
New-QuickCD dev W:\dev
Adds a 'dev' function to $PROFILE, which on invocation changes the current
location to W:\dev
* Call just 'dev' to change to W:\dev. Use popd to return to the previous
location.
* Call 'dev -PrintOnly' to print what location function 'dev' *would*
change to.
.EXAMPLE
New-QuickCD proj .
Adds a 'proj' function to $PROFILE, which on invocation changes to the
the location that is current at the time of calling New-QuickCd.
#>
param(
[Parameter(Mandatory)] [string] $FunctionName,
[Parameter(Mandatory)] [string] $DirectoryPath
)
Set-StrictMode -Version 1; $ErrorActionPreference = 'Stop'
# Resolve the path to a full path. Fail if it doesn't exist.
$fullPath = (Resolve-Path -ErrorAction Stop -LiteralPath $DirectoryPath).Path
# As a courtesy, if the path is a *file*, we use its parent path instead.
if (Test-Path -PathType Leaf $fullPath) {
$fullPath = [IO.Path]::GetDirectoryName($fullPath)
}
# Define a comment that identifies the functions we add to $PROFILE as
# quick-CD functions.
$idComment = '<# quick-CD function generated with New-QuickCD #>'
# Generate the new function's source code...
# * on a *single line*, which enables easy filtering when updating $PROFILE below
# * with a distinctive comment at the end of the line that identifies the
# function as a quick-CD function.
# * with the global: scope specifier, which makes it easier to call the
# same definition with Invok-Expression to make the function available in the
# current session too.
$newFuncDef = #"
$idComment function global:$FunctionName { param([switch] `$PrintOnly) if (`$PrintOnly) { "$fullPath" } else { Push-Location -LiteralPath "$fullPath" } }
"#
# ... define it in the current session (doing this *before* updating $PROFILE ensures early exit if the function name is invalid)
Invoke-Expression $newFuncDef
# ... and update $PROFILE:
# Get the current content of $PROFILE
[string] $currentProfileContent = if (Test-Path -LiteralPath $PROFILE) { Get-Content -Raw -LiteralPath $PROFILE }
# Try to replace an existing definition.
$newProfileContent = $currentProfileContent -replace ('(?m)^{0} function global:{1} .+$' -f [regex]::Escape($idComment), [regex]::Escape($FunctionName)), $newFuncDef
if (-not $currentProfileContent -or $newProfileContent -ceq $currentProfileContent) { # Profile didn't exist or nothing was replaced -> we must append the new definition.
$newProfileContent = $newProfileContent.TrimEnd() + [Environment]::NewLine * 2 + $newFuncDef
}
# Write the file.
$newProfileContent > $PROFILE
}
[1] By contrast, batch files run in-process when invoked from cmd.exe, analogous to how PowerShell runs its *.ps1 scripts in-process.
POSIX-like shells such as Bash, on the other hand, by default run their scripts in a child process, except when sourcing is used (., source)
[2] While this is a safe use of Invoke-Expression, it should generally be avoided.
Another simple way to do that is create a dev.ps1 (PowerShell Script file) rather batch file, whith the follow code Set-Location -Path "W:\dev"
Note: batch file runs on CMD process as a child process even if you runs it on powershell
#mkelement is correct: there is no simple way to do this from a .bat file on your path - that is old school. The proper PowerShell way is to create an alias to a function which does what you want.
Borrowing from this answer my solution is:
Step 1: Create a reusable function to make an alias:
PS> echo 'function myAlias {
$g=[guid]::NewGuid();
$alias = $args[0]; $commands = $args[1];
echo "function G$g { $commands }; New-Alias -Force $alias G$g">>$profile
};'>>$profile
Re-start powershell (to load the above function) and then define your dev shortcut as follows:
Step 2: Create a dev shortcut/alias which gets you where you want to be:
PS> myAlias dev "Set-Location W:\dev"
Step 3: Happily use dev
PS C:\> dev
PS W:\dev>
I have a batch script called SET_ENV.bat which contains environment variables that are used by other batch scripts. Currently this SET_ENV.bat is lauched by existing batch scripts.
Now I have a need to use Powershell script and I would like to launch the same SET_ENV.bat. I managed to do this using:
cmd.exe /c ..\..\SET_ENV.bat
I know that the batch file was run because it contained an echo
echo *** Set the environment variables for the processes ***
But after looking at the environment variables, I can see that none of them have been updated. Is there something that is preventing me from updating environment variables with Powershell + batch file combo?
I have tried SET_ENV.bat directly from command line and it works. I have also tried Start-Process cmdlet with "-Verb runAs" but that didn't do any good.
Launching PowerShell again at the end of the batch commands will keep every environment variable so far.
My use case was: set up Anaconda environment, set up MSVC environment, continue with that. Problem is both Anaconda and MSCV have a separate batch script that initialises the env.
The following command starting from PowerShell will:
initialise Anaconda
initialise MSVC
re-launch PowerShell
cmd.exe "/K" '%USERPROFILE%\apps\anaconda3\Scripts\activate.bat %USERPROFILE%\apps\anaconda3 && "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" && powershell'
Just swap the paths with what you need. Note that if the path contains spaces in needs to be inside double quotes ".
Breaking down the call above:
cmd.exe "/K": call cmd and do not exit after the commands finish executing /K
The rest is the full command, it is wrapped in single quotes '.
%USERPROFILE%\apps\anaconda3\Scripts\activate.bat %USERPROFILE%\apps\anaconda3: calls activate.bat with parameter ...\anaconda3
&& "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat": && and if the previous command didn't fail, run the MSVC vars setup file. This is wrapped in " as it has spaces in it.
&& powershell: finally run PowerShell. This will now contain all environment variables from the ones above.
Just adding a better way of doing the aforementioned setup: using Anaconda's PowerShell init script to actually get it to display the environment name on the prompt. I won't break down this as it's just a modified command above.
cmd.exe "/K" '"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" && powershell -noexit -command "& ''~\apps\anaconda3\shell\condabin\conda-hook.ps1'' ; conda activate ''~\apps\anaconda3'' "'
Note that the single quotes in the powershell call are all doubled up to escape them
Environment variables are local to a process and get inherited (by default at least) to new child processes. In your case you launch a new instance of cmd, which inherits your PowerShell's environment variables, but has its own environment. The batch file then changes the environment of that cmd instance, which closes afterwards and you return back to your PowerShell script. Naturally, nothing in PowerShell's environment has changed.
It works in cmd since batch files are executed in the same process, so a batch file can set environment variables and subsequently they are available, since the batch file wasn't executed in a new process. If you use cmd /c setenv.cmd in an interactive cmd session you will find that your environment hasn't changed either.
You can try another option, such as specifying the environment variables in a language-agnostic file, to be read by either cmd or PowerShell to set the environment accordingly. Or you could launch your PowerShell scripts from cmd after first running your batch file. Or you could set those environment variables under your user account to no longer have to care for them. Or you just have one setenv.cmd and one setenv.ps1 and keep them updated in sync.
Summary
Write the environment variables to file and load them after.
Example
I've included an MWE below that exemplifies this by saving and loading the VS-studio environment.
Usage
To run the script, call New-Environment. You will now be in the VS2022 environment.
How it works
The first time New-Environment is called, the VS-studio environment batch file runs, but the results are saved to disk. On returning to PowerShell the results are loaded from disk. Subsequent times just use the saved results without running the environment activator again (because it's slow). The New-Environment -refresh parameter may be used if you do want to resave the VS-studio environment again, for instance if anything has changed.
Script
NOTE: This script must be present in your powershell $profile so the second instance can access the function! Please ensure to change the VS path to reflect your own installation.
function New-Environment()
{
param (
[switch]
$refresh
)
Write-Host "Env vars now: $($(Get-ChildItem env: | measure-object).Count)"
$fileName = "$home\my_vsenviron.json"
if ((-not $refresh) -and (Test-Path $fileName -PathType Leaf))
{
Import-Environment($fileName)
return;
}
$script = '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" && '
$script += "pwsh --command Export-Environment `"$fileName`""
&"cmd.exe" "/C" $script
Import-Environment($fileName)
}
function Export-Environment($fileName)
{
Get-ChildItem env: | Select-Object name,value | ConvertTo-Json | Out-File $fileName
Write-Host "I have exported the environment to $fileName"
}
function Import-Environment($fileName)
{
Get-Content $fileName | ConvertFrom-json | ForEach-Object -process {Set-Item "env:$($_.Name)" "$($_.Value)"}
Write-Host "I have imported the environment from $fileName"
Write-Host "Env vars now: $($(Get-ChildItem env: | measure-object).Count)"
}
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 }