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.

#!/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 |
Originally published: 2025-02-08
--
Your support through ko.fi keeps this blog brewing!
--
Comments: @gingerbeardman