How to Work with Dates and Times in Bash

How to Work with Dates and Times in Bash

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:

SpecifierMeaningExample
%Y4-digit year2026
%m2-digit month07
%d2-digit day28
%H24-hour hour14
%MMinute32
%SSecond01
%AFull weekday nameTuesday
%BFull month nameJuly
%sUnix 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 +%s converts 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.
  • -d tells date to parse and operate on an arbitrary date string instead of “now,” and GNU date includes a fairly flexible natural-language parser capable of understanding phrases like "next monday" or "3 weeks ago".
  • 86400 is 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". While date itself 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 date repeatedly 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 -d doesn’t work the same on macOS: this is the BSD vs. GNU date divide. Install GNU coreutils via Homebrew (brew install coreutils) and use gdate for GNU-compatible behavior on macOS.
  • “date: invalid date” error: usually means the format date -d was 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 TZ environment variable and the system’s configured timezone with timedatectl (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 -d syntax 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 %s gives 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 date documentation: 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
Total
2
Shares

Leave a Reply

Previous Post
How to Generate Random Numbers in Bash

How to Generate Random Numbers in Bash

Next Post
How to Access System Information in Bash

How to Access System Information in Bash

Related Posts