skolima
skolima

Reputation: 32684

How can I source variables from a .bat file into a PowerShell script?

I'm replacing parts of a .bat script with PowerShell. Configuration for the batch files is done via files that set appropriate environment variables. I'm looking for a way to load those variable values into the .ps1 script, without modifying the .bat files (as they are also used in other places.

An example .bat looks as follows:

set VAR_ONE=some_value
set VAR_TWO=/other-value

In a batch script, I'd just CALL the configuration file and the variables would be available. I've tried both dot-sourcing (. filename.bat) and calling (& filename.bat) the configuration files from PowerShell, neither of those makes the variables visible. Tried accessing them with both with $VAR_ONE and $env:VAR_ONE syntax.

What would be a good way to load such configuration file without modifying it's format on disk?

Upvotes: 26

Views: 18160

Answers (6)

mklement0
mklement0

Reputation: 438018

  • Calling a batch file from PowerShell of necessity executes the batch file in a child process, given that a cmd.exe child process must be launched to execute it.

  • Whatever environment variables are set in a child process (batch file) are not seen by the calling process (PowerShell).

Therefore, a workaround is required:

cmd /c 'sample.cmd >NUL & SET' | 
  Where-Object { $_ -notmatch '^PROMPT=' } |
  ForEach-Object {
    # Split the "<name>=<value>" line into the variable's name and value.
    $name, $value = $_ -split '=', 2
    # Define it as a process-level environment variable in PowerShell.
    Set-Content ENV:$name $value
  }

Note:

  • This recreates the batch-file environment variables as environment variables in PowerShell too, using the Env: PowerShell drive and Set-Content, thereby ensuring that external programs launched from your PowerShell session see them too.

    • By contrast, if Set-Variable were used, it is PowerShell-only variables that would get created, which would only be seen by the current PowerShell session, not also by child processes (which external programs invariably run in) called from it.
  • Executing SET (unconditionally after the batch-file call, via &) prints all then-current environment-variable definitions as <name>=<value> pairs.

    • cmd.exe also defines a PROMPT environment variable, which isn't relevant to PowerShell or other processes, so it is filtered out above.
  • Via the ForEach-Object call, $name, $value = $_ -split '=', 2 then splits each <name>=<value> pair into its constituent parts.

  • Assumptions:

    • A fundamental prerequisite is that the target batch file must not use setlocal (as that would limit the scope of the variables to that batch file and the subsequent SET command wouldn't see them); that said, batch files whose express purpose is to set environment variables for the caller by design do not.

    • The assumption is that sole intent of invoking the batch file is to capture the environment variables that are set by the batch file; therefore, the latter's output - if any - is discarded (>NUL), which simplifies parsing of the output.

    • Also, it is assumed that it isn't necessary to propagate removal of environment variables by the batch file.

    • If either or both of the latter two assumptions do not hold, you can find solutions in this answer.

Upvotes: 1

Ansgar Wiechers
Ansgar Wiechers

Reputation: 200293

The preferred option would be to change the configuration to a .ps1 file and change the variable definitions to PowerShell syntax:

$VAR_ONE = 'some_value'
$VAR_TWO = '/other-value'

Then you'll be able to dot-source the file:

. filename.ps1

If you want to stick with the format you currently have, you'll have to parse the values, e.g. like this:

Select-String '^set ([^=]*)=(.*)' .\filename.bat | ForEach-Object {
    Set-Variable $_.Matches.Groups[1].Value $_.Matches.Groups[2].Value
}

Note: The above won't work in PowerShell versions prior to v3. A v2-compatible version would look like this:

Select-String '^set ([^=]*)=(.*)' .\filename.bat | ForEach-Object {
    $_.Matches
} | ForEach-Object {
    Set-Variable $_.Groups[1].Value $_.Groups[2].Value
}

Upvotes: 7

Keith Hill
Keith Hill

Reputation: 201672

If you are using the PowerShell Community Extensions, it has a Invoke-BatchFile that does this. I use with the Visual Studio vcvarsall.bat file to configure my PowerShell session to use the Visual Studio tools.

Upvotes: 24

Aacini
Aacini

Reputation: 67216

You can do that via a Batch file that first call the configuration file and then execute the PowerShell script.

Upvotes: 3

Iain Ballard
Iain Ballard

Reputation: 4818

Assuming the .bat is called test.bat, define testps.ps1:

$lines = cat "test.bat"
$output = @{};

foreach ($line in $lines) {
    $bits = $line.Split("=");
    $name = $bits[0].Split(" ")[1];
    $val = $bits[1];
    $output[$name] = $val
}

return $output

Then the result is something like:

C:\temp> .\testps.ps1

Name                           Value
----                           -----
VAR_TWO                        /other-value
VAR_ONE                        some_value


C:\temp> $x = .\testps.ps1
C:\temp> $x

Name                           Value
----                           -----
VAR_TWO                        /other-value
VAR_ONE                        some_value


C:\temp> $x["VAR_ONE"]
some_value

There is probably a nicer way of doing the splits (will edit if I find it)

Upvotes: 1

Adriano Repetti
Adriano Repetti

Reputation: 67090

I'd parse them (just skip all lines that don't start with set and split them with first = character. You can do it from o small C# cmdlet or directly with a small PowerShell script:

CMD /c "batchFile.bat && set" | .{process{
    if ($_ -match '^([^=]+)=(.*)') {
        Set-Variable $matches[1] $matches[2]
    }
}}

I have this code and I'm sure it comes from somewhere but credits have been lost, I suppose it comes from Power Shell Community Extensions for an Invoke-Batch script.

Upvotes: 8

Related Questions