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

07
2014-07
  • WilliamKF

    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)                      = ?
    
  • Answers
  • WilliamKF

    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'
    

  • Related Question

    command line - find: -exec vs xargs (aka Why does "find | xargs basename" break?)
  • quack quixote

    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?


  • Related Answers
  • akira

    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).
    
  • perlguy9

    Try this:

    find . -name '*.deb' | xargs -n1 basename
    
  • John T

    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.