rsync --exclude: skip files and directories when syncing
- tool
- rsync
- versions
- rsync 3.x
- os
- linux, macos
rsync -a --exclude='node_modules' --exclude='*.log' src/ dest/ copies src to dest but skips every node_modules directory and every .log file. A pattern without a slash matches a file or directory name at any depth; a pattern starting with / is anchored at the root of the transfer (--exclude='/build' skips only the top-level build); a trailing / matches directories only. Quote patterns so the shell does not expand *. Long lists go in a file: --exclude-from=.rsync-exclude.
Commands
# Skip dependency folders, logs and the Git directory
rsync -av --exclude='node_modules' --exclude='*.log' --exclude='.git' ./app/ /backup/app/
# Patterns from a file, one per line; lines starting with # are comments
rsync -av --exclude-from=.rsync-exclude ./app/ user@server.example.com:/srv/app/
# Only the top-level build directory, not app/src/build
rsync -av --exclude='/build/' ./app/ /backup/app/
# Copy only .jpg files, keeping the directory tree
rsync -av -m --include='*/' --include='*.jpg' --exclude='*' ./photos/ /backup/photos/An example .rsync-exclude file:
# dependencies and build output
node_modules/
dist/
*.log
.envPattern rules
| Pattern | Matches |
|---|---|
| node_modules | Any file or directory named node_modules, at any depth |
| node_modules/ | Only directories with that name |
| *.log | Any name ending in .log; * does not cross / |
| /build | build at the root of the transfer only |
| src/*.tmp | .tmp files directly in a directory named src |
| logs/** | Everything below logs, at any depth (** crosses /) |
| cache/*** | The cache directory itself and everything in it |
| --exclude-from=FILE | Read patterns from FILE, one per line |
| -C, --cvs-exclude | A built-in list of version control and editor files (CVS, .git, *.o, *~ and more) |
| -F | Read rules from .rsync-filter files in each directory |
Where "/" anchors
An anchored pattern starts at the root of the transfer, which depends on the source path and its trailing slash, not at the root of the filesystem:
| Command | Pattern that skips app/build |
|---|---|
| rsync -a /home/me/app/ /dest | --exclude=/build |
| rsync -a /home/me/app /dest | --exclude=/app/build |
| rsync -a --relative /home/me/app/ /dest | --exclude=/home/me/app/build |
This is why an exclude that works in one command can stop working after a trailing slash is added or removed. See the page on the rsync trailing slash.
Pitfalls
- Order matters with includes. Rules are checked in order and the first match wins:
--includemust come before the--exclude='*'it overrides. - An excluded directory hides its contents.
--exclude='*'without--include='*/'stops rsync from descending into subdirectories, so nested files you included never match. - Unquoted wildcards.
--exclude=*.logmay be expanded by the shell when a matching file exists in the current directory. Quote it. - Paths written as absolute filesystem paths.
--exclude=/home/me/app/node_modulesdoes not match unless--relativeis used; write the path relative to the transfer root.
Check before syncing
Run the command with -n (dry run) and -i (itemize changes) first, and read the list: an excluded file must not appear in it. The page on rsync --dry-run explains the output. rsync runs on Linux and macOS (macOS ships an older openrsync or rsync 2.6.9 depending on the version; install rsync 3 for *** and other newer features); on Windows use it inside WSL or Cygwin.
FAQ
How do I exclude several directories at once?
Repeat the option, --exclude=node_modules --exclude=.git --exclude=dist, or put one pattern per line in a file and pass --exclude-from=FILE. Bash brace expansion, --exclude={node_modules,.git}, also works but only in shells that support it.
Why is my excluded file still deleted on the destination with --delete?
It is not: excluded files are protected from deletion by default. They are deleted only with --delete-excluded. If a file disappears, check the pattern with --dry-run and -i to see what rsync matches.
How do I copy only some file types?
Include directories and the wanted files, then exclude the rest, in this order: --include='*/' --include='*.jpg' --exclude='*'. Add -m (--prune-empty-dirs) to skip directories that end up empty.