2

I am in the directory foo which contains the subdirectories bar1 bar2 and bar3 and nothing else.

I would like to act pushd on foo, bar1, bar2 and bar3 in one command, however I am encountering difficulties:

find `pwd` | xargs pushd

returns

xargs: pushd: No such file or directory

I tried to circumvent the problem with a while loop:

find `pwd` | while read line ; do pushd $line ; done

Which gives an output which looks correct:

~/foo ~/foo
~/foo/bar1 ~/foo ~/foo
~/foo/bar2 ~/foo/bar1 ~/foo ~/foo
~/foo/bar3 ~/foo/bar2 ~/foo/bar1 ~/foo ~/foo

However using dirs after that shows that I have not added any new directories to the stack:

~/foo

Can anyone spot what I am doing wrong?

Miguel
  • 23

2 Answers2

2

You’re very close.  You can do what you want with

while read line; do pushd "$line"; done < <(find "$(pwd)" -type d)

The problem with your command is that pushd, like cd, must be done in the main (parent) process to be useful, and (with some variation based on how your system is set up), the commands in a pipeline run in subprocesses (child processes).  < <(cmd) magically gives you a pipe without giving you a pipeline.

This requires that you are running bash (or maybe one of the other advanced shells?), since POSIX doesn’t support <(cmd).

The xargs approach, sadly, was doomed to failure, because pushd (like cd) is a shell builtin command (i.e., there is no program called pushd), and xargs requires an external, executable program.  You can get output that (almost) looks right with this command:

$ find "$(pwd)" -type d | xargs -i sh -c 'pushd "$1"' sh
~/foo ~/foo
~/foo/bar1 ~/foo
~/foo/bar2 ~/foo
~/foo/bar3 ~/foo

but that is executing the shell as an external program, and doing so (independently) for each directory.  This is a little closer:

$ find "$(pwd)" -type d | xargs sh -c 'for line do pushd "$line"; done' sh
~/foo ~/foo
~/foo/bar1 ~/foo ~/foo
~/foo/bar2 ~/foo/bar1 ~/foo ~/foo
~/foo/bar3 ~/foo/bar2 ~/foo/bar1 ~/foo ~/foo

executing a single shell process, and telling it to loop over all the directories.  As you can see, this technique is similar to your second attempt (and my answer), and the result is the same as your second attempt — you get a new shell process that calls pushd four times, and ends up with a directory stack that is five levels deep (counting the starting directory twice) — but that new shell process is in a subprocess, and, by the time you see the next shell prompt, that shell process is gone.

For reference, note that this command is somewhat similar to an answer I gave a couple of years agoStéphane Chazelas discusses the sh -c long_complex_shell_command sh command construct here and here.

1

This is not a job for find.

for subdirectory in */; do
  pushd -- "$subdirectory"
done

Your code doesn't work because pushd, like cd, needs to execute in the shell environment that you want to affect. There is no external command pushd that xargs could call because an external command would be pointless: it would not affect anything. The right-hand side of a pipe runs in a subshell (in your shell, there are shells where it runs in your other shell and your second attempt would work); you can see pushd working with

find `pwd` | {
  while read line ; do pushd $line ; done;
  dirs
}

Working unless the directory names contain special characters, that is. Always put double quotes around variable substitutions unless you know why you need to leave them off.

  • It would be neat if DIRSTACK+=( */ ) worked, but you can only modify existing entries in DIRSTACK[], not add (or remove) :-/ – mr.spuratic Jun 27 '17 at 10:33