A fast and efficient command-line tool for comparing and synchronizing directories.
- Recursive directory comparison - Scan subdirectories automatically
- Multiple output formats - Text, JSON, or summary output
- Fast concurrent processing - Uses Swift's modern concurrency for optimal performance
- Smart file comparison - Quickly identifies identical, modified, and unique files
- Fuzzy filename matching - Match files with similar names using configurable precision threshold
- Fuzzy size tolerance - Treat fuzzy-matched files with similar sizes as renamed rather than modified
- Clear exit codes - Easy integration with scripts and CI/CD pipelines
- Pattern-based file filtering - Use .filesignore to exclude files from operations
- Configuration files - Use .files to set default options per directory
- One-way sync - Mirror source directory to destination
- Two-way sync - Bidirectional synchronization with conflict resolution
- Conflict resolution strategies - Keep newest, source, destination, or skip
- Dry-run mode - Preview changes before applying them
- Safe operations - Creates intermediate directories and validates paths
The latest macOS build can be downloaded from the GitHub Releases page.
See the Building section below for instructions on building from source.
Files supports .filesignore files to exclude certain files and directories from comparison and sync operations. This works similarly to .gitignore.
The tool automatically looks for .filesignore files in three locations (in order):
- User home directory:
~/.filesignore- Global patterns for all operations - Source/left directory:
<source-dir>/.filesignore - Destination/right directory:
<dest-dir>/.filesignore
Patterns from all found files are merged together.
*- Matches any characters except/?- Matches a single character except/**- Matches zero or more directories/at start - Pattern is relative to directory root/at end - Pattern matches directories only!- Negates a pattern (includes files that were previously excluded)#- Comment line- Empty lines are ignored
By default, .filesignore and .files are always ignored and will not be copied during sync operations. This prevents configuration files from being propagated to destination directories.
To explicitly include them in sync operations, add negation patterns:
!.filesignore
!.files
# Ignore build artifacts
*.o
*.class
build/
# Ignore dependencies
node_modules/
vendor/
# Ignore version control
.git/
.svn/
# Ignore OS files
.DS_Store
Thumbs.db
# Ignore all log files
*.log
# But keep important.log
!important.log
# Ignore config.json only at root
/config.json
# Ignore all .txt files in build directory and subdirectories
build/**/*.txt
You can disable .filesignore pattern matching with the --no-ignore flag:
files compare dir1 dir2 --no-ignore
files sync source dest --no-ignoreInstead of passing options via CLI flags every time, you can create a .files configuration file in either the left or right directory. This is especially useful for directories that always need the same settings.
The tool looks for .files in two locations:
- Source/left directory:
<source-dir>/.files - Destination/right directory:
<dest-dir>/.files
Right directory values override left directory values. CLI flags override all .files settings.
Simple key = value format, one per line. Comments start with #:
# Fuzzy matching settings
matchPrecision = 0.8
sizeTolerance = 0.2
# Sync behavior
recursive = true
deletions = false
| Key | Type | Description |
|---|---|---|
matchPrecision |
0.0β1.0 |
Fuzzy filename matching threshold |
sizeTolerance |
0.0β1.0 |
File size difference tolerance for fuzzy matches |
recursive |
true/false |
Scan subdirectories recursively |
deletions |
true/false |
Delete files not in source (one-way sync) |
showMoreRight |
true/false |
Show additional right-side diff info |
dryRun |
true/false |
Preview changes without applying |
verbose |
true/false |
Show detailed output |
format |
text/json/summary |
Output format |
twoWay |
true/false |
Enable two-way sync |
conflictResolution |
newest/left/right/skip |
Conflict resolution strategy |
noIgnore |
true/false |
Disable .filesignore loading |
Boolean values accept: true/false, yes/no, 1/0.
See .files.example for a complete example with documentation.
You can disable .files configuration loading with the --no-config flag:
files compare dir1 dir2 --no-config
files sync source dest --no-configLike .filesignore, the .files configuration file is automatically excluded from comparison and sync operations. It will not be copied to destination directories.
Files has three main commands: compare (default), sync, and cp.
Compare two directories and report differences:
files compare <left-directory> <right-directory> [options]
# or simply (compare is the default command):
files <left-directory> <right-directory> [options]--recursive/--no-recursive- Scan subdirectories recursively (default: recursive)--match-precision THRESHOLD- Fuzzy filename matching threshold from 0.0 to 1.0 (default: 1.0 for exact matching). Lower values enable matching files with similar names using Levenshtein distance--size-tolerance TOLERANCE- File size difference tolerance for fuzzy matches from 0.0 to 1.0 (default: 0.0 for exact comparison)--verbose,-v- Show detailed output with all file paths--format FORMAT- Output format:text(default),json,summary--no-ignore- Disable .filesignore pattern matching--no-config- Disable .files configuration loading--help,-h- Show help message--version- Show version information
0- Directories are identical1- Differences found2- Error occurred (invalid directory, access denied, etc.)
Copy new and modified files from source to destination (without deletions):
files cp <source-directory> <destination-directory> [options]This is a convenience command equivalent to files sync --no-deletions with one-way mode. It's useful for updating a destination directory with new and changed files from source while preserving any extra files in the destination.
--show-more-right- Scan leaf directories on the right side for additional diff information--match-precision THRESHOLD- Fuzzy filename matching threshold from 0.0 to 1.0 (default: 1.0 for exact matching)--size-tolerance TOLERANCE- File size difference tolerance for fuzzy matches from 0.0 to 1.0 (default: 0.0 for exact comparison)--dry-run- Preview changes without applying them--verbose,-v- Show detailed output with all operations--format FORMAT- Output format:text(default),json,summary--no-ignore- Disable .filesignore pattern matching--no-config- Disable .files configuration loading
# Preview what would be copied
files cp /source /backup --dry-run --verbose
# Copy new and modified files
files cp /source /backup --verboseSynchronize two directories:
files sync <source-directory> <destination-directory> [options]--two-way- Enable bidirectional sync (default: one-way)--conflict-resolution STRATEGY- For two-way sync:newest(default),source,destination,skip--recursive/--no-recursive- Scan subdirectories recursively (default: recursive)--deletions- Delete files in destination that don't exist in source (one-way sync only, default: false)--show-more-right- Scan leaf directories on the right side for additional diff information (one-way sync without deletions only)--match-precision THRESHOLD- Fuzzy filename matching threshold from 0.0 to 1.0 (default: 1.0 for exact matching). Lower values enable matching files with similar names using Levenshtein distance--size-tolerance TOLERANCE- File size difference tolerance for fuzzy matches from 0.0 to 1.0 (default: 0.0 for exact comparison)--dry-run- Preview changes without applying them--verbose,-v- Show detailed output with all operations--format FORMAT- Output format:text(default),json,summary--no-ignore- Disable .filesignore pattern matching--no-config- Disable .files configuration loading
One-way sync (default): Copies and updates files from source to destination
- Copies files that exist only in source
- Updates files that differ between source and destination
- By default, does NOT delete files that exist only in destination
- Use
--deletionsflag to delete extra files in destination (mirrors source exactly)
Two-way sync (--two-way): Bidirectional synchronization
- Copies files that exist only in either directory to the other
- Never deletes files (syncs in both directions)
- Resolves conflicts for modified files based on
--conflict-resolutionstrategy
newest- Keep the file with the most recent modification time (default)source- Always prefer the source filedestination- Always prefer the destination fileskip- Skip conflicting files, leave both unchanged
The --match-precision option enables fuzzy matching of filenames using Levenshtein distance algorithm. This is useful when comparing directories with files that may have typos, slight variations, or systematic naming differences.
-
Threshold value: A number from 0.0 to 1.0
1.0(default): Only exact filename matches0.8: Allows ~20% character differences (recommended for typo detection)0.5: Allows ~50% character differences (very permissive)0.0: Matches any files (not recommended)
-
Matching is based on filename only, not the full path
-
Exact matches are always preferred over fuzzy matches
-
One-to-one mapping: Each right file can only match one left file
When fuzzy matching is enabled, matched files with different names will almost always have different content. The --size-tolerance option controls how to handle these pairs:
0.0(default): Exact content comparison β files are compared byte-by-byte0.2: Files with sizes within 20% of each other are treated as the same file (renamed)0.5: Files with sizes within 50% are treated as the same file
Formula: files match if abs(size1 - size2) <= min(size1, size2) * tolerance
This is useful for detecting renamed files without treating them as modified.
- Typo detection: Find files with misspelled names (e.g., "report.txt" vs "reprot.txt")
- Version variations: Match files with version numbers (e.g., "file_v1.txt" vs "file_v2.txt")
- Renamed files: Identify files that were slightly renamed between directories
- Import cleanup: Find near-duplicate files from different sources
# Compare with fuzzy matching (80% similarity threshold)
files /backup /current --match-precision 0.8 --verboseOutput shows fuzzy-matched files as "modified":
Modified (2):
~ report.txt (matched with: reprot.txt)
~ document.txt (matched with: documnet.txt)
# Sync files even if names have minor differences
files sync /source /dest --match-precision 0.8 --verboseThis will:
- Match "report.txt" (source) with "reprot.txt" (destination)
- Update the file with correct content
- Create new "report.txt" in destination
# Match similar filenames and treat similar-sized files as renamed
files compare /dir1 /dir2 --match-precision 0.8 --size-tolerance 0.2This matches files with 80% filename similarity. Fuzzy-matched pairs with sizes within 20% of each other are treated as the same file (renamed), not as modified.
# Use higher threshold (90%) for stricter matching
files /dir1 /dir2 --match-precision 0.9Only files with very similar names will match (e.g., "file1.txt" and "file2.txt" won't match, but "report.txt" and "reprot.txt" will).
files /path/to/dir1 /path/to/dir2Output:
β Directories differ
Only in LEFT (3):
Use --verbose to see file list
Only in RIGHT (2):
Use --verbose to see file list
Modified (1):
Use --verbose to see file list
Summary: 3 left-only, 2 right-only, 1 modified, 10 unchanged
files /path/to/dir1 /path/to/dir2 --verboseOutput:
β Directories differ
Only in LEFT (3):
- old-file.txt
- deprecated/config.json
- temp/data.csv
Only in RIGHT (2):
+ new-feature.swift
+ assets/logo.png
Modified (1):
~ config.yaml
Summary: 3 left-only, 2 right-only, 1 modified, 10 unchanged
files /path/to/dir1 /path/to/dir2 --format jsonOutput:
{
"onlyInLeft": [
"old-file.txt",
"deprecated/config.json"
],
"onlyInRight": [
"new-feature.swift"
],
"modified": [
"config.yaml"
],
"common": [
"README.md",
"main.swift"
],
"summary": {
"onlyInLeftCount": 2,
"onlyInRightCount": 1,
"modifiedCount": 1,
"commonCount": 2,
"identical": false
}
}files /path/to/dir1 /path/to/dir2 --format summaryOutput:
Identical: no
Only in left: 3
Only in right: 2
Modified: 1
Unchanged: 10
Total files: 16
Compare only top-level files without scanning subdirectories:
files /path/to/dir1 /path/to/dir2 --no-recursive#!/bin/bash
if files /backup /current --format summary; then
echo "Backup is up to date"
else
echo "Backup needs updating"
fiCopy and update files from source to destination (extra files in destination are kept):
files sync /source/dir /backup/dir --dry-run --verboseOutput:
π DRY RUN - No changes will be made
Would perform 4 operation(s)
Copy (3):
would copy new-file.txt
would copy docs/guide.md
would copy src/main.swift
Update (1):
would update config.yaml
Execute the sync:
files sync /source/dir /backup/dir --verboseOutput:
Performed 4 operation(s)
Copy (3):
copied new-file.txt
copied docs/guide.md
copied src/main.swift
Update (1):
updated config.yaml
Summary: 4 succeeded, 0 failed, 0 skipped
Mirror source to destination exactly (deletes extra files in destination):
files sync /source/dir /backup/dir --deletions --dry-run --verboseOutput:
π DRY RUN - No changes will be made
Would perform 5 operation(s)
Copy (3):
would copy new-file.txt
would copy docs/guide.md
would copy src/main.swift
Update (1):
would update config.yaml
Delete (1):
would delete old-file.txt
Execute the sync:
files sync /source/dir /backup/dir --deletions --verboseOutput:
Performed 5 operation(s)
Copy (3):
copied new-file.txt
copied docs/guide.md
copied src/main.swift
Update (1):
updated config.yaml
Delete (1):
deleted old-file.txt
Summary: 5 succeeded, 0 failed, 0 skipped
Synchronize two directories bidirectionally, keeping the newest version of conflicting files:
files sync /dir1 /dir2 --two-way --conflict-resolution newest --verboseOutput:
Performed 4 operation(s)
Copy (3):
copied unique-in-dir1.txt
copied unique-in-dir2.txt
copied another-file.md
Update (1):
updated conflicting-file.txt
Summary: 4 succeeded, 0 failed, 0 skipped
Always prefer the source directory for conflicts:
files sync /source /dest --two-way --conflict-resolution sourceSync unique files but skip conflicting files:
files sync /dir1 /dir2 --two-way --conflict-resolution skip --verbosefiles sync /source /dest --dry-run --format jsonOutput:
{
"operations": [
{
"type": "copy",
"path": "new-file.txt",
"source": "/source/new-file.txt",
"destination": "/dest/new-file.txt"
},
{
"type": "update",
"path": "modified.txt",
"source": "/source/modified.txt",
"destination": "/dest/modified.txt"
}
],
"summary": {
"total": 2,
"succeeded": 0,
"failed": 0,
"skipped": 2
}
}#!/bin/bash
# Mirror production to backup with exact mirroring (including deletions)
echo "Checking what would change..."
files sync /production /backup --deletions --dry-run --format summary
read -p "Proceed with sync? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
files sync /production /backup --deletions --verbose
if [ $? -eq 0 ]; then
echo "Backup completed successfully"
else
echo "Backup failed!"
exit 1
fi
fiKeep two working directories in sync:
# Sync laptop and desktop project directories
files sync ~/projects/myapp /mnt/desktop/projects/myapp --two-way --conflict-resolution newestswift build -c release \
-Xswiftc -O \
-Xswiftc -whole-module-optimization \
-Xswiftc -cross-module-optimization
You can copy it to your PATH:
cp .build/release/files /usr/local/bin/Or create a symlink:
ln -s $(pwd)/.build/release/files /usr/local/bin/filesLICENSE: MIT