Why doesn't xargs on find work with a series of commands using pushd and popd to walk a directory subtree?

1

On a Linux Centos 4 machine, I am trying to create a simple bash command line to walk a directory structure below an arbitrary current directory and in each subdirectory touch a file, list the directory contents but pipe them to /dev/null, and remove the touched file. The obscure point of this script is to tickle the underlying NFS client/server system to ensure the contents of each directory are reflecting a change made on a different machine which otherwise may take some time to propogate. I have found this workaround avoids the delay. Ignoring the merits of my reason for doing this, why doesn't my proposed bash script work?

[CentosMachine] find . -type d -print0 | xargs -0 -I {} pushd {}; touch xYzZy.fixZ; ls &> /dev/null; rm -f xYzZy.fixZ; popd
xargs: pushd: No such file or directory
bash: popd: directory stack empty

The find command is presently returning:

.
./dir
./emptyDir
./dirOfDir
./dirOfDir/ofDir
./dirOfDir/ofDir/Dir(empty)

At first I thought perhaps the ( and ) in one of the directory names might be the issue, but renaming that directory to be ./dirOfDir/ofDir/Dir_empty_ did not change the symptom. I also tried looking at strace output but did not see anything that helped, but did see the directories being processed.

Here is a snippet of the end of the strace output with that directory renamed to use underscores instead of parentheses:

[...]
chdir("ofDir")                          = 0
lstat64(".", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
lstat64("Dir_empty_", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
open("Dir_empty_", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY) = 4
fstat64(4, {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
fcntl64(4, F_SETFD, FD_CLOEXEC)         = 0
getdents64(4, /* 2 entries */, 32768)   = 48
getdents64(4, /* 0 entries */, 32768)   = 0
close(4)                                = 0
chdir("Dir_empty_")                     = 0
lstat64(".", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
chdir("..")                             = 0
lstat64(".", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
chdir("..")                             = 0
lstat64(".", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
chdir("..")                             = 0
lstat64(".", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
fchdir(3)                               = 0
write(1, ".\0./dir\0./emptyDir\0./dirOfDir\0./"..., 75) = 75
exit_group(0)                      = ?

WilliamKF

Posted 2014-05-15T23:39:41.833

Reputation: 6 916

Answers

1

xargs is a very useful tool for

  1. executing commands with arguments taken from a dynamic source, and
  2. minimizing the number of command invocations by building long command lines, with multiple arguments.

When you’re not doing #2 (i.e., executing one command per argument, as you are doing), xargs isn’t quite so valuable; there are other ways to get the commands executed.  In particular, if the source of the arguments is find, you can use the -exec option:

find . -type d -exec bash -c 'pushd "{}" &> /dev/null; touch xYzZy.fixZ; ls &> /dev/null; rm -f xYzZy.fixZ; popd &> /dev/null'

But this answer, like yours, invokes a shell process for each directory.  And each process has its own execution environment; changing the working directory in a sub-process has no effect on the parent process.  So you don’t need the pushd and popd.  And you don’t need to specify bash; plain old sh will do.  And, if you aren’t setting modification times, you don’t need touch; a redirected null command will create a file.  So we can reduce the above to:

find . -type d -exec sh -c 'cd "{}"; > xYzZy.fixZ; ls &> /dev/null; rm -f xYzZy.fixZ'

For your use case, this might not matter, but in other situations you might want to perform the last three steps only if the cd succeeds:

find . -type d -exec sh -c 'cd "{}" && { > xYzZy.fixZ; ls &> /dev/null; rm -f xYzZy.fixZ;}'

Note that you need to have whitespace after the { and a semicolon before the }.

Scott

Posted 2014-05-15T23:39:41.833

Reputation: 17 653

1

I found my answer with this stack overflow question. Put the multiple commands into a form like this:

bash -c 'command1; command2; ...'

Which applied here gives:

find . -type d -print0 | xargs -0 -I {} bash -c 'pushd "{}"; touch xYzZy.fixZ; ls &> /dev/null; rm -f xYzZy.fixZ; popd'

Note the addition of double quotes around the pushd "{}" so that the directory with ( and ) works properly. Without that you get an error:

bash: -c: line 0: syntax error near unexpected token `('
bash: -c: line 0: `pushd ./dirOfDir/ofDir/Dir(empty) &> /dev/null; touch xYzZy.fixZ; ls &> /dev/null; rm -f xYzZy.fixZ; popd &> /dev/null'

However, the pushed and popd also need suppression to avoid output:

find . -type d -print0 | xargs -0 -I {} bash -c 'pushd "{}" &> /dev/null; touch xYzZy.fixZ; ls &> /dev/null; rm -f xYzZy.fixZ; popd &> /dev/null'

WilliamKF

Posted 2014-05-15T23:39:41.833

Reputation: 6 916

You answered your question without answering your question. The short answer is that xargs can’t take a compound command; you need to wrap the compound command in a string and pass it to the shell. – Scott – 2014-07-18T22:18:42.097