Between the two commands in this guide, I use date constantly — in scripts, logs, cron expressions, filenames — and cal occasionally, mostly when I want a quick visual calendar without opening anything else. They seem like small, boring utilities, but date in particular has a genuinely deep feature set once you get past date with no arguments, and it’s worth knowing well because so much shell scripting depends on formatting and calculating dates correctly.
cal: Displaying a Calendar
cal prints a simple text calendar to the terminal. It’s not installed by default on every minimal system or container image — it typically comes from the util-linux package (as cal) or bsdmainutils/ncal package depending on distribution, so if you get a “command not found,” a quick sudo apt install util-linux or sudo apt install ncal (Debian/Ubuntu) usually resolves it.
Basic Usage
cal
This prints the current month, with today’s date typically highlighted, in a layout like:
July 2026
Su Mo Tu We Th Fr Sa
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Common Options
cal 2026
Prints all twelve months of the specified year in a grid layout.
cal 7 2026
Prints a specific month (July) and year.
cal -3
Shows the previous, current, and next month side by side — genuinely useful when you’re planning something that spans a month boundary.
cal -y
Shows the entire current year (equivalent to cal $(date +%Y)).
cal -m monday
On many cal implementations, this changes the calendar to start weeks on Monday instead of Sunday, matching ISO 8601 convention used in most of the world outside the US.
Related: ncal
Some distributions provide ncal, a variant with a different (often more compact, vertical) layout and additional options like -w to show week numbers:
ncal -w
cal is genuinely one of the simplest commands in this whole guide — there’s not much internal complexity to it. It reads the system’s current date (ultimately from the same kernel time source date uses) and formats a static grid. Its real value is purely visual/human convenience, not scripting — for anything script-related involving dates, date is the tool you actually want.
date: The Real Workhorse
date displays or sets the system date and time, and supports an extensive set of format specifiers that make it one of the most frequently used utilities in shell scripting.
Basic Usage
date
Fri Jul 31 01:38:14 UTC 2026
Custom Output Formatting
The real power of date is its +FORMAT syntax, where you supply a format string built from %-prefixed specifiers:
date +"%Y-%m-%d %H:%M:%S %Z"
2026-07-31 01:38:14 UTC
date +"%A, %B %d, %Y"
Friday, July 31, 2026
Common format specifiers:
| Specifier | Meaning |
|---|---|
%Y | 4-digit year |
%y | 2-digit year |
%m | Month (01–12) |
%d | Day of month (01–31) |
%H | Hour, 24-hour format |
%I | Hour, 12-hour format |
%M | Minute |
%S | Second |
%A | Full weekday name |
%a | Abbreviated weekday name |
%B | Full month name |
%b | Abbreviated month name |
%j | Day of year (001–366) |
%Z | Timezone abbreviation |
%s | Unix epoch timestamp (seconds since 1970-01-01 UTC) |
%N | Nanoseconds |
%u | Day of week (1 = Monday .. 7 = Sunday, ISO 8601) |
Checking the current epoch timestamp — extremely common in logging and scripting:
date +"%s"
1785462236
Getting the day-of-year for a specific date, without needing to count manually:
date -d "2026-01-01" +"%j"
001
Calculating Relative Dates with -d / –date
This is where date becomes genuinely powerful for scripting — it understands human-readable relative date expressions:
date -d "next monday"
Mon Aug 3 00:00:00 UTC 2026
date --date="1 week ago"
Fri Jul 24 01:43:56 UTC 2026
Other expressions that work: "tomorrow", "yesterday", "2 days", "3 months", "next year", "last friday", and combinations like "2026-01-01 + 90 days". This is a huge time-saver over manually calculating date math with %s epoch arithmetic, though epoch math is still the right tool when you need exact, unambiguous calculations across timezone/DST boundaries.
Timezone Handling
date -u
Fri Jul 31 01:43:56 UTC 2026
Prints the current time in UTC regardless of the system’s configured local timezone — the -u/--utc flag is essential for consistent logging across distributed systems in different timezones.
TZ="America/New_York" date
Thu Jul 30 21:43:56 EDT 2026
Temporarily overrides the timezone for a single command by setting the TZ environment variable inline, without changing the system’s actual configured timezone.
Setting the System Date and Time
sudo date -s "2026-08-01 09:00:00"
Directly sets the system clock. In practice, on any modern system with NTP (Network Time Protocol) synchronization active, manually setting the date this way is usually temporary — the NTP daemon will correct it back on the next sync. For permanent timezone or sync configuration, the correct tool on systemd-based distributions is timedatectl:
timedatectl status
timedatectl set-timezone America/New_York
timedatectl set-ntp true
Note that timedatectl requires the system to be running under systemd as PID 1 — it won’t function inside minimal containers or non-systemd init systems, where you’d fall back to manually editing /etc/timezone and /etc/localtime (a symlink into /usr/share/zoneinfo/), or using ntpdate/chronyd directly.
ISO 8601 and Standardized Output
date -I
2026-07-31
date --iso-8601=seconds
2026-07-31T01:38:14+00:00
date --rfc-3339=seconds
Produces RFC 3339 formatted output, similar to ISO 8601 but with a space instead of T separating date and time — the format many logging systems and APIs expect.
Practical Sysadmin Use Cases
Timestamping backup filenames:
tar -czf backup_$(date +%Y%m%d_%H%M%S).tar.gz /etc
Produces something like backup_20260731_014300.tar.gz — sortable by filename, unambiguous, no spaces.
Logging with consistent, greppable timestamps in a script:
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
log "Starting deployment"
Calculating a certificate or log rotation cutoff date:
cutoff=$(date -d "30 days ago" +%Y-%m-%d)
find /var/log/myapp -name "*.log" -newermt "$cutoff" -delete
Measuring script execution duration using epoch math:
start=$(date +%s)
# ... work happens here ...
end=$(date +%s)
echo "Elapsed: $((end - start)) seconds"
Cross-timezone coordination for scheduled maintenance:
TZ="Asia/Tokyo" date -d "2026-08-15 09:00:00 UTC"
Troubleshooting
date -dexpression not parsing correctly → GNUdate(Linux) and BSDdate(macOS) have genuinely different-d/-vsyntax; a script written and tested on macOS often breaks on Linux and vice versa. Always test date arithmetic directly on the target platform.- Wrong timezone shown despite correct
/etc/timezone→ check whetherTZis set in the environment, since an explicitTZvariable overrides the system default for that session. calnot found → installutil-linux(usually already present) orncal/bsdmainutils, depending on distribution.- System clock drifting → check
timedatectl statusfor NTP sync state, orchronyc trackingif usingchronyinstead ofsystemd-timesyncd.
Security Implications
Accurate system time matters more for security than people usually assume: TLS certificate validation depends on correct system time (a clock too far in the past or future can cause valid certificates to be rejected, or expired ones to be silently accepted), and many authentication protocols (like Kerberos, and TOTP-based two-factor authentication) have strict time-synchronization tolerances — a drifted clock can cause legitimate logins to fail or, in some misconfigured cases, weaken security assumptions. Keeping NTP sync active and monitored is a genuinely important, if unglamorous, security hygiene item.
date/cal vs Related Tools
| Tool | Purpose |
|---|---|
date | Display/format/calculate dates and times; set system clock |
cal | Human-readable visual calendar display only |
timedatectl | Manage system time, timezone, and NTP sync (systemd systems) |
hwclock | Read/set the hardware (BIOS/RTC) clock directly, independent of the OS |
chronyc / ntpq | Inspect and manage NTP synchronization state |
Compatibility Across Distributions
date is provided by GNU coreutils on virtually every Linux distribution (Debian, Ubuntu, RHEL, Fedora, Arch, openSUSE), so its extensive -d/+FORMAT feature set is consistent across all of them. cal availability varies more — it’s standard on most desktop-oriented installs but sometimes absent from minimal server or container images, requiring a manual install. Be aware that macOS and BSD systems ship a fundamentally different (non-GNU) date implementation with different relative-date syntax (-v flags instead of -d strings) — scripts intended to be portable across Linux and macOS need to account for this difference explicitly, often by detecting the OS and branching, or using a portable epoch-based approach instead.
Shell Scripting Patterns Worth Knowing
A few patterns come up often enough in real automation work that they’re worth having memorized rather than looked up each time.
Generating a range of dates for a report:
for i in {0..6}; do
date -d "$i days ago" +%Y-%m-%d
done
This produces the last seven calendar days in order, which is a common building block for generating date-partitioned report filenames or querying a week’s worth of logs one day at a time.
Comparing two dates numerically:
d1=$(date -d "2026-01-01" +%s)
d2=$(date -d "2026-06-15" +%s)
diff_days=$(( (d2 - d1) / 86400 ))
echo "Difference: $diff_days days"
Converting both dates to epoch seconds first sidesteps all the messiness of manually accounting for month lengths, leap years, and daylight saving transitions — the kernel’s date arithmetic already handles all of that correctly, so leaning on %s for comparisons is far more reliable than trying to do calendar math by hand in a script.
Validating that a string is actually a well-formed date before using it:
if date -d "$user_input" >/dev/null 2>&1; then
echo "Valid date"
else
echo "Invalid date string"
fi
This is a genuinely useful guard in any script that accepts a date as a parameter from a user or config file, since date -d will happily fail loudly (and predictably, via a non-zero exit code) on garbage input rather than silently producing something wrong.
A Note on Locale Behavior
Both cal and date are locale-aware, meaning weekday names, month names, and the first day of the week can change depending on the system’s configured locale (LANG/LC_TIME environment variables). A server configured with LC_TIME=de_DE.UTF-8, for instance, will print German month and day names by default. This is worth knowing if a script’s output looks unexpectedly “wrong” on one server but not another — it’s very often a locale difference, not a bug. You can force a specific, predictable locale for a single command without changing the system default:
LC_TIME=C date +"%A, %B %d, %Y"
Setting LC_TIME=C (the “POSIX”/default locale) guarantees consistent, English, unambiguous output — a good habit for scripts and logs that might run on differently-configured systems, where locale-dependent formatting could otherwise silently break downstream parsing.
Handling Leap Years and Edge Cases
Date arithmetic edge cases are exactly where manual calculation goes wrong and date -d proves its worth. Leap years, month-end rollovers, and daylight saving transitions are all handled correctly by GNU date without any special-casing on your part:
date -d "2028-02-29 + 1 year" +%Y-%m-%d
2029-03-01
Notice that adding exactly one year to a leap-day date correctly rolls forward to March 1st in a non-leap year, rather than producing an invalid February 29th — this is the kind of edge case that’s easy to get subtly wrong in a hand-rolled date calculation, and exactly why offloading date math to date (or a proper date-handling library in a scripting language, for more complex applications) is worth doing rather than reimplementing calendar logic yourself.
Summary
cal is a small convenience for visually checking a calendar in the terminal, while date is genuinely one of the most-used tools in day-to-day Linux scripting — from timestamped filenames and log lines to relative date math and timezone-aware calculations. Learning its +FORMAT specifiers and -d relative date syntax pays off constantly, since nearly every non-trivial shell script eventually needs to know or calculate “what time is it, in what format, relative to what.”
References
man 1 dateman 1 cal- GNU coreutils manual: https://www.gnu.org/software/coreutils/manual/coreutils.html
man 1 timedatectl- IANA Time Zone Database: https://www.iana.org/time-zones
