1

I want to call a Batch file from a PowerShell script. This Batch file calls another Batch file which resides in the same directory. The PowerShell script is in a different directory. For example consider this files/directories:

C:
+-- Test
    +-- Batch
        + -- Foo.bat
        + -- Bar.bat
    +-- PS
        +-- Foo.ps1

Foo.bat simply calls Bar.bat:

call Bar.bat

And in Foo.ps1 I'm calling Foo.bat:

Set-Location "C:\Test\Batch"
Invoke-Item "Foo.bat"

My problem now is, that Foo.bat gets called but still uses C:\Test\PS as its working directory and therefore cannot find Bar.bat. How can I force Foo.bat to use C:\Test\Batch as its working directory?

Neither changing Foo.bat nor putting Foo.ps1 in the same directory as Foo.bat are options.

4
  • 1
    Open a command prompt window, run call /? and read the output usage help. Argument 0 is always the string used to start the processing of a batch file. It is Foo.bat and Bar.bat in your examples. There can be used in Foo.bat the command line call "%~dp0Bar.bat" to reference the batch file Bar.bat with full path of the currently processed batch file Foo.bat. %~dp0 references the full path of the currently processed batch file which always ends with a backslash. Commented Apr 17 at 16:31
  • 1
    NOTE: The recommended file extension for batch files processed by the Windows Command Processor cmd.exe is .cmd. The file extension .bat is still supported but should not be used nowadays anymore. The file extension .bat is for batch files processed by COMMAND.COM of MS-DOS and Windows 95/98/ME. The Windows Command Processor processes batch files with file extension .bat a very little bit different to batch files with file extension .cmd. See: Windows batch files: .bat vs .cmd? Commented Apr 17 at 16:41
  • I've installed MS-DOS, Windows 3.11, Windows 95. I've even programmed on these operating systems. I used the FOR command a lot. They always said that Microsoft wanted to do away with EXE files, but to this day it has never done away with COM files (chcp, format, mode, etc.). They still exist in Windows 11 24H2! I've always used %~dp0 for both BAT and CMD, and I agree that the file should have the CMD extension, but the poster was clear: Neither changing Foo.bat, so you can't change it to CMD and you can't have %~dp0. Commented Apr 17 at 18:31

2 Answers 2

3

Preface:

  • This answer addresses the question as asked, but the improvements suggested by Mofi in the comments on the question are, in general, well worth considering, especially making Foo.bat (better: Foo.cmd) call Bar.bat using call "%~dp0Bar.bat", which makes the call predictably target Bar.bat in the same directory where Foo.bat is located, irrespective of what the current directory is.

  • Use direct invocation to invoke a batch file in PowerShell in order to execute it synchronously, in the same console window as the current PowerShell session.

  • Not only does this ensure that the current PowerShell location (directory) is inherited by the batch file, it also connects the batch file's standard streams to PowerShell's streams (which enables capturing or redirecting output) and reports the exit code via the automatic $LASTEXITCODE variable.

Therefore:

Set-Location "C:\Test\Batch" # See non-hard-coded alternative below.
& .\Foo.bat # Direct, synchronous invocation in the current console window.
# $LASTEXITCODE now contains the batch file's exit code.

Note:

  • Use of &, the call operator, isn't strictly necessary here, but would be if your batch-file path were quoted and/or contained variable references.

  • Note the need to use an explicit path in order to execute an executable located in the current directory, in the simplest case .\
    Unlike cmd.exe, PowerShell by security-minded design allows name-only invocation (e.g. foo.cmd) only of executables that are discoverable via the PATH environment variable ($env:PATH).

  • Given your directory structure, you could use the automatic $PSScriptRoot variable to avoid having to hard-code the path passed to Set-Location:

    Set-Location "$PSScriptRoot\..\..\Batch"
    

