How can I convert a multi-line batch file statement into Powershell? - powershell

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

Related

How to apply EWDK's SetupBuildEnv.cmd to running powershell session

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.

How do I have to change PowerShell variables code so that I can run it via CMD?

How do I have to change PowerShell code so that I can run it via CMD?
I came up with the following code:
$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt
$text_auslesen.Replace("Count :","") > $env:APPDATA\BIOS-Benchmark\Count_only.txt
$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt
$text_auslesen.Replace("Average :","") > $env:APPDATA\BIOS-Benchmark\Durchschnitt_only.txt
If I copy and paste it completely into a powershell, it can run. But now I have to put the code next to other code in a batch file. How do I have to adjust the code so that the cmd.exe executes the whole thing?
I suspect setting the variables via Powershell code is problematic here.
Unfortunately, a PS1 file is out of the question for my project.
To execute PowerShell commands from a batch file / cmd.exe, you need to create a PowerShell child process, using the PowerShell CLI (powershell.exe for Windows PowerShell, pwsh for PowerShell (Core) 7+) and pass the command(s) to the -Command (-c) parameter.
However, batch-file syntax does not support multi-line strings, so you have two options (the examples use two simple sample commands):
Pass all commands as a double-quoted, single-line string:
powershell.exe -Command "Get-Date; Write-Output hello > test.txt"
Do not use quoting, which allows you to use cmd.exe's line continuations, by placing ^ at the end of each line.
powershell.exe -Command Get-Date;^
Write-Output hello ^> test.txt
Note:
In both cases multiple statements must be separated with ;, because ^ at the end of a batch-file line continues the string on the next line without a newline.
Especially with the unquoted solution, you need to carefully ^-escape individual characters that cmd.exe would otherwise interpret itself, such as & and >
See this answer for detailed guidance.
Powershell -c executes PowerShell commands. You can do this from cmd, however, it looks like it needs to be run as administrator.
PowerShell -c "$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt;
$text_auslesen.Replace('Count :','') > $env:APPDATA\BIOS-Benchmark\Count_only.txt;
$text_auslesen = Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt;
$text_auslesen.Replace('Average :','') > $env:APPDATA\BIOS-Benchmark\Durchschnitt_only.txt"
It is possible to execute the PowerShell code in a batch file, but technically what you are doing is pulling a copy of it out and executing it someplace else. Here are 3 methods that I know of.
mklement0's answer addresses executing a copy of it that is passed as a parameter to PowerShell.
You could build a ps1 file from CMD, and then execute that ps1 file by passing it as a parameter to PowerShell.
And the method I've worked with the most is to pass specially designed PowerShell code to PowerShell that, when it runs, will load all, or part, of the current CMD file into memory and execute it there as a ScriptBlock. I have tried loading parts of the current CMD file, but my experience has been that this gets too complicated and I just stick with loading the entire current CMD file.
That last method is what I'm presenting here. The trick is to make the batch/CMD portion of the script look like a comment that is ignored by PowerShell, but still runs without throwing error messages in CMD. I'm not sure where I first found this trick, but it goes like this:
First, place <# : at the start of script. PowerShell sees this as the start of a comment, but CMD seems to ignore this line. I think CMD is trying to redirect < the contents of a non-existing file : to a non-existing command. But what does CMD do with the #? It works, and that's the important thing.
Place your batch code in lines following the <# :.
You end the batch/CMD part with a GOTO :EOF.
You then end the PowerShell comment with #>, but visually I find it easier to find <#~#>, which does the same job.
The rest of the file is your PowerShell code.
This version treats the PowerShell code as a function with defined parameters. The batch part builds %ARGS% and passes, with double quotes intact, to a PowerShell ScriptBlock that in turn is wrapped in another ScriptBlock. The PowerShell function is called twice with the same SourceFile parameter, but different DestinationFile and TextToRemove parameters. Perhaps there is a simpler way to reliably pass double quotes " in arguments passed to a ScriptBlock from batch, but this is the method I got working.
<# :
#ECHO OFF
SET f0=%~f0
SET SourceFile=%APPDATA%\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt
SET ARGS="%SourceFile%" "%APPDATA%\BIOS-Benchmark\Count_only.txt" "Count :"
PowerShell -NoProfile -Command ".([scriptblock]::Create('.([scriptblock]::Create((get-content -raw $Env:f0))) ' + $Env:ARGS))"
SET ARGS="%SourceFile%" "%APPDATA%\BIOS-Benchmark\Durchschnitt_only.txt" "Average :"
PowerShell -NoProfile -Command ".([scriptblock]::Create('.([scriptblock]::Create((get-content -raw $Env:f0))) ' + $Env:ARGS))"
GOTO :EOF
<#~#>
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$SourceFile,
[Parameter(Mandatory = $true, Position = 1)]
[string]$DestinationFile,
[Parameter(Mandatory = $true, Position = 2)]
[string]$TextToRemove
)
(Get-Content $SourceFile).Replace($TextToRemove, '') > $DestinationFile
This script passes a single parameter that, in PowerShell, is used by the Switch command to decide which section of PowerShell you intend on executing. Since we are not passing double quotes " in the args, the PowerShell lines can be greatly simplified. Information could still be passed to PowerShell by defining environmental variables in batch and reading them in PowerShell.
<# :
#ECHO OFF
SET f0=%~f0
PowerShell -NoProfile -Command .([scriptblock]::Create((get-content -raw $Env:f0))) Script1
PowerShell -NoProfile -Command .([scriptblock]::Create((get-content -raw $Env:f0))) Script2
GOTO :EOF
<#~#>
switch ($args[0]) {
'Script1' {
(Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt).Replace("Count :", '') > $env:APPDATA\BIOS-Benchmark\Count_only.txt
break
}
'Script2' {
(Get-Content $env:APPDATA\BIOS-Benchmark\PowerShell-Protokoll-Auswertung.txt).Replace("Average :", '') > $env:APPDATA\BIOS-Benchmark\Durchschnitt_only.txt
break
}
default {}
}
The -c parameter is intended to solve this scenario.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pwsh?view=powershell-7.2#-command---c
If possible, it would be more efficient to invoke PowerShell\Pwsh directly rather than using a cmd wrapper.

