A cron job that pops up a dialog and hopes for the best is a cron job that will confuse someone at 3am. xmessage fixes that: one command gets you a button, a predictable exit status, and, if you ask for it, the clicked label back on stdout. These examples follow the xmessage 1.0.6 interface documented by the installed x11-utils package. Budget 10 minutes if a display is already up; a headless box needs a decision about how to report the missing one before you go further.
You need the xmessage executable and a working X display. Check both before touching anything else:
command -v xmessage
printf 'DISPLAY=%s\n' "\${DISPLAY:-unset}"
dpkg-query -W -f='\${Package} \${Version}\n' x11-utils
On the machine used for this guide, the package reports x11-utils 7.7+6build2, and its manpage identifies the program as xmessage 1.0.6. The DISPLAY value must point to an X server your user can actually access. An empty value is not a prompt problem to debug: it just means there is nowhere for the window to appear.
Checkpoint: Continue when command -v prints a path and DISPLAY is set. Nothing in this guide needs elevated privileges. Do not run sudo xmessage: root may not have permission to touch your desktop display at all.
Pass the message as one or more non-option arguments. Without -buttons, xmessage creates a single okay button with exit value 0:
xmessage -center 'Backup finished: /srv/example was copied.'
-center just asks for the centre of the screen; it is optional, so drop it if the window manager should place the window itself. The command blocks the terminal until someone clicks, then exits with that button's value.
Check the exit status immediately, before another command overwrites it:
xmessage -center 'Click OK, then inspect this status.'
status=$?
printf 'xmessage exited with status %s\n' "$status"
Click the default button and you should see xmessage exited with status 0. A message bigger than the window gets scroll bars; without an explicit size, xmessage grows to fit, capped at 70 percent of the screen dimensions by default.
-buttons with comma-separated labels. Each one becomes a clickable button, left to right.:value to a label when the script needs a status number that is easy to read back.xmessage -center \
-buttons 'Open:10,Later:20,Cancel:30' \
-default Open \
'The report is ready. What should happen next?'
status=$?
case "$status" in
10) printf '%s\n' 'opening the report' ;;
20) printf '%s\n' 'leaving the report for later' ;;
30) printf '%s\n' 'cancelling' ;;
1) printf '%s\n' 'xmessage could not open or display the prompt' >&2; exit 1 ;;
*) printf 'unexpected xmessage status: %s\n' "$status" >&2; exit 1 ;;
esac
Buttons are numbered from the left, and an unassigned value defaults to 100 plus that button's number, which is exactly the kind of thing you do not want to rely on. Explicit values decouple your script from button order. -default Open makes Return activate that button; leave out -default and Return does nothing at all.
Safety boundary: Keep the case branches separate from the prompt itself. Never put a destructive command directly in a button label, and never assume that closing the window means approval. The documented error status is 1, so reserve it and handle it as a failure.
Add -print when the label itself is the useful value. It goes to standard output while the exit status still carries the configured number:
choice=$(xmessage -print \
-buttons 'Yes:0,No:1' \
-default No \
'Send the report now?')
status=$?
if [ "$status" -eq 1 ] && [ "$choice" = 'No' ]; then
printf '%s\n' 'not sending'
elif [ "$status" -eq 0 ] && [ "$choice" = 'Yes' ]; then
printf '%s\n' 'sending'
else
printf 'prompt failed: status=%s label=%s\n' "$status" "$choice" >&2
exit 1
fi
Checking both values stops a partial or unexpected result being treated as consent. Quote your command substitutions and variables too: labels are user-visible text rather than shell syntax, but unquoted output can still split into multiple words.
Use -file once the message is too long for a readable command line. xmessage reads the file itself, and a filename of - means standard input:
xmessage -file /path/to/report.txt -buttons 'Close:0'
printf '%s\n' 'Service check passed.' | \
xmessage -file - -center -buttons 'Close:0'
Choose either -file or command-line arguments, not both. There is nothing to undo here: xmessage only displays text and exits, it never creates or edits the input file.
A prompt with no timeout waits forever, which is a bad look in an unattended job. Add -timeout and on expiry xmessage exits with status 0, the exact same status the default okay button uses:
xmessage -timeout 30 -buttons 'Continue:0,Stop:2' \
-default Stop 'This prompt expires in 30 seconds.'
status=$?
case "$status" in
0) printf '%s\n' 'continued or timed out' ;;
2) printf '%s\n' 'stopped by the operator' ;;
1) printf '%s\n' 'display failure' >&2; exit 1 ;;
*) printf 'unexpected status: %s\n' "$status" >&2; exit 1 ;;
esac
If timeout and acceptance need to mean different things, give the acceptance button its own value and treat status 0 as a timeout only when the label was printed with -print. A missing X display still produces an error rather than a timeout, even in an unattended job. Recovery there is operational: run the job somewhere an authorised display exists, build a separate non-graphical path, or stop and report the failure.
xmessage runs and DISPLAY points to an accessible X server.$? straight away and reserves status 1 for errors.-file or standard input, never an unreadable command line.