Diff two directories recursively
See which files exist only in one directory tree, which differ, and which are identical — for verifying a backup, comparing two checkouts, or spotting accidental changes.
How to diff two directories recursively in each shell
diff -r dir1 dir2`-r` recurses into subdirectories. `-q` (quiet) lists only WHICH files differ, not the content of the diff — much faster for large trees. `-x '*.log'` excludes a glob; `--exclude-from=patterns.txt` reads exclusions from a file. For a side-by-side view: `diff -ry dir1 dir2 | less`. Add `--brief` to skip the per-byte content comparison and just say "Files X and Y differ".
diff -r dir1 dir2Same external. For a more visual diff across trees, install `meld` (GUI) or `vimdiff -r` / `nvim -d` (terminal-based). On macOS, `opendiff dir1 dir2` (ships with Xcode Command Line Tools) launches the FileMerge GUI.
diff -r dir1 dir2Same external. Fish-flavored quality-of-life: capture into a variable for further processing: `set differences (diff -rq dir1 dir2)`. For a "files only in dir1" filter: `diff -rq dir1 dir2 | grep "^Only in dir1"`.
Compare-Object (Get-ChildItem -Recurse dir1) (Get-ChildItem -Recurse dir2) -Property Name, LengthCompares the file LISTING (name + size), NOT contents. The `SideIndicator` column is `<=` (only in dir1), `=>` (only in dir2), `==` (in both, suppressed by default unless `-IncludeEqual`). For BYTE-level comparison: `Compare-Object (Get-FileHash dir1\* -Algorithm MD5).Hash (Get-FileHash dir2\* -Algorithm MD5).Hash`. Robocopy is the heavier tool: `robocopy dir1 dir2 /L /MIR /NJH /NJS` (the `/L` flag means "list only, do not actually copy").
robocopy dir1 dir2 /L /MIR`/L` is "list mode" — robocopy prints what it WOULD do without doing it. `/MIR` means "mirror" (so the report covers files-to-add, files-to-update, files-to-delete in dir2 to make it match dir1). For byte-level: `fc /B file1 file2` per file pair (cmd `fc` doesn't recurse). `comp dir1 dir2 /M /S` recurses but is positively ancient (DOS-era) — robocopy is the modern answer.
Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.
Gotchas & notes
- Three different "diff" granularities exist: NAMES (which files exist in each), METADATA (size + mtime), BYTES (actual content). Each tool defaults differently. `diff -rq` is byte-level. `Compare-Object` with `Get-ChildItem` is metadata-level (name + length unless you specify more). `robocopy /L` is metadata-level by default; add `/IT` to flag tweaked attrib-only-changes. Pick the level you actually need — byte-level is O(GB) work; name-only is O(file count) work.
- Symbolic links are a portability trap. `diff -r` on Linux follows symlinks by default (compares the targets); add `--no-dereference` to compare the link itself. `Compare-Object` with `Get-ChildItem` does NOT follow symlinks on Windows by default; add `-Follow` (pwsh 7.1+) to follow them. Decide upfront: are the symlinks part of the directory structure (compare as links) or just pointers to real data (compare targets)?
- For huge trees (millions of files), `diff -r` reads every byte and is slow. Better: hash each file once, compare hashes. `find dir1 -type f -exec sha256sum {} \; | sort > /tmp/dir1.hashes`, do the same for dir2, then `diff /tmp/dir1.hashes /tmp/dir2.hashes` shows differing files in O(file count) network/disk I/O instead of O(total bytes × 2).
- When comparing source trees with line-ending differences (CRLF vs LF — common after Windows / WSL round-trips), `diff -r --strip-trailing-cr` ignores the CRs. pwsh `Compare-Object` on file CONTENTS (after `Get-Content`) is line-by-line and will show every line as different across CRLF/LF — normalize first with `(Get-Content $f -Raw).Replace("`r`n", "`n")`.
Related commands
Related tasks
- Find duplicate files by content— Identify files with identical contents across a directory tree (regardless of name) — for cleanup, deduplication, or media library audits.
- Find and replace text in files— Substitute one string for another inside a file (or every file in a tree), in place.
- List files recursively— Print every file under a directory tree, optionally as a flat list of full paths.