Setting environment variables in powershell script reading them from a .bat file

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.

In Windows power shell, how do you extract a properties file value and save it to an env var?

I have a properties file with entries like the below ...
...
USERNAME=myuser
...
In my Makefile, I have the below which uses Unix like commands to get the value of the variables ...
export USERNAME=$(shell grep USERNAME my_properties.txt | cut -d'=' -f 2-)
However, in a Windows power shell (maybe command prompt is the right phrase?), the above doesn't work because "grep" is not a standard command (among others). What's the equivalent way to extract a property from a properties file in a Windows power shell environment?
We could achieve this in PowerShell by following the below steps
Read the contents of the file
Convert the contents into key-value pairs
Create environment variable with the required value
(you can combine the steps if you like, I've kept them separate for better understanding)
Here's the script
$content = Get-Content .\user.properties -raw
$hashTable = ConvertFrom-StringData -StringData $content
$Env:USERNAME = $hashTable.USERNAME
Assuming that cmd.exe is the default shell:
export USERNAME=$(shell powershell -noprofile -c "(Select-String 'USERNAME=(.+)' my_properties.txt).Matches.Group[1]")
Note: -NoProfile suppresses loading of PowerShell's profiles, which unfortunately happens by default. Should you need the -File parameter to execute a script file, you may additionally need -ExecutionPolicy Bypass, unless your effective execution policy allows script execution.
The above uses the PowerShell CLI's -c (-Command) parameter to pass a command that uses the Select-String cmdlet, PowerShell's grep analog.
A closer analog to your command would be the following, which additionally uses -split, the string-splitting operator (showing the raw PowerShell command only; place it inside the "..." above):
((Select-String USERNAME my_properties.txt) -split '=', 2)[-1]

Batch/Shell script to update to text file arguments

I have a text file with the contents
example.txt
My name is {param1}
I live in {param2}
These are all the parameters passed to batch scripts {params}
How should i write and Batch/shell script to pass arguments to example.txt file
Caveat: Use the following solutions only with input files that you trust, because it is possible to embed arbitrary commands inside the text (preventing that is possible, but requires more work):
In a PowerShell script, named, say Expand-Text.ps1:
# Define variables named for the placeholders in the input file,
# bound to the positional arguments passed.
$param1, $param2, $params = $args[0], $args[1], $args
# Read the whole text file as a single string.
$txt = Get-Content -Raw example.txt
# Replace '{...}' with '${...}' and then use
# PowerShell's regular string expansion (interpolation).
$ExecutionContext.InvokeCommand.ExpandString(($txt -replace '\{', '${'))
Calling .\Expand-Text.ps1 a b c then yields:
My name is a
I live in b
These are all the parameters passed to batch scripts a b c
In a batch file, named, say expandText.cmd, using PowerShell's CLI:
#echo off
:: # \-escape double quotes so they're correctly passed through to PowerShell
set params=%*
set params=%params:"=\"%
:: "# Call PowerShell to perform the expansion.
powershell.exe -executionpolicy bypass -noprofile -c ^
"& { $txt = Get-Content -Raw example.txt; $param1, $param2, $params = $args[0], $args[1], $args; $ExecutionContext.InvokeCommand.ExpandString(($txt -replace '\{', '${')) }" ^
%params%
You don't need to create the text file. I am giving a
Code of batch file to create the text file and pass arguments to it.
#echo off
echo My name is %1 >example.txt
echo I live in %2 >>example.txt
echo These are all the parameters passed to batch scripts %* >>example.txt
goto :eof
Save it as param.bat and run it as param.bat param1 param2.