3

Is there a way to undo all pushd at the end of script. What I have is:

pushd somwhere
rem doing stuff
goto end

:end
popd
goto :EOF

What I'd like to have is:

setlocal
pushd somwhere
rem doing stuff
goto :EOF

But that doesn't work, the directory stays "pushd". Another try where I can't say how many pushd will occur would be:

set CurrentDir=%CD%
rem doing a lot of pushd and popd
pushd somwhere

:POPBACK
if /i not "%CD%" == "%CurrentDir%" popd & goto POPBACK

But that looks like I can easily get stuck at :POPBACK. Adding a counter to exit that loop is a bit uncertain at which dir I end up.

In that context I'd like to know if a can see the stack of pushd directories. The only thing I found was $+ in prompt $P$+$G which adds a '+' for each pushd directory e.g. D:\CMD++>. But I can't see a way how to make use of that.

EDIT: I just noticed there is something wrong with the $+ of prompt that confused me. The setlocal in the example above actually makes the script return to the initial directory, but the prompt shows a '+' indicating a missing popd.

D:\CMD>test.cmd
D:\CMD>prompt $P$+$g
D:\CMD>setlocal
D:\CMD>pushd e:\DATA
e:\DATA+>goto :EOF
D:\CMD+>popd
D:\CMD>

EDIT: Due to the hint to the difference between local environment and directory stack I expanded the example form above, pushing to C:\DATA before calling the script. the script returns to where it has been called from (C:\DATA). So far so good. The first popd shows no effect as it removes the last pushd from stack but not changing the directory, since the directory change has been undone by the (implicit) endlocal. The second popd returns to initial directory as expected.

D:\CMD>pushd C:\DATA
C:\DATA+>d:\cmd\test.cmd
C:\DATA+>prompt $P$+$g
C:\DATA+>setlocal
C:\DATA+>pushd e:\DATA
e:\DATA++>goto :EOF
C:\DATA++>popd
C:\DATA+>popd
D:\CMD>
aschipfl
  • 33,626
  • 12
  • 54
  • 99
ooLi
  • 63
  • 6
  • 2
    The easiest way is to simply avoid it and use `cd /D` instead; when you use `setlocal`, the environment is localised, and the current directory is also part of it; localization is always (!) properly ended by an implicit `endlocal`… – aschipfl Feb 09 '22 at 08:18
  • The `pushd` is to use UNC paths. I forgot to write the endlocal, luckily it's implicit :-). – ooLi Feb 09 '22 at 08:32
  • 1
    Environment localisation and putting directories on the `pushd`/`popd` stack are totally different things, so `endlocal` does unfortunately not at all affect the directory stack… – aschipfl Feb 09 '22 at 09:01
  • I tried that last example with and without `setlocal` ending up either in D:\CMD or in E:\DATA. Right now I'd say it does undo the pushd. But it appears it is not done by a `popd` thus having the remaing '+' at the `prompt`. – ooLi Feb 09 '22 at 09:22
  • Did you try that in directly Command Prompt? because `setlocal`/`endlocal` has no effect then… – aschipfl Feb 09 '22 at 09:32
  • Not entirly, the `setlocal` and `pusd` are in test.cmd – ooLi Feb 09 '22 at 09:45

3 Answers3

5

The pushd command without any arguments lists the contents of the directory stack, which can be made use of, by writing the list with output redirection > to a (temporary) file, which is then read by a for /F loop, and to determine how many popd commands are necessary.

In a :

pushd > "tempfile.tmp" && (
    for /F "usebackq delims=" %%P in ("tempfile.tmp") do popd
    del "tempfile.tmp"
)

Directly in :

pushd > "tempfile.tmp" && (for /F "usebackq delims=" %P in ("tempfile.tmp") do @popd) & del "tempfile.tmp"

Note, that you cannot use for /F "delims=" %P in ('pushd') do ( … ), because for /F would execute pushd in a new instance of cmd.exe, which has got its own new pushd/popd stack.


