Skip to content
shellhacks.dev
Shell Scripting

How to Use date on Both macOS and Linux Scripts Step by Step

The date command differs between macOS and Linux: -d versus -v, formatting flags, epoch conversion, and portable patterns for scripts that run on both.

6 min read
Summarize in AI
Illustration for "How to Use date on Both macOS and Linux Scripts Step by Step": a shell script in an editor, a loop arrow, a table of open ports.

Date arithmetic is where a "portable" script most often breaks. The format string after + is shared, but everything that reads or shifts a date is not. The commands below assume GNU coreutils 9.x on Linux and the date shipped with macOS, and they extend the portability themes in editing in place with sed.

What works everywhere

Shell
date +%Y-%m-%d
date +%s
date -u +%Y-%m-%dT%H:%M:%SZ
Text
2026-11-03
1793664000
2026-11-03T08:00:00Z

Printing the current time with a format string is portable. Use -u for UTC timestamps in logs and file names, since local time shifts by an hour twice a year. Stay away from %N for nanoseconds and %-d for non-padded days on a Mac; BSD date prints them literally or ignores the flag.

Relative dates

Shell
# GNU (Linux)
date -d "yesterday" +%Y-%m-%d
date -d "3 days ago" +%Y-%m-%d

# BSD (macOS)
date -v-1d +%Y-%m-%d
date -v-3d +%Y-%m-%d

GNU date parses natural phrases; BSD date adjusts with -v flags such as -v+1m for a month ahead. The adjustments are applied in order, so -v1d -v-1d gives the last day of the previous month. There is no overlap in the syntax, so a script has to branch.

Epoch to date and back

Shell
# GNU
date -d @1700000000 +%F
date -d "2026-01-15" +%s

# BSD
date -r 1700000000 +%F
date -j -f "%Y-%m-%d" "2026-01-15" +%s

On GNU, @ marks an epoch value. On BSD, -r takes seconds and -j -f parses a formatted string without setting the clock; forgetting -j on a Mac can try to change the system time and fail with a permission error. When parsing with -f, BSD fills the unspecified fields from the current time, so give a time as well if the result must be reproducible.

A wrapper that hides the difference

Shell
days_ago() {
  if date --version >/dev/null 2>&1; then
    date -d "$1 days ago" +%Y-%m-%d
  else
    date -v-"$1"d +%Y-%m-%d
  fi
}

days_ago 7

Test with --version, which only GNU accepts. Call sites then stay clean. An alternative is installing coreutils with brew install coreutils and calling gdate everywhere, which makes the script identical on both systems but demands that setup from every Mac user. For more on the command itself, see using date in real scripts.

Why the two dates diverged

GNU date comes from the coreutils project and grew a flexible parser for human phrases such as "next Friday" and "2 weeks ago". BSD date, which macOS inherits, follows older POSIX conventions: it takes an explicit format for parsing and an explicit adjustment for arithmetic. Neither is wrong, and neither is going to change, so a script that has to run on both needs a plan. The cost of ignoring the difference is not a crash in every case. Some commands succeed on both systems and return different answers, which is worse.

Option letters are the usual trap. The flag -d sets the date on a BSD system and parses a string on GNU, and -r prints a file's modification time on GNU and converts an epoch on BSD. Pasting a snippet from a forum answer without knowing which system it targets can therefore change a clock or print the wrong date without an error message. Read the manual page on the machine you are using before trusting any example.

Time zones and daylight saving

Most date bugs in scripts are zone bugs. A script that computes "midnight yesterday" in local time gets a 23-hour or 25-hour day around the clock change, and one that stores local times in files cannot order them reliably across the change. Store and compare UTC, and convert for display only. Setting TZ=UTC for the command, as in TZ=UTC date -d @1700000000, gives the same result on every machine regardless of its configuration, which also makes a test reproducible on a CI runner whose zone differs from a laptop's.

Be specific when a date has no time. A date such as 2026-01-15 turned into an epoch uses the current time of day on BSD when no time is given, and midnight in the local zone on GNU. The two answers differ by up to a day, and a script that compares them against a stored value fails only when it runs at certain hours. Always give both a date and a time, and a zone, when the result is going to be compared.

Leap years, month ends and the "last day" problem

Adding a month is not a fixed number of days. Starting from January 31 and adding one month gives different results depending on the implementation: GNU rolls forward to March 3 in a common year, while a library that clamps gives February 28. For billing or retention rules this ambiguity has real consequences, so write down the intended rule and test it on the 29th, 30th and 31st. To find the last day of a month portably, take the first day of the next month and subtract one day, using the epoch to avoid month-length arithmetic.

The same care applies to week numbers. ISO weeks start on Monday and week 1 is the one containing the first Thursday, so the last days of December can belong to week 1 of the next year. The format codes %V with %G give the ISO values, and %U with %Y gives a different and often unwanted count. Mixing the year from one scheme and the week from the other produces dates like week 53 of the wrong year in reports.

Choosing a strategy for shared scripts

There are three workable approaches. Branch on the platform with a small wrapper, as shown above, which keeps dependencies to zero. Require GNU tools everywhere and install them on the Mac, which gives identical behaviour at the price of a setup step. Or move the date logic into a language with a standard date library, so the shell only calls it. The best choice depends on who runs the script: a personal tool can require Homebrew packages, a script shipped to a team should minimise setup, and anything that runs in CI on a known image can rely on that image's tools. Document the choice at the top of the file so a new contributor knows what the script assumes.

Where this does not apply

If the dates are fixed by the data, skip shell arithmetic and use a language with a real date library, such as Python's datetime, where the code is the same on every system. Busybox date in Alpine containers supports only a subset of GNU options, so a script that works on Debian may still fail there. The references are the GNU date input formats and the FreeBSD date manual page.

If the script only needs "today minus N days" in a fixed format, there is a third option that avoids both flavours: compute from the epoch. Take now=$(date +%s), subtract N*86400, and format the result with the platform-specific epoch flag shown above. Daylight saving changes make a day 23 or 25 hours long in local time, so do this arithmetic in UTC with TZ=UTC when exact calendar days matter. See also calendars and time zones for the zone side of the problem.

Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.