Shell notifications

Get notified when a long command finishes, locally or over SSH

Append ; notify-send "Done" "exit $?" to any command and a desktop popup reports its exit status when it ends, with 0 meaning success. Over SSH there is no desktop session bus, so notify-send fails with exit status 1; send the alert to your phone with curl to ntfy or a Discord webhook instead. For a job that is already running, tail --pid waits for it.

4 min readNotifications and alerts

At a glance

  • ; alerts after success and failure; && only after success.
  • $? holds the last exit status, and 0 means success.
  • Over SSH, use curl to ntfy or a webhook instead of notify-send.
  • Already running? tail --pid=PID -f /dev/null waits for it.

Desktop popup with notify-send

Add notify-send after your command to get a desktop popup when it finishes. The syntax is notify-send [-u low|normal|critical] [-t ms] [-a app] [-i icon] "summary" "body", and it talks to the notification service over your desktop session's D-Bus.

How you join the two commands matters. With ; the popup appears whatever happened, && fires only after success (exit status 0) and || only after a failure. $? holds the exit status of the last command, so "exit $?" in the body tells you whether it worked before you switch windows.

Bash
make build; notify-send "Build finished" "exit $?"
make build && notify-send "Build passed"
make build || notify-send -u critical "Build failed"

A few flags worth knowing:

  • -u sets the urgency: low, normal or critical.
  • -t sets how long the popup stays, in milliseconds, but GNOME Shell ignores it.
  • -a sets the app name and -i an icon.

Typing that every time gets old. Put a small function in ~/.bashrc or ~/.zshrc that runs any command, keeps its exit status and passes it to the popup.

Bash
notify_done() {
  "$@"
  local status=$?
  notify-send "Finished: $*" "exit $status"
  return $status
}

notify_done make build

Already running? Wait for the PID

You started a backup 20 minutes ago and forgot the notification. Open a second terminal, find the process ID with pgrep -f backup.sh, and wait for it from there.

The bash builtin wait doesn't work for this: it only waits for children of the current shell. From another terminal it fails with wait: pid 12345 is not a child of this shell and exit status 127.

Use tail --pid=PID -f /dev/null from GNU coreutils, which exits as soon as that process is gone. Without GNU tail, poll with kill -0 in a loop. Neither way can see the other process's exit status, only that it ended.

Bash
tail --pid=12345 -f /dev/null; notify-send "Process 12345 ended"

while kill -0 12345 2>/dev/null; do sleep 5; done; notify-send "Process 12345 ended"

Terminal bell and macOS

If you only want the terminal to get your attention, printf '\a' rings the bell. What that does, a sound, a flashing tab or a dock badge, depends on your terminal settings. Because it is just a character in the output, it reaches your local window over SSH too.

macOS has no notify-send. Use osascript to show a Notification Center banner, or say to have the Mac read the result out loud.

Bash
./backup.sh; printf '\a'
./backup.sh; osascript -e 'display notification "Done" with title "backup.sh"'
./backup.sh; say "backup finished"

Over SSH: send it to your phone

On a remote server over plain SSH there is usually no notification service to talk to, so notify-send exits with status 1 and an error such as Failed to show notification: Cannot autolaunch D-Bus without X11 $DISPLAY.

Send the alert over HTTPS instead, and run long jobs inside tmux so a dropped connection doesn't kill the job before it reaches the notification. ntfy needs only curl and a topic name; topics on ntfy.sh are public and capped at 250 messages a day, so pick a long random name. A Discord webhook takes JSON with content up to 2,000 characters.

Bash
./backup.sh; curl -s -H "Title: backup.sh finished" -d "exit $?" ntfy.sh/your-long-random-topic

./backup.sh; status=$?
curl -s -H "Content-Type: application/json" \
  -d "{\"content\": \"backup.sh finished with exit $status\"}" \
  "$DISCORD_WEBHOOK_URL"

For a single Python script, run PYTHONUNBUFFERED=1 tqdml run -- python job.py so its output reaches the live page as it prints.

Which method works where

MethodWorks over SSHFires on failureNotes
cmd; notify-sendNoYesNeeds the desktop D-Bus; put $? in the body
cmd && notify-sendNoNoOnly after exit status 0; add || for failures
printf '\a'YesYes, with ;Sound, flash or badge depends on the terminal
osascriptNoYes, with ;Notification Center banner on the Mac that runs it
curl to ntfyYesYes, with ;250 messages a day on ntfy.sh; public topics
curl to a Discord webhookYesYes, with ;JSON content up to 2,000 characters
tqdml run with TQDMLIVE_NOTIFYYesYesLive output page; exit code and duration to Discord or Slack

FAQ

What is the difference between ; and && before notify-send?

A semicolon runs notify-send when the first command ends, whether it worked or not. && runs it only after exit status 0, and || only after a non-zero exit status.

Why does notify-send not work over SSH?

notify-send talks to the notification service over a desktop session D-Bus. A plain SSH session usually has none, so it exits with status 1 and an error such as Failed to show notification: Cannot autolaunch D-Bus without X11 $DISPLAY. Use curl to ntfy or a webhook instead.

How do I get notified when an already running process finishes?

Find its PID with pgrep -f, then run tail --pid=PID -f /dev/null followed by your notification command. Without GNU tail, loop on kill -0 PID. The bash wait builtin only works for children of the same shell.

How do I get a notification on macOS when a command finishes?

Append osascript -e 'display notification "Done" with title "Job"' to show a Notification Center banner when the command ends. For an audio alert, append say with the text to read out.

Can the notification include the exit code?

Yes. The shell keeps the last exit status in $?, where 0 means success. Join your job and the notification with ; and put $? inside the double-quoted message.

Check on long runs from your phone

Change one import and every tqdm bar prints a link you can open on any device. Get a Discord or Slack message when the run finishes, fails or stalls.

Get a live linkpip install tqdm-live

Sources