Skip to content

Instantly share code, notes, and snippets.

Last active June 12, 2024 21:04
Show Gist options
  • Save ttscoff/cded212ec4dd457186ca to your computer and use it in GitHub Desktop.
Save ttscoff/cded212ec4dd457186ca to your computer and use it in GitHub Desktop.
Quick reminders from Terminal (bash)
# dontforget
# A stupid script for short term reminders in bash
# Arguments just need to contain a number and a bunch of words.
# The number can be anywhere in the arguments, but there shouldn't
# be any other numeric digits.
# If the number has h, m, or s immediately after it (no space), it will
# be interpreted as hours, minutes, or seconds. The default assumption
# is minutes.
# Save the script as `dontforget` in your path and make it executable
# Usage
# $ dontforget let the dog back inside 10
# => I'll remind you: "let the dog back inside" in 10 minutes
# It can parse a little extra verbosity around the text, too
# $ dontforget to let my dog back inside in 10m
# => I'll remind you: "let your dog back inside" in 10 minutes
# $ dontforget I really need to take a break in 1h
# I'll remind you: "you really need to take a break" in 60 minutes
# Running dontforget with no arguments will list upcoming reminders
# Use `dontforget cancel` or `dontforget nevermind` to cancel the last
# reminder that was created
# $ dontforget cancel
# Canceled "you really need to take a break"
# To turn on LaunchBar "large text" display
# export DF_LAUNCHBAR=true
# To make dontforget remain in the foreground and cancelable with ^c
# export DF_NO_BACKGROUND=true
# Tip:
# alias forget="dontforget cancel"
__df() {
if [[ $# == 0 ]]; then
if [[ $# == 1 && $1 =~ (cancel|nevermind) ]]; then
return $?
local timespan reminder
for arg in $@; do
# e.g. 3:30pm, 10am -- parse as date
if [[ $arg =~ ^[0-9][0-9]?(:[0-9][0-9])?(a|p)m?$ && -z $timespan ]]; then
# ensure meridian (2a = 2am)
arg=$(echo "$arg"|sed -E 's/m?$/m/')
timespan=$(__df_parse_date $arg)
# e.g. 1h or 30m -- parse as interval
elif [[ $arg =~ ^[0-9]+[hms]?$ && -z $timespan ]]; then
timespan=$(echo "$arg"|sed -E 's/^([0-9]+)([HhSsMm])?$/\1 \2/')
# Assume part of reminder string
reminder+=" $arg"
length=${timespan%% *}
unit=${timespan#* }
if [[ $unit == "h" ]]; then
elif [[ $unit != "s" ]]; then
reminder=$(__df_clean_reminder "$reminder")
# echo "I'll remind you: \"$reminder\" in $(($length/60)) minutes"
echo "I'll remind you: \"$reminder\" in $(__df_show_time $length)"
if [[ $DF_NO_BACKGROUND == true ]]; then
sleep $length && __df_remind "$reminder"
sleep $length && __df_remind "$reminder" &
__df_parse_date() {
TIMESTAMP=$(ruby -rtime -e "puts Time.parse('$1').strftime('%s')")
NOW=$(date '+%s')
echo "$(($TIMESTAMP-$NOW)) s"
__df_list_reminders() {
IFS=$'\n'; for line in $(ps ax | grep -E "bash .*?dontforget \w+" | grep -v grep); do echo $(__df_clean_reminder $(echo "$line" | sed -E 's/.*dontforget //')); done
__df_remind() {
local reminder="$*"
if [[ -n $(ps ax | grep && $DF_LAUNCHBAR == true ]]; then
osascript -e "tell application \"LaunchBar\" to display in large type \"Time to $reminder\" with sound \"Glass\""
osascript -e "display dialog \"Time to $reminder\""
/usr/bin/afplay /System/Library/Sounds/Glass.aiff
say "$reminder"
__df_clean_reminder() {
local input="$*"
# trim whitespace
input=$(echo -e "$input" | sed -E 's/(^ *| *$)//g') | tr -s ' '
# change " I " to " you "
input=$(echo -e "$input" | sed -E 's/(^| +)[Ii]( +|$)/\1you\2/g')
# change " my " to " your "
input=$(echo -e "$input" | sed -E 's/(^| +)[Mm]y( +|$)/\1your\2/g')
# strip leading "forget" in case you alias to dont, i.e. "dont forget to..."
input=$(echo -e "$input" | sed -E 's/^ *forget//')
# strip extra words from natural language
local output=$(__df_strip_naturals "$input")
# final whitespace trim
echo -e "$output" | sed -E 's/(^ *| *$)//g' | tr -s ' '
__df_strip_naturals() {
local original=$(echo "$*"|sed -E 's/^( *remind me *)//')
while [[ "$original" =~ ^[[:space:]]*(to|in|about|at) ]]; do
original=$(echo "$original"| sed -E 's/^ *(to|in|about|at) *//g')
[[ "$original" =~ in[[:space:]]*$ ]] && original=$(echo "$original"| sed -E 's/ *in *$//')
# [[ "$original" =~ at[[:space:]]*$ ]] && original=$(echo "$original"| sed -E 's/ *at *$//')
echo $original | sed -E 's/(^ *| *$)//g'
__df_show_time () {
local num=$1
local sec=0
local min=0
local hour=0
local day=0
local output=""
if ((num>59)); then
if ((sec>0)); then
output="${sec} $(__df_pluralize $sec second) $output"
if((num>59)); then
if ((min>0)); then
output="${min} $(__df_pluralize $min minute) $output"
if((num>23)); then
if ((hour>0)); then
output="${hour} $(__df_pluralize $hour hour) $output"
if ((day>0)); then
output="${day} $(__df_pluralize $day day) $output"
output="${hour} $(__df_pluralize $hour hour) $output"
output="${min} $(__df_pluralize $min minute) $output"
output="${sec} $(__df_pluralize $sec sec) $output"
echo $output
__df_pluralize() {
if (($1>1)); then
echo "${2}s"
echo $2
__df_cancel() {
local df_job=$(ps ax | grep dontforget | grep -v grep | grep -v cancel | grep -v nevermind | tail -n 1)
if [[ -z $df_job || $df_job == "" ]]; then
echo "No timer found"
return 1
local pid=$(echo "$df_job" | awk '{print $1}')
kill $pid
local title=$(echo "$df_job" | sed -E 's/.*dontforget (.*)$/\1/')
echo "Canceled \"$(__df_clean_reminder "$title")\""
__df $@
Copy link

ghost commented Jan 23, 2016

Thanks for this! Super handy script.

On line 59 you have variable local instring="$*" but it's never used. Not a big deal.

On line 103 shellcheck recommended to move the final parenthesis to properly assign the output of the pipeline:

input=$(echo -e "$input" | sed -E 's/(^ *| *$)//g' | tr -s ' ')

Copy link

ghost commented Jan 23, 2016

You're a really good programmer and it's always inspiring seeing the way you write code. Thanks for sharing. 👍

Copy link

I really need to touch up on my bash scripting :-/

A convenient and somewhat related add-on to this script could be something that also runs lightly in the background and if you type a certain command, it will give you a little reminder of any common followup commands that you should probably do (in case you aren't in a good habit of some command sequence or something).

Just thought of it as I'm working through Django and it is good practice that if you change a model, you should then generate a migration file as well, then execute the migration (depending on if your team's setup), and then you should make a single commit which includes both the migration file and the changes to the model.

Anyways, cool gist! I'll probably get around to making that modification one day, but it could be a long while so if you like it, feel free to build on it @ttscoff or others.

Copy link

pendashteh commented Aug 12, 2017

Excellent! And impressive. Haven't gone through the code yet. I'm leaving a reminder to myself to do so tomorrow ;)
P.S. Wonder wether it recognises 1d or not!


  1. Ok, I tried 1d and didn't work and I used 24h and of course it did.
  2. I noticed a tiny bug! I used dontforget in the description and it trimmed it and everything before it!
~/dev/apps/dontforget [global_reporistory__master]$ ./dontforget.bash to look at dontforget source code in 1d
./dontforget.bash: line 75: *60: syntax error: operand expected (error token is "*60")
~/dev/apps/dontforget [global_reporistory__master]$ ./dontforget.bash to look at dontforget source code in 24h
I'll remind you: "look at dontforget source code" in 1440 minutes
~/dev/apps/dontforget [global_reporistory__master]$ ./dontforget.bash 
source code in 24h

Copy link

tl87 commented Sep 25, 2017

You could change say "$reminder" on line 97 to notify-send Test "$reminder"

Then reminders would be posted as a notification:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment