Automating the cleaning of macOS-specific files on Eject

Dot underscore ._ and .DS_Store files are macOS-specific metadata cruft generated for foreign filesystems (like FAT32 or exFAT) that are not usually needed for disks that are mainly used on other platforms. Digital cameras, music players, e-book readers, and handheld gaming devices can get confused when they encounter these odd files during file system parsing and directory listing. The problem is compounded if the devices naïvely process files by looking only at the file extension as they will then see the dot underscore version of a file as a duplicate and try to preview/play/open it.

For years I’ve used an app called CleanMyDrive to remove such files, but it was discontinued in October 2023. I continued to use it until it recently stopped working completely …so I needed to find an alternative solution. There are some apps on the Mac App Store that look like they’ll do the trick, but I don’t really want to spend the time buying and trialling multiple apps to find one that fits my usage habits. I can make one!

I already use an app called xbar for keeping track of my GitHub issues, itch.io sales, network/server status, and more. So I decided to flex my shell script muscles and put together an xbar script to do it.

The script adds a menu bar item that allows you to:

  • Eject (click)
  • Unmount (option-click)
  • Eject All (without cleaning, useful when you want to disconnect all drives from your computer)
  • All with a handy notification

That’s it! Straight to the point, no frills, functional software.

IMG

#!/bin/zsh
# <bitbar.title>Volume Manager</bitbar.title>
# <bitbar.version>250225</bitbar.version>
# <bitbar.author>Matt Sephton</bitbar.author>
# <bitbar.author.github>gingerbeardman</bitbar.author.github>
# <bitbar.desc>Lists and manages mounted user volumes</bitbar.desc>
# <bitbar.dependencies>zsh</bitbar.dependencies>
# <bitbar.abouturl>https://gist.github.com/gingerbeardman/610f22180117ad20465d7c529cc5faa0</bitbar.abouturl>
setopt EXTENDED_GLOB
# Function to show notification
show_notification() {
local title=$1
local message=$2
osascript -e "display notification \"$message\" with title \"$title\""
local script=$(basename "$0")
open "xbar://app.xbarapp.com/refreshPlugin?path=$script"
}
# Function to clean and eject/unmount volume
clean_and_process() {
local action=$1
local dev_id=$2
local mount_point=$(diskutil info $dev_id | grep "Mount Point:" | sed 's/.*Mount Point: *//')
local label=$(diskutil info $dev_id | grep -E "Volume Name:" | sed 's/.*Volume Name: *//')
label=${label:-"Untitled"}
# Clean ._ files if mount point exists
if [[ -d "$mount_point" ]]; then
# Remove ._ files
dot_clean -m "$mount_point"
# Remove .DS_Store files
find "$mount_point" -name ".DS_Store" -delete 2>/dev/null
fi
# Perform the requested action
if [[ $action == "eject" ]]; then
if diskutil eject $dev_id; then
show_notification "Volume Ejected" "Successfully ejected $label"
fi
elif [[ $action == "unmount" ]]; then
if diskutil unmount $dev_id; then
show_notification "Volume Unmounted" "Successfully unmounted $label"
fi
fi
}
# Function to eject all volumes (without cleaning)
eject_all() {
local -a volumes=("${(f)$(mount | grep '^/dev/')}")
local ejected_count=0
for vol in $volumes; do
local mount_point=${${(s: :)vol}[3]}
# Skip system volumes and developer paths
[[ -z $mount_point ]] && continue
[[ $mount_point == "/" ]] && continue
[[ $mount_point == /System* ]] && continue
[[ $mount_point == /Library/Developer* ]] && continue
local dev_id=${${(s: :)vol}[1]}
dev_id=${dev_id#/dev/}
# Direct eject without cleaning
if diskutil eject $dev_id; then
((ejected_count++))
fi
done
if (( ejected_count > 0 )); then
show_notification "Volumes Ejected" "Successfully ejected $ejected_count volumes"
fi
exit 0
}
# Handle actions
if [[ $1 == "eject-all" ]]; then
eject_all
elif [[ $1 == "unmount" || $1 == "eject" ]]; then
clean_and_process $1 $2
exit 0
fi
# Print the menu bar icon and text
print -- "⏏"
print -- "---"
print -- "Refresh | refresh=true"
print -- "---"
# Get list of mounted volumes using zsh array
local -a volumes=("${(f)$(mount | grep '^/dev/')}")
# Add Eject All option if there are valid volumes to eject
local has_valid_volumes=0
for vol in $volumes; do
local mount_point=${${(s: :)vol}[3]}
[[ $mount_point == "/" ]] && continue
[[ $mount_point == /System* ]] && continue
[[ $mount_point == /Library/Developer* ]] && continue
if [[ -n $mount_point ]]; then
has_valid_volumes=1
break
fi
done
# Process each volume
for vol in $volumes; do
# Parse mount point using zsh parameter expansion
local mount_point=${${(s: :)vol}[3]}
# Skip system volumes and developer paths using zsh pattern matching
[[ -z $mount_point ]] && continue
[[ $mount_point == "/" ]] && continue
[[ $mount_point == /System* ]] && continue
[[ $mount_point == /Library/Developer* ]] && continue
# Get device ID using zsh parameter expansion
local dev_id=${${(s: :)vol}[1]}
dev_id=${dev_id#/dev/}
# Get volume label using zsh process substitution
local label=$(diskutil info $dev_id | grep -E "Volume Name:" | sed 's/.*Volume Name: *//')
label=${label:-"Untitled"}
# Print volume info and actions, using -- to prevent option parsing
print -- "${mount_point} | bash='$0' param1=eject param2=$dev_id terminal=false length=40"
print -- "Unmount: ${mount_point} | alternate=true bash='$0' param1=unmount param2=$dev_id terminal=false length=40"
done
if (( has_valid_volumes )); then
print -- "---"
print -- "Eject All | bash='$0' param1=eject-all terminal=false color=red"
fi
view raw volumes.60s.sh hosted with ❤ by GitHub
--
Originally published: 2025-02-08
--
Your support through ko.fi keeps this blog brewing!
--
Comments: @gingerbeardman