finder-selection-sync
v2.2.0
Published
Sync Finder tags, clean files by reference extension, and keep or delete files by list.
Maintainers
Readme
finder-selection-sync
Sync macOS Finder Tags across original and processed file folders, clean files by a reference extension, and keep or delete files using a text list.
Photo and list cleanup (2.2.0)
These subcommands integrate the workflows previously provided by photome and deletebutme.
# Mixed folder: keep NEF and all matching filenames; preview orphan files of other extensions
finder-selection-sync clean-orphans '/path/to/photos' --reference-ext nef
# Any one extension can be the reference: CR3, JPG, ARW, DNG, etc.
finder-selection-sync clean-orphans '/path/to/photos' --reference-ext jpg
# Separate folders: base supplies reference filenames, targets are cleaned
finder-selection-sync clean-orphans --base '/path/to/NEF' --target '/path/to/exports' --reference-ext nef
# Keep filenames listed in a UTF-8 text file; preview everything else
finder-selection-sync clean-list '/path/to/target' --list '/path/to/keep.txt' --mode keep
# Delete only listed filenames; preview
finder-selection-sync clean-list '/path/to/target' --list '/path/to/delete.txt' --mode deleteNew commands are dry runs by default. Add --apply to move selected files to macOS Trash, or --apply --permanent to permanently delete. Trash uses Finder and may require macOS Automation permission. A failure stops subsequent operations without falling back to permanent deletion. --list-limit 0 shows every candidate.
clean-orphans uses --reference-ext <extension> to keep the specified extension and clean all other extensions that have no matching reference filename. No RAW-format setting or target-extension list is needed. For example:
Input With --reference-ext nef
A.NEF keep (reference)
A.JPG / A.CR3 / A.XMP / A.TIF keep (matching reference A.NEF)
B.JPG / B.CR3 / B.XMP / B.PSD clean (no B.NEF)This includes orphan sidecars, documents and videos, not just image formats. Files without an extension (including names ending in a bare dot) are preserved. Reference-extension files are never selected, including those in target directories. In separate-folder mode only the base supplies reference keys; target reference files do not expand that set. With an empty reference and explicit --allow-empty, all eligible non-reference extensions are selected.
The extension is case-insensitive, may include a leading dot (nef, NEF, .nef), and must be a single extension. jpg and jpeg are distinct: --reference-ext jpg uses only .jpg files as references; matching .jpeg files are retained as other formats. Matching uses the filename without its last extension, so A.NEF.XMP has stem A.NEF, not A.
Matching is case-insensitive by relative subdirectory and filename stem; use --match-by name to ignore directories. Processing suffixes are not stripped. Separate reference and target roots must not overlap; use the single-folder syntax for mixed photos. Multiple --target folders are supported.
The older --reference cr3 / --reference jpeg options remain available with their original paired CR3/JPEG-only scope. They cannot be combined with --reference-ext. Use the new option for multi-format cleanup.
Lists require explicit --mode keep|delete. Default matching is by full filename, including extension, with exact case and Unicode NFC normalization. Options:
--match-by name|relative|stem: full basename (default), relative file path, or basename without extension.--ignore-case: case-insensitive list matching.--include-ext jpg,jpeg: restrict eligible file types.--exclude-ext cr3,xmp: always preserve these types; exclusions take precedence.- Multiple positional targets or repeated
--targetoptions.
List lines may contain paths in name/stem modes; only the basename is used. Relative mode rejects absolute paths and traversal. A single leading UTF-8 encoding BOM is removed; any subsequent literal BOM character belongs to the filename. Blank lines are ignored, filename whitespace is preserved, and lines are not shell commands or comments. Lists and their hard links are protected from cleanup.
Both commands support --no-recursive or --depth N (0 = top level), reject overlapping cleanup roots, and skip hidden entries and symlinks. Empty references/lists fail unless --allow-empty is explicitly passed. With an empty keep-list, all eligible candidates are selected; an empty delete-list selects nothing. Directories are never deleted. Planned files and directory inventories are rechecked before execution. During orphan cleanup, a newly matching reference stops further deletion; this check uses the same directory, case and Unicode rules as planning. Directory paths are revalidated before descent to detect substituted symlinks. These checks reduce stale-plan risks but do not make cleanup transactional; avoid concurrent changes while it runs.
Use clean-orphans --help or clean-list --help for all options. Before publishing/installing this version, run node ./bin/finder-selection-sync.js from this checkout instead of the global command or npx.
The original commands below remain supported. In particular, legacy --delete-unmatched --apply still permanently deletes files; it now refuses empty reference folders and overlapping roots. Relative base paths literally named clean-orphans or clean-list must use ./clean-orphans / ./clean-list to avoid subcommand parsing.
Finder tag workflows
The CLI treats a base folder as the reference selection and matches its filenames against one or more target folders. It can assign one color tag to a selection or copy every Finder Tag from each base file to its target matches. A single file may contain multiple colors and custom named tags. It works with processing workflows that append bracketed variants such as -[variant-a][small].
Requirements
- macOS
- Node.js 18 or newer
Install
Run without installing:
npx finder-selection-sync <base> <target...>Or install the CLI globally:
npm install --global finder-selection-syncQuick start
Preview the matching files in two processed target folders:
finder-selection-sync \
'/path/to/base-selection' \
'/path/to/processed-target-1' \
'/path/to/processed-target-2'All commands are dry runs by default. Add --apply only after reviewing the output:
finder-selection-sync \
'/path/to/base-selection' \
'/path/to/processed-target' \
--applySync existing base tags
Copy all Finder Tags from each tagged base file to every matching target file:
finder-selection-sync \
'/path/to/base' \
'/path/to/processed-target-1' \
'/path/to/processed-target-2' \
--sync-tagsThe command above is a dry run. After reviewing each planned tag set, apply the changes:
finder-selection-sync <base> <target...> \
--sync-tags \
--applyBy default, the base tag set replaces the complete target tag set. To preserve target-only tags and add the base tags, use --merge-tags:
finder-selection-sync <base> <target...> \
--sync-tags \
--merge-tags \
--applyUntagged base files are skipped and do not change their target matches. To synchronize the untagged state as well, use --include-clear; matching target tags will then be cleared:
finder-selection-sync <base> <target...> \
--sync-tags \
--include-clear \
--applyIf duplicate base files have the same match key but different tag sets, the CLI reports the conflicting files and makes no changes. Tag order does not create a conflict. Without --include-clear, an untagged duplicate is ignored when another duplicate has tags.
--sync-tags operates on targets and cannot be combined with --scope base|both, --select, --color, --match, or --clear-all.
Positive and inverse selection
The default scope is targets.
| Selection | Option | Result |
| --- | --- | --- |
| Positive | --select matched | Target files with a matching filename in base |
| Inverse | --select unmatched | Target files without a matching filename in base |
| All | --select all | Every file in each target, regardless of matching |
Replace the tags on the inverse selection with a green color tag:
finder-selection-sync \
'/path/to/base-selection' \
'/path/to/processed-target-1' \
'/path/to/processed-target-2' \
--select unmatched \
--color green \
--applyClear tags
Clear all Finder Tags from every file in the target folders:
finder-selection-sync \
'/path/to/base-selection' \
'/path/to/processed-target-1' \
'/path/to/processed-target-2' \
--clear-all \
--apply--clear-all is a shortcut for --select all --color clear. It follows the selected scope, which is targets by default. Without --apply, the command only previews the files whose tags would be cleared.
To clear tags only from a positive or inverse selection, combine that selection with --color clear:
finder-selection-sync <base> <target...> \
--select unmatched \
--color clear \
--applyDelete unmatched target files
Preview target files that do not have a matching filename in the base folder:
finder-selection-sync <base> <target...> --delete-unmatchedAfter reviewing the complete list, permanently delete those files:
finder-selection-sync <base> <target...> \
--delete-unmatched \
--list-limit 0 \
--apply--delete-unmatched affects target files only. It does not delete base files or directories, and it cannot be combined with tag operations, --select, --color, or --match. Deletion is permanent and does not move files to the Trash.
Filename matching
Matching is case-insensitive. File extensions and the processing suffix beginning with the first -[ are ignored. These files therefore share the match key item_0001:
ITEM_0001.DATITEM_0001-[variant-a][small].datITEM_0001-[variant-a][small][export][web].dat
Files with the same match key are all selected; the implementation does not discard duplicates.
Options
--scope <base|targets|both> Side on which tag changes are applied (default: targets)
--select <mode> matched, unmatched, or all (default: matched)
--clear-all Clear all Finder tags from the selected scope
--sync-tags Copy all base Finder tags to matching target files
--merge-tags Merge base and target tags instead of replacing
--include-clear Also copy the untagged state with --sync-tags
--delete-unmatched Permanently delete target files not found in base
--match <any|all> Match base files in any or all targets (default: any)
--color <color> Finder tag color (default: green)
--apply Apply changes; otherwise preview only
--no-recursive Do not scan nested folders
--list-limit <number> Maximum selected paths to print; 0 prints all
-v, --version Show the installed version
-h, --help Show helpAvailable colors are clear, gray, green, purple, blue, yellow, red, and orange. Applying a color replaces the selected files' tag sets with that single color tag. clear removes all tags from the selected files.
--match any|all affects selection in base scope. Each target is compared directly with the base folder when the scope is targets.
Base and both scopes
Select base files found in at least one target:
finder-selection-sync <base> <target...> --scope base --match anySelect base files found in every target:
finder-selection-sync <base> <target...> --scope base --match allApply the selection to both sides:
finder-selection-sync <base> <target...> --scope both --applySafety
- The CLI never writes tags unless
--applyis present. - Hidden files and hidden directories are ignored.
- Existing unrelated
com.apple.FinderInfodata is preserved; only Finder's color bits are updated for compatibility. - Modern Finder Tags are read from and written to
com.apple.metadata:_kMDItemUserTags. --sync-tagsskips untagged base files unless--include-clearis present.- Conflicting base tag sets stop the entire tag-sync operation before any file is changed.
--delete-unmatchedis a dry run unless--applyis present, and it never deletes directories.- Paths are passed directly to
xattrwithout shell interpolation.
Development and tests
npm test
npm run test:coverageTests use temporary files and cover both cleanup directions, lists, filtering, filesystem guards, CLI integration, generated datasets, and Finder metadata. The default suite uses a substitute executor for Trash argument handling and failures and does not alter the user's Trash. An explicit macOS integration test exercises real Finder deletion, filename collisions, Unicode/quotes, list/reference preservation, and stopping when a new matching reference appears:
# From the source checkout only; may request macOS Automation permission
npm run test:finder-trashThis opt-in test creates isolated temporary fixtures, verifies their contents at the exact Trash paths returned by Finder, then restores and removes only those fixtures. It never enumerates or empties Trash. It does not run under npm test or during publishing. If recovery fails, retain the printed fixture directory for investigation; do not empty Trash. Concurrency checks are not a filesystem transaction and cannot eliminate every race between checking a path and modifying it.
License
MIT