As for what you tried:

  • Invoke-Item is the wrong to tool for invoking executables - its purpose is to open documents, deferring to the Windows (GUI) shell for the appropriate executable (application) to open the document with.

    • While you can target executables with Invoke-Item (in which case use of an explicit path to target executables in the current directory is not strictly needed), this is ill-advised for several reasons:

      • In the case of a console executable, it invariably executes in a new console window, and that window closes automatically when the executable process exits.

      • As you've observed, PowerShell's notion of the current directory is not guaranteed to be honored: Invoke-Item in effect uses the current process' current directory, which typically differs from PowerShell's (see this answer for background information).

      • The call is invariably asynchronous, you cannot capture output or determine the process exit code.

  • While Start-Process can overcome these limitations in principle (the launched process inherits PowerShell's working directory, you can make the call synchronous with -Wait, you can query the exit code via the process-info object returned via -PassThru, you can capture output, albeit only in files) it too is generally the wrong tool for invoking console applications such as batch files - use direct invocation instead, as shown in the top section.

Sign up to request clarification or add additional context in comments.

5 Comments

Thank you for sharing this important information. Since 2015, I stopped programming in CMD and BAT. I always did everything in PS1. Much better and more practical. But I understand why some people need to keep their old scripts. I have a slightly different experience. At first, I used: ( Invoke-Expression -Command $dummy ) 2>&1 If ( $LastExitCode ) {} After learning about the Start-Process cmdlet, I adopted it without any problems for batch files. It has RedirectStandardError, RedirectStandardInput and RedirectStandardOutput arguments and I use them when necessary.
Now, regarding the issue you raised about opening in the same console, I agree with the direct invoke. And I routinely do this in PS1 scripts with another operator: . '.\scrpits.ps1', to maintain the same environment. The issue of running in the same console was not clear in the post. Since I usually develop routines that run in the background, it doesn't matter whether or not I'm on the same console.
@JoãoMac: Except in unusual cases, you want batch files to execute synchronously, in the same console window. That's what direct invocation does, while giving you access to all streams. Re output streams (stdout and stderr), see my comment on your answer. Re stdin: use the pipeline ... | .\Foo.bat. Invoke-Expression (iex) should generally be avoided and used only as a last resort, due to its inherent security risks - see this answer.
Thank you for your answer and explanation. As I wrote and explained in more detail, in the sector of the company I work for, it is common to run tasks in the background. In this matter, we have different realities. Regarding iex, I only used it when I was learning PowerShell; I never used it again.
@JoãoMac: Understood re running tasks in the background, but that still doesn't amount to a good reason to use Start-Process - direct invocation is simpler and more flexible, irrespective of whether a task runs in the foreground or background.
2

Avoid using the Invoke-Item cmdlet to execute commands. Use the Start-Process cmdlet instead.

The problem is that the working folder is not defined. There are several ways to define this. Using the Start-Process cmdlet is the most appropriate for this specific case.

$Result = Start-Process -FilePath 'cmd.exe' -ArgumentList '/c Foo.bat' -WorkingDirectory 'C:\Test\Batch' -Wait -PassThru
Write-Output -InputObject "code $( $Result.ExitCode )"

The Wait and PassThru arguments will be required if you want to check the exit code.

I used an online translator. I apologize for not being fluent in the language.

6 Comments

Good point about Invoke-Item, but direct invocation is much simpler and preferable to use of Start-Process: Set-Location C:\Test\Batch; .\Foo.bat; "code $LASTEXITCODE" is enough.
I have a question. Using the direct form, how do I handle the difference between RedirectStandardOutput and RedirectStandardError? In Start-Process cmdlet there is this option.
To redirect stdout output only: .\Foo.bat >stdout.txt. To redirect stderr output only: .\Foo.bat 2>stderr.txt. To capture both: .\Foo.bat *>alloutput.txt
Indeed it would: you can test with a quick simulation: cmd /c 'echo stdout & echo stderr >&2' >stdout.txt 2>stderr.txt
I created the file foo.bat with the line echo stdout & echo stderr >&2. Then I invoked in powershell console .\Foo.bat >stdout.txt 2>stderr.txt. It worked!
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.