bash - Why doesn't xargs on find work with a series of commands using pushd and popd to walk a directory subtree?
2014-07
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) = ?
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'
I was trying to find all files of a certain type spread out in subdirectories, and for my purposes I only needed the filename. I tried stripping out the path component via basename
, but it did't work with xargs
:
$ find . -name '*.deb' -print | xargs basename
basename: extra operand `./pool/main/a/aalib/libaa1_1.4p5-37+b1_i386.deb'
Try `basename --help' for more information.
I get the same thing (exactly the same error) with either of these variations:
$ find . -name '*.deb' -print0 | xargs -0 basename
$ find . -name '*.deb' -print | xargs basename {}
This, on the other hand, works as expected:
$ find . -name '*.deb' -exec basename {} \;
foo
bar
baz
This happens on up-to-date Cygwin and Debian 5.0.3. My diagnosis is that xargs is for some reason passing two input lines to basename, but why? What's going on here?
because basename
wants just one parameter... not LOTS of. and xargs
creates lots parameters.
to solve your real problem (only list the filenames):
find . -name '*.deb' -printf "%f\n"
which prints just the 'basename' (man find):
%f File's name with any leading directories
removed (only the last element).
Try this:
find . -name '*.deb' | xargs -n1 basename
basename only accepts a single argument. Using -exec
works properly because each {}
is replaced by the current filename being processed, and the command is run once per matched file, instead of trying to send all of the arguments to basename in one go.