Read a crontab
Problem statement
Read a crontab and print each job's schedule in plain words, flagging the job that runs every minute, which is almost always a mistake. Cron runs backups, reports and cleanups on nearly every server, and misreading the five time fields is how a nightly job ends up running 1,440 times a day.
crontab.txt
# m h dom mon dow command*/5 * * * * /opt/app/health.sh30 2 * * * /opt/app/backup.sh0 9 * * 1-5 /opt/app/report.sh* * * * * /opt/app/cleanup.sh0 0 1 * * /opt/app/invoice.sh@reboot /opt/app/start.shFor each job, print the script name and its schedule in words. Mark the every-minute job with a warning.
Expected output:
health.sh every 5 minutesbackup.sh every day at 02:30report.sh weekdays at 09:00cleanup.sh EVERY MINUTE <-- is that intended?invoice.sh day 1 of every month at 00:00start.sh once, when the machine bootsHints
* means "every", */5 means "every 5th", and 1-5 in the last field means Monday to Friday.Approach
Optimal: Cron fields with awk
Covers: the five cron fields, *, */N, ranges 1-5, lists 1,15, @reboot and other shortcuts, crontab -l, awk tests on several fields, printf "%02d".
Five fields, then the command. Each crontab line is a schedule and a command:
30"]:::blue L --> H["hour
2"]:::blue L --> D["day of month
*"]:::gray L --> MO["month
*"]:::gray L --> W["day of week
*"]:::gray L --> C["command"]:::green classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px
| Field | Allowed | Example | Means |
|---|---|---|---|
| minute | 0-59 | 30 |
at minute 30 |
| hour | 0-23 | 2 |
at 2 am |
| day of month | 1-31 | 1 |
on the 1st |
| month | 1-12 | * |
every month |
| day of week | 0-7 (0 and 7 are Sunday) | 1-5 |
Monday to Friday |
| Symbol | Means | Example |
|---|---|---|
* |
every value | * in hour = every hour |
*/N |
every Nth value | */5 in minute = every 5 minutes |
a-b |
a range | 1-5 = Monday to Friday |
a,b |
a list | 0,30 = at :00 and :30 |
A job runs when all fields match the current time. So 30 2 * * * means minute 30 and hour 2, on any day: 02:30 every night.
The classic mistakes.
every minute"]:::red ~~~ B2["* 2 * * *
60 times from 02:00"]:::red end subgraph GOOD["what was meant"] direction TB G1["0 * * * *
once an hour"]:::green ~~~ G2["0 2 * * *
once at 02:00"]:::green end BAD ~~~ GOOD classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px style BAD fill:transparent,stroke:#dc2626,stroke-width:2px style GOOD fill:transparent,stroke:#059669,stroke-width:2px
* * * * *runs every minute. People write it while testing and forget to change it.* 2 * * *runs every minute from 02:00 to 02:59, 60 times, not once at 2 am. The minute must be set too:0 2 * * *.- If both day of month and day of week are set, the job runs when either matches, not both.
Special shortcuts. @reboot runs once when the machine starts. @daily, @hourly and @weekly are short forms of common schedules (@daily is 0 0 * * *).
Walking through the code. The # Setup: lines only save the sample crontab, so skip past them.
- awk skips comments and empty lines, and splits the script name off the command path with
splitandn. - The tests run from most specific to least:
@reboot, all five*,*/Nin the minute, then fixed minute and hour with different day fields. printf "%02d:%02d"prints hours and minutes with a leading zero, like02:30.
Edge cases. This reader only covers common shapes, and prints custom schedule for anything else, such as lists or ranges in the minute field. Cron uses the server's own time zone. A system crontab (/etc/crontab, /etc/cron.d/*) has an extra user column before the command, which shifts the fields by one.
# Setup: save the sample crontab in a fresh temporary folder (on a server: crontab -l)
cd "$(mktemp -d)"
cat > crontab.txt << 'CRON'
# m h dom mon dow command
*/5 * * * * /opt/app/health.sh
30 2 * * * /opt/app/backup.sh
0 9 * * 1-5 /opt/app/report.sh
* * * * * /opt/app/cleanup.sh
0 0 1 * * /opt/app/invoice.sh
@reboot /opt/app/start.sh
CRON
awk '
/^[[:space:]]*#/ || NF == 0 { next }
{
cmd = ($1 == "@reboot") ? $2 : $6
n = split(cmd, part, "/"); name = part[n] # just the script name
if ($1 == "@reboot") s = "once, when the machine boots"
else if ($1 == "*" && $2 == "*" && $3 == "*" && $4 == "*" && $5 == "*")
s = "EVERY MINUTE <-- is that intended?"
else if ($1 ~ /^\*\/[0-9]+$/ && $2 == "*") s = "every " substr($1, 3) " minutes"
else if ($3 == "*" && $4 == "*" && $5 == "*") s = sprintf("every day at %02d:%02d", $2, $1)
else if ($3 == "*" && $4 == "*" && $5 == "1-5") s = sprintf("weekdays at %02d:%02d", $2, $1)
else if ($4 == "*" && $5 == "*") s = sprintf("day %d of every month at %02d:%02d", $3, $2, $1)
else s = "custom schedule"
printf "%-12s %s\n", name, s
}
' crontab.txtInterview follow-ups
Write the backup job as a systemd timer instead.
A timer needs two files.
backup.servicehas[Service] Type=oneshot ExecStart=/opt/app/backup.sh.backup.timerhas[Timer] OnCalendar=*-*-* 02:30:00andPersistent=true, and[Install] WantedBy=timers.target. Enable it withsystemctl enable --now backup.timer. Timers log to the journal, can catch up on runs missed while the machine was off (Persistent=true), andsystemctl list-timersshows when each will run next.
Frequently asked questions
Cron runs jobs with a tiny environment: a short PATH (often just /usr/bin:/bin), no login files, no terminal, and the user's home as the working folder. So commands like aws or node in /usr/local/bin are not found, and relative paths point somewhere else. Use full paths in the script, set PATH at the top of the crontab, and cd to the right folder first. Send output to a log: ... >> /var/log/job.log 2>&1, so you can see the error.
crontab -e edits your own crontab safely and checks the syntax when you save; crontab -l lists it. They are stored under /var/spool/cron, but you should not edit those files directly. System jobs live in /etc/crontab and /etc/cron.d/, which have an extra user field, and in /etc/cron.daily and friends, which hold plain scripts run by run-parts. Check all of them when hunting for a mystery job.
In a crontab line, % means "new line", and everything after the first % is sent to the command as input. So date +%F in a crontab is cut off at the %. Escape it as \%, like date +\%F, or move the command into a script file and call the script from cron. The script approach is cleaner and easier to test.