1

I sometimes encounter situations where something like this happens in a Bash script:

pushd /tmp
wget -q http://download.redis.io/redis-stable.tar.gz
tar xzf redis-stable.tar.gz
pushd redis-stable
make
sudo make install
popd; popd

The important bits are the two pushds, ending with two matching popds in a row. That last line always bothered me; it seems like there should be a better way to express the intention of popping an arbitrary number of items off the stack and switching back to the original directory.

I've searched the Bash man pages and tried variants of the popd +n and -n arguments, but all they seem to do is remove a single item from the stack each time. That's not what I'm looking to do; I'm trying to unwind the directory stack, however many entries deep it may be, back to some previous state.

Short of switching to a paradigm where I save OLDPWD=$(pwd) somewhere and cd $OLDPWD at the end, is there a better way to do this?

  • Can you better clarify what you mean by "some previous state"? Would you pass a number like mypopd 5, or would it always be to the top? – Jeff Schaller Aug 7 '16 at 18:06
  • @JeffSchaller I'd accept either, honestly. Although it seems more immediately useful to have mypopd 5, otherwise the user has to give up a certain degree of control over how far it goes. – smitelli Aug 7 '16 at 19:52
  • here's my answer on another question that addresses the same issue. – Eliran Malka Mar 23 '17 at 16:16
3

The use of the directory in scripts is questionable because it's risk-prone. popd doesn't return to the original directory, it returns to the directory with the same name. If the original directory has been moved, your script will switch locations midway. This may seem like a remote risk, but sooner or later this will happen and the users of the script will curse you.

If you want to temporarily act in a different directory, the best way is to run that part of the script in a subshell.

( set -e
  cd "${TMPDIR:-/tmp}"
  wget -q http://download.redis.io/redis-stable.tar.gz
  tar xzf redis-stable.tar.gz
  cd redis-stable
  make
  sudo make install
)

Just for the sake of the example, I'll show alternative approaches which are not necessary here but can be useful in similar situations where a subshell is not suitable, e.g. because you want to set some variables and have them available after returning to the original directory.

You might not actually need to switch to a different directory, or only for the duration of an individual command. If you're using GNU tools, both tar and make support a -C argument to switch to a directory. Given that you're creating that directory in your script, unlike the original directory, it is a reasonable assumption that its location won't move.

set -e
: "${TMPDIR:=/tmp}"
wget -q -O "$TMPDIR/redis-stable.tar.gz" http://download.redis.io/redis-stable.tar.gz
tar xzf redis-stable.tar.gz -C "$TMPDIR"
make -C "$TMPDIR/redis-stable"
sudo make -C "$TMPDIR/redis-stable" install

With commands that don't have an option to switch to a different directory, you can use a subshell with a narrow scope.

set -e
: "${TMPDIR:=/tmp}"
( cd "$TMPDIR"
  wget -q http://download.redis.io/redis-stable.tar.gz
  tar xzf redis-stable.tar.gz -C "$TMPDIR"
)
( cd "$TMPDIR/redis-stable"
  make
  sudo make install
)

If you really need to switch back and forth, the obvious improvement to multiple calls to popd is to call cd when you mean cd, instead of calling pushd for a directory you don't want to return to.

set -e
pushd "${TMPDIR:-/tmp}"
wget -q http://download.redis.io/redis-stable.tar.gz
tar xzf redis-stable.tar.gz
cd redis-stable
make
sudo make install
popd

But I'd really recommend not using pushd and popd at all. They're meant for interactive use, and in scripts they tend to be more confusing than anything. If you want to save the location of the current directory, save it in a variable. This makes your script easier to read, this way the reader doesn't have to figure out where a popd call will return.

set -e
original_directory="$PWD"
cd "${TMPDIR:-/tmp}"
wget -q http://download.redis.io/redis-stable.tar.gz
tar xzf redis-stable.tar.gz
cd redis-stable
make
sudo make install
cd "$original_directory"
1

If you have a mental image of the level in the dirs stack that you want to pop up, you could use a function:

function mpopd {
  n=$1
  while [[ $n > 0 ]]
  do
    popd
    n=$((n-1))
  done
}

Sample run:

[schaller@host smitelli] dirs
~/tmp/smitelli
[schaller@host smitelli] pushd tmp/
~/tmp/smitelli/tmp ~/tmp/smitelli
[schaller@host tmp] pushd redis-stable/
~/tmp/smitelli/tmp/redis-stable ~/tmp/smitelli/tmp ~/tmp/smitelli
[schaller@host redis-stable] mpopd 2
~/tmp/smitelli/tmp ~/tmp/smitelli
~/tmp/smitelli
0

You could simply start a new child shell before beginning your work, then exit it at the end, getting back to your original shell and directory.

But it is perhaps best to keep your directory history, as often you need to repeat the same pushd later. I actually alias cd to the equivalent of pushd then use other functions to easily navigate the dirstack without losing any of it. Rather than that you can do something simple such as a function to list your dirstack and ask which to go to:

cdp(){
  select v in $(dirs -l -p)
  do pushd "$v"
     break
  done
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.