Set permissions with rsync for all files but the root directory

Solution 1:

Another solution would be to use rsync -av --delete a/* b/, but this would prevent deleted files in a/ from being removed in b/.

And then you run

rsync -rv --delete --existing --ignore-existing a/ b/

to deal with this. You don't use -a here so the ownership of the target directory is not a problem.

From man 1 rsync:

--existing, --ignore-non-existing
This tells rsync to skip creating files (including directories) that do not exist yet on the destination. If this option is combined with the --ignore-existing option, no files will be updated (which can be useful if all you want to do is delete extraneous files).

(Credits to this answer.)


Improvements:

  • In general * is not enough to match all files and directories. You also need .[!.]* if you have dot files, and ..?* if you have file names beginning with two dots. In Bash dotglob helps, so additional patterns are not needed:

    # in Bash
    shopt -s dotglob
    rsync -av --delete a/* b/
    

    But you need to make sure that the pattern expands to something. If it doesn't, rsync will get its literal form and complain. Creating a dummy (temporary) file in a/ may be inelegant, but it will certainly make the pattern expand. If you remove the file before you run the second rsync then it will be removed from the destination as well.

  • If you ever decide to work from within a/ directory, the pattern will become *. In this case you should use double dash (--) or modify the pattern (./*) so file names like -n are not treated as options.

An example solution may be like this:

#!/bin/bash

shopt -s dotglob

tmpf="$(mktemp -p a/)" || exit 202
rsync -av --delete a/* b/
rm "$tmpf"
rsync -rv --delete --existing --ignore-existing a/ b/

Notes:

  • Each rsync generates its own exit status, you need additional logic to tell if everything was OK.
  • If there are many objects in a/ then the first invocation of rsync may yield argument list too long. Possible solutions:

    • find + xargs.
    • Processing objects one by one in a loop:

      for f in a/*; do
         rsync -av --delete -- "$f" b/
      done
      

      (in this case use shopt -s nullglob beforehand and you won't even need a dummy file).

      Note I used -- even though it's not necessary in this particular case (because expanded $f must start with a/). But let's say you modify the code and change a/* to *. Then you need to add -- in another line, it's very easy to miss this.