macOS safe rm wrapper that moves user files to Trash while preserving common rm semantics · GitHub
/" data-turbo-transient="true" />
Skip to content
-->
Search Gists
Search Gists
Sign in
Sign up
You signed in with another tab or window. Reload to refresh your session.<br>You signed out in another tab or window. Reload to refresh your session.<br>You switched accounts on another tab or window. Reload to refresh your session.
Dismiss alert
{{ message }}
Instantly share code, notes, and snippets.
Divide-By-0/safe-rm-to-trash.sh
Created<br>August 6, 2026 23:44
Show Gist options
Download ZIP
Star
(0)
You must be signed in to star a gist
Fork
(0)
You must be signed in to fork a gist
Embed
Select an option
Embed<br>Embed this gist in your website.
Share<br>Copy sharable link for this gist.
Clone via HTTPS<br>Clone using the web URL.
No results found
Learn more about clone URLs
Clone this repository at <script src="https://gist.github.com/Divide-By-0/5e1ae59ccf66129dd9ec5ef239f87d65.js"></script>
" readonly="readonly" data-autoselect="true" data-target="primer-text-field.inputElement " aria-describedby="validation-e868d8f5-515a-4f75-ba8b-43c619aad877" class="form-control FormControl-monospace FormControl-input FormControl-small rounded-left-0 rounded-right-0 border-right-0" type="text" name="gist-share-url-sized-down" />
Save Divide-By-0/5e1ae59ccf66129dd9ec5ef239f87d65 to your computer and use it in GitHub Desktop.
Embed
Select an option
Embed<br>Embed this gist in your website.
Share<br>Copy sharable link for this gist.
Clone via HTTPS<br>Clone using the web URL.
No results found
Learn more about clone URLs
Clone this repository at <script src="https://gist.github.com/Divide-By-0/5e1ae59ccf66129dd9ec5ef239f87d65.js"></script>
" readonly="readonly" data-autoselect="true" data-target="primer-text-field.inputElement " aria-describedby="validation-9cc1a991-355b-45f7-b7cd-c67de10b3ecf" class="form-control FormControl-monospace FormControl-input FormControl-small rounded-left-0 rounded-right-0 border-right-0" type="text" name="gist-share-url-original" />
Save Divide-By-0/5e1ae59ccf66129dd9ec5ef239f87d65 to your computer and use it in GitHub Desktop.
Download ZIP
macOS safe rm wrapper that moves user files to Trash while preserving common rm semantics
Raw
safe-rm-to-trash.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.<br>Learn more about bidirectional Unicode characters
Show hidden characters
#!/bin/bash
# Move files to macOS Trash (safe rm wrapper).
# Goal: behave like POSIX rm for every case a script or build system can observe
# (exit codes, -f semantics, refusing to remove a directory without -r), while
# routing real user data to ~/.Trash instead of destroying it.
# NOTE: bash 3.2 compatible on purpose (/bin/bash on macOS is 3.2) — no mapfile,
# no ${var,,}, no `set -u` (3.2 errors on "${arr[@]}" when the array is empty).
status=0
force=false
recursive=false
verbose=false
interactive=false
dir_ok=false
end_of_flags=false
flags=()
paths=()
for arg in "$@"; do
if [ "$end_of_flags" = false ] && [ "$arg" = "--" ]; then
# REASON: POSIX `--` ends option parsing. Without this, `rm -rf -- -weirdname`
# treated the filename as a flag and silently never deleted it.
end_of_flags=true
continue
fi
if [ "$end_of_flags" = false ] && [[ "$arg" == -?* ]]; then
[[ "$arg" == *f* ]] && force=true
[[ "$arg" == *r* || "$arg" == *R* ]] && recursive=true
[[ "$arg" == *v* ]] && verbose=true
[[ "$arg" == *i* ]] && interactive=true
[[ "$arg" == *d* ]] && dir_ok=true
flags+=("$arg")
continue
fi
paths+=("$arg")
done
if [ ${#paths[@]} -eq 0 ]; then
# REASON: `rm -f` with no operands must exit 0 (POSIX). Build scripts do
# `rm -f $(FILES)` where FILES can expand to nothing.
[ "$force" = true ] && exit 0
echo "usage: rm [-f | -i] [-dIPRrvW] file ..." >&2
exit 1
fi
# Find the mount point containing $1 by walking up until the device number changes.
# REASON: parsing `df` output breaks on volume names with spaces (e.g. /Volumes/Power Protect).
mount_point_of() {
local p="$1" dev parent
dev="$(stat -f %d -- "$p" 2>/dev/null)" || return 1
while [ "$p" != "/" ]; do
parent="$(dirname -- "$p")"
[ "$(stat -f %d -- "$parent" 2>/dev/null)" != "$dev" ] && break
p="$parent"
done
printf '%s' "$p"
# Move $1 to the Trash using the same API Finder uses (NSFileManager -trashItemAtURL:).
# REASON: a raw rename(2) into ~/.Trash fails with EDEADLK ("Resource deadlock avoided")
# for items under a FileProvider/iCloud domain such as ~/Documents (and its .nosync
# subtrees), and also under concurrent modification of the same tree. trashItemAtURL is
# domain-aware and routes to the correct trash instead of doing a cross-domain rename.
# REASON: osascript is wrapped in `timeout 60 taskpolicy...