Dates and times show up in almost every script I write — timestamping log entries, naming backup files, calculating how long a job has been running, scheduling tasks. Bash itself doesn’t have a native date type, but the date command is powerful enough to cover nearly everything I need, from simple formatting to date arithmetic. This article walks through everything I’ve learned about handling dates and times in Bash scripts.
The date Command Basics
At its simplest, date with no arguments prints the current date and time:
date
Output:
Tue Jul 28 14:32:01 PKT 2026
Formatting Dates
The real power of date comes from format specifiers, passed after a +:
date +"%Y-%m-%d"
Output:
2026-07-28
Some format specifiers I use constantly:
| Specifier | Meaning | Example |
|---|---|---|
%Y | 4-digit year | 2026 |
%m | 2-digit month | 07 |
%d | 2-digit day | 28 |
%H | 24-hour hour | 14 |
%M | Minute | 32 |
%S | Second | 01 |
%A | Full weekday name | Tuesday |
%B | Full month name | July |
%s | Unix timestamp (seconds since epoch) | 1785331921 |
Combining several:
date +"%A, %B %d, %Y at %H:%M:%S"
Output:
Tuesday, July 28, 2026 at 14:32:01
Getting a Unix Timestamp
timestamp=$(date +%s)
echo "$timestamp"
This is one of the most useful things I do with date — a Unix timestamp is just an integer, which makes it trivial to do arithmetic, compare two points in time, or generate a guaranteed-unique filename.
Step-by-Step: Timestamping a Filename
#!/usr/bin/env bash
set -euo pipefail
FILENAME="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
tar -czf "$FILENAME" /data
echo "Created: $FILENAME"
Example output:
Created: backup_20260728_143201.tar.gz
Using %Y%m%d_%H%M%S produces a filename that sorts correctly alphabetically in the same order as chronologically, which I rely on constantly when listing backup directories.
Date Arithmetic
GNU date (the version on most Linux distributions) supports relative date calculations using the -d flag:
# Tomorrow
date -d "tomorrow" +%Y-%m-%d
# 7 days from now
date -d "+7 days" +%Y-%m-%d
# 30 days ago
date -d "-30 days" +%Y-%m-%d
# A specific date, 1 year later
date -d "2026-07-28 + 1 year" +%Y-%m-%d
Example output:
2026-07-29
2026-08-04
2026-06-28
2027-07-28
Note: macOS ships BSD date, which uses different flags entirely (-v instead of -d). If you need cross-platform scripts, this is one of the most common portability headaches in Bash.
# BSD/macOS equivalent for "7 days from now"
date -v+7d +%Y-%m-%d
Calculating the Difference Between Two Dates
#!/usr/bin/env bash
set -euo pipefail
start=$(date -d "2026-01-01" +%s)
end=$(date -d "2026-07-28" +%s)
diff_seconds=$((end - start))
diff_days=$((diff_seconds / 86400))
echo "Difference: $diff_days days"
Output:
Difference: 208 days
How This Works Internally
date +%sconverts the current (or specified) date into the number of seconds elapsed since the Unix epoch (January 1, 1970, 00:00:00 UTC). Because it’s just an integer, subtracting two timestamps directly gives you an elapsed duration in seconds.-dtellsdateto parse and operate on an arbitrary date string instead of “now,” and GNUdateincludes a fairly flexible natural-language parser capable of understanding phrases like"next monday"or"3 weeks ago".86400is simply the number of seconds in a day (24 × 60 × 60), used here to convert a seconds-based difference into a day count.
Real-World Use Case: Measuring Script Execution Time
#!/usr/bin/env bash
set -euo pipefail
start_time=$(date +%s)
# Simulate some work
sleep 3
echo "Doing some work..."
end_time=$(date +%s)
elapsed=$((end_time - start_time))
echo "Script took ${elapsed} seconds to run."
Output:
Doing some work...
Script took 3 seconds to run.
For sub-second precision, use date +%s%N for nanosecond timestamps, then divide as needed.
Real-World Use Case: Rotating Old Log Files by Age
#!/usr/bin/env bash
set -euo pipefail
LOG_DIR="/var/log/myapp"
DAYS_TO_KEEP=30
find "$LOG_DIR" -type f -name "*.log" -mtime "+${DAYS_TO_KEEP}" -exec rm {} \;
echo "Removed logs older than ${DAYS_TO_KEEP} days"
While this uses find‘s own -mtime flag rather than date directly, it’s a common companion pattern: date calculates cutoffs for reporting, while find -mtime handles filesystem-based age comparisons efficiently without needing to parse every file’s timestamp manually in a loop.
Automation Example: Generating a Weekly Date Range Report
#!/usr/bin/env bash
set -euo pipefail
echo "This week's dates:"
for i in {0..6}; do
date -d "monday this week + $i days" +"%A, %Y-%m-%d"
done
Output (assuming the script runs during the week of July 27, 2026):
Monday, 2026-07-27
Tuesday, 2026-07-28
Wednesday, 2026-07-29
Thursday, 2026-07-30
Friday, 2026-07-31
Saturday, 2026-08-01
Sunday, 2026-08-02
I use this pattern to generate weekly report headers or to loop through a date range when checking daily log files one by one.
Working with Time Zones
TZ="America/New_York" date +"%Y-%m-%d %H:%M:%S %Z"
TZ="Asia/Karachi" date +"%Y-%m-%d %H:%M:%S %Z"
Setting the TZ environment variable just for that single command (without permanently changing the system timezone) is a clean way to display the same moment in time across multiple time zones — useful for logging timestamps that need to be human-readable for teams in different regions.
To convert a Unix timestamp back into a readable date:
date -d @1785331921 +"%Y-%m-%d %H:%M:%S"
The @ prefix tells date to interpret the number as a Unix timestamp rather than a date string.
Security Considerations
- Be cautious when passing user-supplied strings directly into
date -d "$user_input". Whiledateitself doesn’t execute shell code, malformed or unexpected input can cause silent misparsing that leads to incorrect logic downstream (for example, in an access-control check based on dates). Validate the format before trusting it. - When timestamps are used for filenames in security-sensitive contexts (like audit logs), make sure the system clock itself is protected via NTP and not something a local user can trivially manipulate to obscure evidence.
Optimization Tips
- Avoid calling
daterepeatedly inside a tight loop if you just need the same “current time” reference throughout; capture it once into a variable at the top of the script. - For high-frequency timestamp generation,
date +%s%N(nanosecond epoch) inside a loop is faster than repeatedly formatting a full human-readable string, since string formatting has more overhead than a raw integer. - When comparing many dates, convert everything to Unix timestamps first (
date +%s) and do arithmetic comparisons rather than comparing formatted date strings, which is both faster and less error-prone.
Troubleshooting
date -ddoesn’t work the same on macOS: this is the BSD vs. GNUdatedivide. Install GNU coreutils via Homebrew (brew install coreutils) and usegdatefor GNU-compatible behavior on macOS.- “date: invalid date” error: usually means the format
date -dwas given doesn’t match any pattern it understands; try a more explicit format like"2026-07-28"instead of an ambiguous one like"07/28/2026". - Timezone looks wrong in output: check the
TZenvironment variable and the system’s configured timezone withtimedatectl(on systemd-based distributions). - Arithmetic on dates gives unexpected results near daylight saving time changes: when precision matters across a DST boundary, work in UTC (
date -u) to avoid the one-hour shifts entirely.
Common Mistakes to Avoid
- Assuming
date -dsyntax is identical on macOS and Linux — it isn’t, and this trips up almost everyone writing cross-platform scripts for the first time. - Comparing formatted date strings (like
"07/28/2026") with standard string comparison instead of converting to Unix timestamps first, which breaks for anything beyond simple equality checks. - Not accounting for time zones when timestamps from different systems or services need to be compared directly.
- Forgetting that
%sgives seconds, not milliseconds — a common source of “1000x too small” bugs when integrating with APIs that expect millisecond timestamps.
FAQs
How do I get the current date in milliseconds for an API call? Use $(( $(date +%s%N) / 1000000 )) to convert nanoseconds to milliseconds, since date doesn’t provide milliseconds directly.
How can I check if today is a weekday or weekend?
day_num=$(date +%u) # 1 = Monday ... 7 = Sunday
if [ "$day_num" -ge 6 ]; then
echo "Weekend"
else
echo "Weekday"
fi
Does Bash have any date functionality built in without calling date? Not really — Bash relies on the external date utility for anything beyond what $SECONDS (a built-in variable tracking seconds since the shell started) provides.
How do I make my date scripts work identically on Linux and macOS? Install GNU coreutils on macOS (brew install coreutils) and call gdate explicitly, or detect the OS at the top of your script and branch your date syntax accordingly.
Summary
The date command turns out to be one of the most versatile tools in Bash once you get past its slightly cryptic format specifiers. Between formatting, relative date arithmetic with -d, Unix timestamp conversions, and time zone handling, I can cover nearly every date-related task a script needs without reaching for a heavier scripting language — as long as I remember that GNU and BSD date speak two different dialects.
References
- GNU Coreutils
datedocumentation: https://www.gnu.org/software/coreutils/manual/html_node/date-invocation.html - Bash Reference Manual: https://www.gnu.org/software/bash/manual/bash.html
- IANA Time Zone Database: https://www.iana.org/time-zones
strftime(3)man page (format specifier reference): https://man7.org/linux/man-pages/man3/strftime.3.html
