#!/bin/bash # notify - show a macOS notification via osascript # # Usage: # notify [options] [message ...] # command | notify [options] # notify -h # # Options: # -t, --title TITLE Notification title (default: "Notification") # -s, --subtitle SUBTITLE Notification subtitle # --sound SOUND Sound name (default: "Glass") # -n, --no-sound Suppress notification sound # -l, --list-sounds List available sound names and exit # -h, --help Show help and exit # # Environment: # NOTIFY_TITLE Default title (overridden by --title) # NOTIFY_SOUND Default sound (overridden by --sound / --no-sound) # # macOS only. osascript is the backing mechanism for notifications _notify() ( local SCRIPT_NAME; SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" case "${BASH_SOURCE[0]}" in /dev/*|/proc/*) SCRIPT_NAME="" ;; esac case "$SCRIPT_NAME" in ""|bash|sh|zsh|dash) SCRIPT_NAME="notify" ;; esac _color() { [ -t 2 ] && [ -z "${NO_COLOR:-}" ] && printf '%s' "$1"; } _error() { printf '%s[ERR][%s] %s%s\n' "$(_color $'\033[31m')" "$SCRIPT_NAME" "$*" "$(_color $'\033[0m')" >&2; } _show_help() { local s; [ -t 1 ] && s=$'\033[4m' local r; [ -t 1 ] && r=$'\033[24m' echo "NAME" echo " $SCRIPT_NAME - show a macOS notification via osascript" echo "SYNOPSIS" echo " $SCRIPT_NAME [${s}options${r}] [${s}message${r} ...]" echo " ${s}command${r} | $SCRIPT_NAME [${s}options${r}]" echo " $SCRIPT_NAME -h" echo "DESCRIPTION" echo " Displays a macOS notification in the top-right corner and in" echo " Notification Center. The ${s}message${r} may be passed as positional" echo " arguments or piped to stdin." echo "" echo " Notifications are attributed to Script Editor (the osascript host" echo " app). Notification duration is controlled by macOS, not this script" echo " (System Settings > Notifications > Script Editor)." echo "OPTIONS" echo " -t, --title ${s}title${r} Notification title (default: \"Notification\")" echo " -s, --subtitle ${s}subtitle${r} Notification subtitle" echo " --sound ${s}sound${r} Sound name (default: \"Glass\")" echo " -n, --no-sound Suppress notification sound" echo " -l, --list-sounds List available sound names and exit" echo " -h, --help Show this help message" echo "ENVIRONMENT" echo " NOTIFY_TITLE Default title (overridden by --title)" echo " NOTIFY_SOUND Default sound (overridden by --sound / --no-sound)" echo "EXIT STATUS" echo " 0 Success" echo " 2 Invalid arguments or missing required message" echo " 3 osascript not found (not running on macOS)" echo "DEPENDENCIES" echo " osascript (macOS)" echo "CAVEATS" echo " macOS only. osascript is the backing mechanism for notifications." } _list_sounds() { local sounds="" local dir local file local name for dir in /System/Library/Sounds ~/Library/Sounds; do [ -d "$dir" ] || continue for file in "$dir"/*; do [ -f "$file" ] || continue name="${file##*/}" name="${name%.*}" sounds="${sounds:+$sounds$'\n'}$name" done done if [ -z "$sounds" ]; then _error "No sounds found in /System/Library/Sounds/ or ~/Library/Sounds/" return 1 fi printf '%s\n' "$sounds" | sort } _expand_short_opts() { # $1 = string of short-opt letters that take a value (e.g. "nXHd"); "" for flag-only scripts # $2..$N = "$@" # Populates _EXPANDED; caller does: set -- "${_EXPANDED[@]}"; unset _EXPANDED local value_opts="$1"; shift _EXPANDED=() local passthru="" local arg local rest local c for arg in "$@"; do if [ -n "$passthru" ]; then _EXPANDED+=("$arg"); continue; fi case "$arg" in --) passthru=1; _EXPANDED+=("$arg") ;; --*|-|"") _EXPANDED+=("$arg") ;; -[a-zA-Z]?*) rest="${arg#-}" while [ -n "$rest" ]; do c="${rest%"${rest#?}"}"; rest="${rest#?}" _EXPANDED+=("-$c") case "$value_opts" in *"$c"*) [ -n "$rest" ] && _EXPANDED+=("$rest") rest="" ;; esac done ;; *) _EXPANDED+=("$arg") ;; esac done } if ! command -v osascript >/dev/null 2>&1; then _error "osascript not found (macOS only)" return 3 fi local title="${NOTIFY_TITLE:-Notification}" local subtitle="" local sound="${NOTIFY_SOUND:-Glass}" local no_sound="" local list_sounds="" local -a args=() _expand_short_opts "ts" "$@" set -- "${_EXPANDED[@]}"; unset _EXPANDED while [ $# -gt 0 ]; do case "$1" in -h|--help) _show_help return 0 ;; -l|--list-sounds) list_sounds=1 shift ;; -n|--no-sound) no_sound=1 shift ;; -t|--title) [ -n "${2-}" ] || { _error "-t|--title requires a value. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } title="$2" shift 2 ;; --title=*) title="${1#*=}" [ -n "$title" ] || { _error "-t|--title requires a value. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } shift ;; -s|--subtitle) [ -n "${2-}" ] || { _error "-s|--subtitle requires a value. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } subtitle="$2" shift 2 ;; --subtitle=*) subtitle="${1#*=}" [ -n "$subtitle" ] || { _error "-s|--subtitle requires a value. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } shift ;; --sound) [ -n "${2-}" ] || { _error "--sound requires a value. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } sound="$2" shift 2 ;; --sound=*) sound="${1#*=}" [ -n "$sound" ] || { _error "--sound requires a value. Run \`$SCRIPT_NAME -h\` for usage"; return 2; } shift ;; --) shift while [ $# -gt 0 ]; do args+=("$1") shift done ;; -*) _error "Unknown argument '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 ;; *) args+=("$1") shift ;; esac done if [ -n "$list_sounds" ]; then _list_sounds return $? fi # Build message from positional args or stdin local message="" if [ ${#args[@]} -gt 0 ]; then message="${args[*]}" elif [ ! -t 0 ]; then message="$(cat)" fi if [ -z "$message" ]; then _error "MESSAGE is required (pass as argument or pipe to stdin)" _show_help >&2 return 2 fi local effective_sound="$sound" if [ -n "$no_sound" ]; then effective_sound="" fi osascript - "$message" "$title" "$subtitle" "$effective_sound" <<'APPLESCRIPT' on run argv set msg to item 1 of argv set ttl to item 2 of argv set sub to item 3 of argv set snd to item 4 of argv if sub is "" and snd is "" then display notification msg with title ttl else if sub is "" then display notification msg with title ttl sound name snd else if snd is "" then display notification msg with title ttl subtitle sub else display notification msg with title ttl subtitle sub sound name snd end if end run APPLESCRIPT ) _notify "$@" __notify_rc=$? unset -f _notify if [ -n "${BASH_SOURCE[0]}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then eval "unset __notify_rc; return $__notify_rc" fi eval "unset __notify_rc; exit $__notify_rc"