One way to prevent residues from the pushd/popd stack is to simply avoid pushd and to use cd /D instead within a localised environment (by setlocal/endlocal in a batch file), because, unlike popd, endlocal becomes implicitly executed whenever necessary (even if the batch file contains wrong syntax that leads to a fatal error that aborts execution, like rem %~). This of course only works when there are no UNC paths to be handled (that is, to be mapped to local paths).


It is advisable to use pushd/popd only in a limited code section with out any branches (by if, goto, etc.). Additionally, ensure to account for potential errors (using conditional execution here):

rem // First stack level:
pushd "D:\main" && (
    rem // Second stack level:
    pushd "D:\main\sub" && (
        rem /* If `D:\main\sub` does not exist, `pushd` would obviously fail;
        rem    to avoid unintended `popd`, use conditional execution `&&`: */
        popd
    )
    rem // We are still in directory `D:\main` here as expected.
    popd
)
aschipfl
  • 33,626
  • 12
  • 54
  • 99
  • Yes, @Aacini, `popd` indeed sets the exit code (proven by `popd || rem/`, upon which `ErrorLevel` is also set to `1` with an empty stack), which I was not aware of… – aschipfl Feb 09 '22 at 14:42
  • Well, this lack of knowledge cost you 15 rep points... **`¯\_(ツ)_/¯`** But I gave you 10 to compensate! **`:)`** – Aacini Feb 09 '22 at 14:45
  • Well, @Aacini, this loss of reputation is absolutely worth the gain of knowledge… `;)` – aschipfl Feb 09 '22 at 14:48
  • @Aacini, off course.. I don't know why I did not focus on the exitcode on windows.. I kept thinking `errorlevel` while I wrote the comment.. I also have a version to post as an Answer.. hold a sec. – Gerhard Feb 09 '22 at 14:52
3

Two points here:

1. Setlocal/Endlocal commands save/restore current directory!

This means that when Endlocal command is executed, the current directory is returned to the point it has when the previous Setlocal command was executed. This happens no matter if the Endlocal is explicitly executed, or it is implicitly executed by the termination of the called subroutine (via exit /B or goto :EOF commands).

This behaviour could be used to solve your problem, because all changes of the current directory are reverted when endlocal command is executed. That is:

setlocal
cd somwhere
rem doing stuff
goto :EOF

However note that this behavior is not related to pushd/popd commands! You can not mix the use of this feature with pushd/popd commands. You would need to complete several tests until the mechanism of the interaction between these two features be comprehended.

This is an undocumented behavior discussed here.


2. POPD command sets the Exit Code when there is no previous PUSHD

As described below Exit Code management section at this answer, there are some commands that does not set the ErrorLevel when an error happens, but sets an internal value we could call "Exit Code". This value can be tested via the && || construct this way:

command && Then command when Exit Code == 0 || Else command when Exit code != 0

POPD is one of this type of commands (described under Table 5), so we could use this feature to execute POPD several times until there is not a matching PUSHD previously executed:

@echo off
setlocal

echo Enter in:
for /L %%i in (1,1,%1) do echo %%i & pushd ..

echo Leave out:
set "n=0"
:nextLevel
popd || goto exitLevels
   set /A n+=1
   echo %n%
   goto nextLevel
:exitLevels

echo Done

Bottom line: a simple popd && goto POPBACK line solves your problem...

Aacini
  • 65,180
  • 12
  • 72
  • 108
0

After my own reminder of exit codes in a now deleted comment and the answer posted by @Aacini, one more version

@echo off
pushd "somewhere"
pushd "somewhere else"

:pop
popd && (echo %cd% & goto :pop) || goto :EOF

Where echo %cd% will only show the each item in the stack, so can be removed and simply become:

:pop
popd && goto :pop || goto :EOF
Gerhard
  • 22,678
  • 7
  • 27
  • 43