Shell – Is it possible to perform shell command substitution without using a subshell

directoryshell

I have a scenario that calls for command substitution without using a subshell. I have a construct like this:

pushd $(mktemp -d)

Now I want to exit and remove the temporary directory in one go:

rmdir $(popd)

However that doesn't work because popd doesn't return the popped directory (it returns the new, now current, directory) and also because it's performed in a subshell.

Something like

dirs -l -1 ; popd &> /dev/null

will return the popped directory but it can't be used like this:

rmdir $(dirs -l -1 ; popd &> /dev/null)

because the popd will only affect the subshell. What is called for is the ability to do this:

rmdir { dirs -l -1 ; popd &> /dev/null; }

but that's invalid syntax. Is it possible to achieve this effect ?

(note: I know I can save the temporary directory in a variable; I was trying to avoid the need to do so and learn something new in the process!)

Best Answer

The choice of the title of your question is a bit confusing.

pushd/popd, a csh feature copied by bash and zsh, are a way to manage a stack of remembered directories.

pushd /some/dir

pushes the current working directory onto a stack, and then changes the current working directory (and then prints /some/dir followed by the content of that stack (space-separated).

popd

prints the content of the stack (again, space separated) and then changes to the top element of the stack and pops it from the stack.

(also beware that some directories will be represented there with their ~/x or ~user/x notation).

So if the stack currently has /a and /b, the current directory is /here and you're running:

 pushd /tmp/whatever
 popd

pushd will print /tmp/whatever /here /a /b and popd will output /here /a /b, not /tmp/whatever. That's independent of using command substitution or not. popd cannot be used to get the path of the previous directory, and in general its output cannot be post processed (see the $dirstack or $DIRSTACK array of some shells though for accessing the elements of that directory stack)

Maybe you want:

pushd "$(mktemp -d)" &&
popd &&
rmdir "$OLDPWD"

Or

cd "$(mktemp -d)" &&
cd - &&
rmdir "$OLDPWD"

Though, I'd use:

tmpdir=$(mktemp -d) || exit
(
  cd "$tmpdir" || exit # in a subshell 
  # do what you have to do in that tmpdir
)
rmdir "$tmpdir"

In any case, pushd "$(mktemp -d)" doesn't run pushd in a subshell. If it did, it couldn't change the working directory. That's mktemp that runs in a subshell. Since it is a separate command, it has to run in a separate process. It writes its output on a pipe, and the shell process reads it at the other end of the pipe.

ksh93 can avoid the separate process when the command is builtin, but even there, it's still a subshell (a different working environment) which this time is emulated rather than relying on the separate environment normally provided by forking. For example, in ksh93, a=0; echo "$(a=1; echo test)"; echo "$a", no fork is involved, but still echo "$a" outputs 0.

Here, if you want to store the output of mktemp in a variable, at the same time as you pass it to pushd, with zsh, you could do:

pushd ${tmpdir::="$(mktemp -d)"}

With other Bourne-like shells:

unset tmpdir
pushd "${tmpdir=$(mktemp -d)}"

Or to use the output of $(mktemp -d) several times without explicitly storing it in a variable, you could use zsh anonymous functions:

(){pushd ${1?} && cd - && rmdir $1} "$(mktemp -d)"