Classic Crontabs - ptweezy/cronstable GitHub Wiki
cronstable reads classic (Vixie-style) crontabs in the
m h dom mon dow command format described by man 5 crontab. Pass a
crontab to -c, place it beside YAML files in a config directory, or load
it with include:.
Entries use cronstable's defaults: UTC schedules, stderr and exit-status
failure detection, concurrencyPolicy: Allow, and no retries. Cron's
environment and mail behavior are not emulated.
See deviations from cron before migrating an
existing crontab. The loader is implemented in cronstable/crontabs.py
and cronstable/config.py.
The file name decides whenever it can:
| Name | Treated as |
|---|---|
*.crontab, *.cron (case-insensitive) |
classic crontab |
a file named exactly crontab (case-insensitive), for example, a crontab -l > crontab export |
classic crontab |
*.yml, *.yaml
|
YAML, always; never content-sniffed |
anything else, passed explicitly with -c or pulled in with include:
|
content-sniffed (described later) |
| anything else, inside a config directory | skipped |
Name recognition also fires on /etc/crontab, but system crontabs
(/etc/crontab, /etc/cron.d) carry a sixth user column that cronstable
does not parse. Only the five-field user-crontab format runs as-is (see
deviations).
In a config directory, crontab-named files load right alongside
*.yml/*.yaml files, in the same name-sorted order. The usual skip rule
still applies: entries whose name starts with _ or . are ignored (see
includes, defaults, and multi-file config).
The content sniff exists so that cronstable -c /var/spool/cron/crontabs/root
works even though the file has no telling name. It looks at the first
meaningful (non-blank, non-comment) line only, and accepts only shapes no
valid cronstable YAML document can open with: a NAME=value assignment, a
line starting with @, or five valid cron fields followed by a command.
Anything inconclusive is parsed as YAML, so an extensionless YAML config is
never mistaken for a crontab. When in doubt, name the file *.crontab and the
question never arises.
A YAML configuration can also pull a crontab in directly:
include:
- legacy.crontabcronstable -v -c legacy.crontab validates a crontab the same way it validates
YAML. Parse errors are reported with the offending file:line. A runnable
example mixing a crontab with a YAML file (and the web dashboard) ships in
the repository as example/crontab.
The user-crontab format from man 5 crontab:
# comments and blank lines are ignored
SHELL=/bin/bash
PATH = /usr/local/bin:/usr/bin:/bin
MAILTO="[email protected]"
# m h dom mon dow command
*/15 * * * * /usr/local/bin/backup --incremental
30 4 * * mon-fri /usr/local/bin/report --daily
0 0 1 jan * /usr/local/bin/happy-new-year
CRON_TZ=Europe/Berlin
0 6 * * * echo "6am in Berlin, not UTC"
@daily /usr/local/bin/rotate-logs
@reboot echo "cronstable started"
0 0 * * * pg_dump mydb > /backup/mydb-$(date +\%F).sqlSpecifically:
-
Entries: five time fields, then the rest of the line is the command.
Ranges (
1-5), steps (*/5), lists (1,15,30), and month/weekday names (jan,mon-fri) are supported. Day-of-week accepts both0and7as Sunday. The fields go through the same built-in cron engine that parses YAMLschedulestrings, so both formats share one field dialect; a crontab entry is always five fields (no year or seconds column), and only crontabs accept@midnight(see schedules and time zones). -
Nicknames:
@reboot,@yearly,@annually,@monthly,@weekly,@daily,@midnight,@hourly.@midnightis rewritten to its synonym@dailyat load time.@rebootbehaves exactly like a YAML@rebootschedule: it runs once at startup, and it understands leadership under clustering. -
Environment assignments:
NAME = valuelines apply to the entries below them, exactly as in cron. A later reassignment affects later entries only. Values may be single- or double-quoted to preserve leading or trailing blanks. All assignments are exported to the job's environment, on top of the environment cronstable itself runs with. -
Escaped percent signs:
\%in a command becomes a literal%, so the ubiquitousdate +\%Fidiom works unchanged. An unescaped%is a load-time error; see deviations.
Two assignments are interpreted as well as exported:
| Variable | Effect |
|---|---|
SHELL |
Sets the job's shell option, so the command runs as $SHELL -c "command", as in cron. Without it, cronstable's standard default applies (/bin/sh on POSIX, the native command processor on Windows). On Windows, a SHELL naming an absolute POSIX path (SHELL=/bin/sh, as /etc/crontab exports always carry) is a load-time error at the assignment's line, because that shell cannot exist there and every entry below it would fail at spawn. A bare name (SHELL=powershell) is kept. A POSIX-style PATH= assignment is kept but warned about on Windows, because it replaces the Windows PATH for the entries below it. See running on Windows. |
CRON_TZ |
Sets the job's timezone option: schedules below it are evaluated in that IANA zone (cronie's CRON_TZ semantics). An unknown zone is a load-time error at the assignment's line. |
Every entry is lowered to a plain job definition, merged over the same
built-in defaults as a YAML job (DEFAULT_CONFIG), and validated by the
same JobConfig code path. From that point on, nothing downstream can tell
the two formats apart: crontab jobs appear in the
web dashboard and HTTP API, participate in the
job-set fingerprint and
clustering, and report failures like any
other job.
The following defaults matter most for a migrated crontab. Each row names a behavior (with the per-job YAML option behind it), what the entry does now that cronstable runs it, and what the same line did under classic cron:
| Behavior | Under cronstable | Under classic cron |
|---|---|---|
time basis (utc / timezone) |
UTC (set CRON_TZ to change) |
local time |
failure detection (failsWhen) |
nonzero exit or any stderr output is a failure | exit status ignored; output mailed |
output (captureStderr / captureStdout) |
stderr is read by cronstable (for failure detection, reports, and the dashboard log tail) and re-emitted into its log with a [<job> stderr] prefix. By contrast, stdout is not read: it flows straight through to cronstable's own stdout, visible there but not to reports or the dashboard |
both mailed to MAILTO
|
concurrency (concurrencyPolicy) |
Allow (overlapping runs permitted) |
overlapping runs permitted |
retries (onFailure.retry) |
none | none |
user (user) |
the user cronstable runs as | the crontab's owner |
For reporting, retries, timeouts, or other per-job options, move the entry
to YAML and use the configuration reference.
A defaults: section in a sibling or including YAML file does not
apply to crontab entries; defaults are scoped to their file (see
includes, defaults, and multi-file config).
Entries are named <file name>:<line number>, for example
legacy.crontab:9. The name is unique within a file and stable across
reloads while the file is unchanged. It appears in logs, the dashboard, and
the HTTP API like any other job name, and points you straight at the source
line. Inserting or removing lines renumbers the entries below the edit, which
cronstable treats the same way as renaming a YAML job: the old name's run
history ends and the new name starts fresh.
Check these differences when migrating:
-
Schedules default to UTC, not local time. Put
CRON_TZ=<zone>above the entries that need a specific zone. -
MAILTOdoes not send mail. It is exported to the job's environment but not interpreted. Configure reporting in YAML to receive email. Failures remain visible in logs, the dashboard, and the HTTP API. stderr is captured; stdout passes through to cronstable's stdout (see the preceding table). -
An unescaped
%is a load-time error, not stdin. In cron,%ends the command, and everything after it is fed to the command as standard input. cronstable does not support that syntax. The escaped form\%(such asdate +\%F) works as in cron. To supply stdin, use a YAML job with a heredoc or file redirect. -
The system-crontab user column is not parsed.
/etc/crontaband/etc/cron.dfiles carry a sixth field naming the user to run as. A parser cannot reliably tell that column from the first word of a command, so cronstable reads the five-field user-crontab format only. A user column would land at the start of the command and typically fail withroot: command not foundat run time. Remove the column, or move the entry to YAML and use theuser:option (commands and environment). -
Cron's implicit environment is not injected. cron gives jobs a
near-empty environment with
LOGNAME,HOME, andSHELL=/bin/shdefaults. Under cronstable, jobs inherit cronstable's own environment plus the crontab's assignments, the same rule as YAML jobs. A crontab that relied on cron's minimalPATHbehaves the same after it setsPATH=itself, as most already do.
When an entry outgrows the crontab format, its YAML equivalent is mechanical. This entry:
CRON_TZ=Europe/Berlin
SHELL=/bin/bash
30 4 * * mon-fri /usr/local/bin/report --dailyis exactly:
jobs:
- name: report
command: /usr/local/bin/report --daily
shell: /bin/bash
schedule: "30 4 * * mon-fri"
timezone: Europe/Berlinplus whatever per-job options prompted the move. Both forms can coexist in one config directory for as long as the migration takes.