0

With a directory structure built using

#!/bin/bash
for name in A B
do
    mkdir -p /tmp/src/${name}/copyme
    echo "do not copy" > /tmp/src/${name}/no.txt
    echo "copy" > /tmp/src/${name}/copyme/yes.txt
done

I would like to mirror just the copyme directories, along with the files within them, to /tmp/tgt.

This should be simple. Relying on rsync distinguishing the order of the command-line options: exclude everything, then include the relevant pattern. Yet

rsync -av --exclude='*' --include='copyme' /tmp/src/ /tmp/tgt/

excludes everything (only the target directory is created). Why?

2
  • 1
    So, what does "doesn't work" mean here? What exactly happens?
    – ilkkachu
    Jun 12, 2018 at 10:30
  • You'd probably have to --include='copyme/*' in addtion to the existing --include.
    – muru
    Jun 12, 2018 at 10:56

1 Answer 1

0

When rsync runs, it tests the found names at the source against the patterns, and the first matching pattern takes effect:

$ rsync -avv --exclude='*' --include='copyme' /tmp/src/ /tmp/tgt/
sending incremental file list
[sender] hiding directory A because of pattern *
[sender] hiding directory B because of pattern *
delta-transmission disabled for local transfer or --whole-file
total: matches=0  hash_hits=0  false_alarms=0 data=0

sent 51 bytes  received 86 bytes  274.00 bytes/sec
total size is 0  speedup is 0.00

When a directory has been excluded (see "hiding directory..." above), its content is not further considered. Reversing the exclusion and inclusion patterns will not help since it will never get to the copyme directories.

The rsync manual says:

For instance, to include /foo/bar/baz, the directories /foo and /foo/bar must not be excluded. Excluding one of those parent directories prevents the examination of its content, cutting off rsync's recursion into those paths and rendering the include for /foo/bar/baz ineffectual (since rsync can't match something it never sees in the cut-off section of the directory hierarchy).

So, instead:

$ rsync -avv --include='[AB]' --include='copyme/***' --exclude='*' /tmp/src/ /tmp/tgt/
sending incremental file list
[sender] showing directory A because of pattern [AB]
[sender] showing directory B because of pattern [AB]
[sender] showing directory A/copyme because of pattern copyme/***
[sender] hiding file A/no.txt because of pattern *
[sender] showing file A/copyme/yes.txt because of pattern copyme/***
[sender] showing directory B/copyme because of pattern copyme/***
[sender] hiding file B/no.txt because of pattern *
[sender] showing file B/copyme/yes.txt because of pattern copyme/***
created directory /tmp/tgt
delta-transmission disabled for local transfer or --whole-file
./
A/
A/copyme/
A/copyme/yes.txt
B/
B/copyme/
B/copyme/yes.txt
total: matches=0  hash_hits=0  false_alarms=0 data=10

sent 305 bytes  received 175 bytes  960.00 bytes/sec
total size is 10  speedup is 0.02

Note that the exclusion has to come last, after the inclusions. The copyme/*** pattern matches both the copyme directory name itself and any pathname underneath it.

If you don't want to hardcode the A and B directory names:

for dir in /tmp/src/*; do
    [ ! -d "$dir" ] && continue
    rsync -avv --include="${dir##*/}" --include='copyme/***' --exclude='*' /tmp/src/ /tmp/tgt/
done

This would output

sending incremental file list
[sender] showing directory A because of pattern A
[sender] hiding directory B because of pattern *
[sender] showing directory A/copyme because of pattern copyme/***
[sender] hiding file A/no.txt because of pattern *
[sender] showing file A/copyme/yes.txt because of pattern copyme/***
created directory /tmp/tgt
delta-transmission disabled for local transfer or --whole-file
./
A/
A/copyme/
A/copyme/yes.txt
total: matches=0  hash_hits=0  false_alarms=0 data=5

sent 180 bytes  received 148 bytes  656.00 bytes/sec
total size is 5  speedup is 0.02
sending incremental file list
[sender] hiding directory A because of pattern *
[sender] showing directory B because of pattern B
[sender] showing directory B/copyme because of pattern copyme/***
[sender] hiding file B/no.txt because of pattern *
[sender] showing file B/copyme/yes.txt because of pattern copyme/***
delta-transmission disabled for local transfer or --whole-file
B/
B/copyme/
B/copyme/yes.txt
total: matches=0  hash_hits=0  false_alarms=0 data=5

sent 180 bytes  received 117 bytes  594.00 bytes/sec
total size is 5  speedup is 0.02

The result would be

$ tree src tgt
src
|-- A
|   |-- copyme
|   |   `-- yes.txt
|   `-- no.txt
`-- B
    |-- copyme
    |   `-- yes.txt
    `-- no.txt

4 directories, 4 files
tgt
|-- A
|   `-- copyme
|       `-- yes.txt
`-- B
    `-- copyme
        `-- yes.txt

4 directories, 2 files

Another approach which does not use exclusion or inclusion patterns with rsync but that uses find to locate the copyme directories and then rsync to copy them:

find /tmp/src -type d -name 'copyme' -prune -exec sh -c '
    cd /tmp/src && rsync -aRvv "${1#/tmp/src/}/" /tmp/tgt/' sh {} ';'

Notice the -R (--relative) flag used with rsync here.

The sh -c script executed for each found copyme directory does a cd to /tmp/src and then copies the pathname with the initial /tmp/src bit of its path removed.

The -prune in the find command stops find from looking for further copyme directories inside the found directory.

3
  • Very nice. But A and B have no special significance. Consider the case of copying all __pycache__ subdirectories and the files that are within, yet exclude everything else. How can I then avoid the explicit dependence on naming the A and B subdirectories (in [AB]) and depend instead on an equivalent of all?
    – Calaf
    Jun 12, 2018 at 12:38
  • @Calaf I believe I showed this at the very end of the answer (using find), but also with the preceding shell loop. The shell loop will only work if the copyme directories are located immediately below /tmp/src/* though, as in your example. Jun 12, 2018 at 12:45
  • The for loop (for dir in /tmp/src/*; do) comes closest. Yet for dir in /path/* leaves me with a lingering feeling that I must be able to put /path/* in a directory, then operate on that directory and get rid of the loop. (My own loop has no special significance, other than populating the files/directories briefly.)
    – Calaf
    Jun 15, 2018 at 6:25

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .