Every shell scripter has, at some point, googled “date subtract day mac” or “GNU date macOS BSD difference” and wished they hadn’t. The date command is one of the most platform-fragmented tools in any Unix toolbox: GNU date and BSD date disagree on basically every flag that matters. And once you’ve worked out the syntax for “yesterday,” you still need to do something useful with it — like generate a range of timestamps for a backfill, or compute end-of-month, or skip weekends.
I got tired of this. So I wrote calcdate. Same binary on Linux, macOS, anywhere. One expression syntax. Version 2 just landed and the syntax is now genuinely pleasant to use.
The expression syntax
calcdate --expr "today +1d" # tomorrow
calcdate --expr "now +2h" # two hours from now
calcdate --expr "yesterday" # yesterday at 00:00
calcdate --expr "today +1w" # one week from today
calcdate --expr "today | endOfMonth" # last day of this month
The pipeline operator (|) is the thing that makes complex expressions readable. Read it left-to-right like a shell pipeline:
# 2 hours from now, then rounded to the hour
calcdate --expr "now | +2h | round hour"
# Last day of next month
calcdate --expr "today | +1M | endOfMonth"
You can list every available operation with calcdate --list-ops. The categories are:
- Date values:
today,now,yesterday,tomorrow, weekday names - Arithmetic:
+/-with unitss,m,h,d,w,M,Y,q - Boundaries:
startOfMonth,endOfMonth,startOfWeek, etc. - Setters:
day,time - Transforms:
round,trunc
Ranges and iterations
This is the part that earns its place in scripts. today...+7d is a range; --each=1d iterates it; --transform reshapes each iteration:
# Each day for the next week
calcdate --expr "today...+7d" --each=1d
# Business hours each day for the next week
calcdate --expr "today...+7d" --each=1d \
--transform='$begin +8h, $end +20h'
Skip weekends with --skip-weekends. Combine the two and you have a one-liner that produces every weekday between two dates with arbitrary working hours. I use this for backfill scripts, cron-window generation, and synthetic data.
Output formats
Pick the format that matches the next tool in your pipeline:
calcdate --expr "tomorrow" --format=iso # 2024-01-16T00:00:00Z
calcdate --expr "today" --format=sql # 2024-01-15 00:00:00
calcdate --expr "now" --format=ts # 1705331400
Or pass a strftime string directly:
calcdate --expr "today" --format='%Y-%m-%d %H:%M:%S %Z'
Stdin works too
The flag-free path: pipe an expression in.
echo "today" | calcdate
echo "today...+7d" | calcdate --each=1d --format=sql | head -3
# 2024-01-15 00:00:00 - 2024-01-16 00:00:00
# 2024-01-16 00:00:00 - 2024-01-17 00:00:00
# 2024-01-17 00:00:00 - 2024-01-18 00:00:00
A few real scripts I keep around:
# DB-friendly timestamps for the last 30 days
echo "today -30d...today" | calcdate --each=1d --format=ts
# Deployment windows: every Sunday at 2am for the next 3 months
echo "today | startOfWeek +7d...+3M" | calcdate --each=1w \
--transform='$begin +2h' --format=iso
# One backup label per weekday in the next month
echo "today...+30d" | calcdate --each=1d --skip-weekends \
--format='backup-%Y%m%d'
Timezones, properly
--tz works on input and output. Convert UTC to CET:
date -u "+%Y-%m-%dT%H:%M:%SZ" \
| calcdate --tz CET --format="%Y-%m-%d %H:%M:%S %Z"
Parse a date in EST, output in UTC:
echo "2024-01-15 10:30:00 EST" | calcdate --format iso
calcdate --list-tz dumps the full list.
Install
# Homebrew
brew tap sgaunet/homebrew-tools
brew install sgaunet/tools/calcdate
# Go
go install github.com/sgaunet/calcdate/v2@latest
Shell completion works for bash, zsh, fish, and powershell — calcdate completion zsh and so on.
Heads up: v2 is a breaking change
If you’ve used calcdate v1, the legacy CLI flags are gone in v2 and the expression syntax is new. There’s a MIGRATION.md in the repo with the rewrite map. The new syntax is a strict improvement, in my biased opinion.
Where to find it
- Source: github.com/sgaunet/calcdate
- License: MIT
If date has ever made you angry — and let’s be honest — give this a try. It’s the tool I now reach for any time a shell script needs to compute, format, or iterate over dates.