Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
systab is a single-file Bash script that provides a cron/at/batch-like interface for systemd user timers. It creates, manages, and cleans up systemd .service and .timer unit files in ~/.config/systemd/user/. Managed units are tagged with a # SYSTAB_MANAGED marker comment. Unit filenames use a 6-char hex ID (e.g., systab_a1b2c3.timer) which doubles as the human-facing job identifier.
Running
./systab [OPTIONS]
No build step. The script requires bash, systemctl, and optionally notify-send (for -i) and sendmail/msmtp (for -m).
Architecture
The script has two modes controlled by CLI flags:
-
Job creation (
-t <time> [-n <name>] [-c <cmd> | -f <script> | stdin]or-s [-n <name>] [-c <cmd> | -f <script> | stdin]): Generates a systemd unit pair (or single unit for services) with a 6-char hex short ID, reloads the daemon, and enables/starts the unit. An optional-n <name>assigns a human-readable name that can be used interchangeably with hex IDs in all operations.- Timer jobs (
-t): Creates a.service+.timerpair. Time specs are parsed viaparseTimewhich handles natural language (every 5 minutes),date -drelative/absolute times, and raw systemd OnCalendar values. One-time jobs getPersistent=falseandRemainAfterElapse=no(auto-unload after firing). Notifications (-idesktop,-memail,-oinclude output) useExecStopPostso they fire on both success and failure with status-aware icons/messages. The-o [N]flag fetches the last N lines of journal output (default 10). Notification flags are persisted in the service file as a# SYSTAB_FLAGS=comment. - Service jobs (
-s): Creates a single.servicefile withType=simple,Restart=on-failure, andWantedBy=default.target. No.timerfile is created. Adaemon-reloadruns beforeenable/startso systemd registers the unit first. Service jobs are tagged with# SYSTAB_TYPE=servicein the service file. Mutually exclusive with-t,-i,-m,-o.
- Timer jobs (
-
Management (
-D,-E,-e,-L,-S,-C,-h— mutually exclusive):-D <id|name>/-E <id|name>: Disable (stop+disable) or enable (enable+start) a job's timer. Accepts hex ID or name.-e: Opens$EDITORwith a pipe-separated crontab (ID[:FLAGS] | SCHEDULE | COMMAND). Flags are appended to the ID with:(s= service,i= desktop,e=addr= email,o= output 10 lines,o=N= output N lines,n=name= job name, comma-separated). Service jobs appear withserviceas the schedule column. On save, diffs against the original to apply creates (ID=new), deletes (removed lines), updates (changed schedule/command/flags), and disable/enable (comment/uncomment lines). Theserviceschedule keyword skipsparseTimevalidation and routes to_writeUnitFileswithjob_type=service.-L [id|name] [filter]: Queryjournalctllogs for managed jobs (both unit messages and command output). Optional job ID or name to filter to a single job.-S [id|name]: Show timer status viasystemctl, including short IDs, names, and disabled state. Optional job ID or name to show a single job.-C: Interactively clean up elapsed one-time timers (removes unit files from disk).
Key functions: parseTime (time spec → OnCalendar), _writeUnitFiles (shared service+timer creation; 4th param job_type selects timer vs service path), createJob/createJobFromEdit (thin wrappers), editJobs (crontab-style edit with diff-and-apply), getManagedUnits (find tagged units by type), getManagedServiceJobs (find service-only jobs by # SYSTAB_TYPE=service marker), isJobService (detect service vs timer job), cleanJobs (remove elapsed one-time timers), disableJob/enableJob (stop+disable / enable+start, branching on isJobService), writeNotifyLines (append ExecStopPost notification lines), buildFlagsString/parseFlags (convert between CLI options and flags format, including s flag), resolveJobId (resolve hex ID or name to hex ID, accepts service-only jobs).
Testing
./test.sh
Runs 81 tests against real systemd user timers and services covering job creation, service creation, job names, status, logs, disable/enable, notifications, time format parsing, error cases, and cleanup. All test jobs are cleaned up automatically via trap.
Tests require a real systemd user session (systemctl --user) and cannot run in containers. CI runs ShellCheck only; tests are enforced locally via a pre-commit hook that also updates badges/tests.json.
After cloning, enable the hooks: git config core.hooksPath .githooks
Notes
- ShellCheck can be used for linting:
shellcheck systab. - Edit mode uses
|as the field delimiter (not tabs or spaces) to allow multi-word schedules. Flags use:after the ID (e.g.,a1b2c3:n=backup,i,o,e=user@host). Service jobs use the literal stringserviceas the schedule column. - Flags (
s= service,i= desktop,o/o=N= include output,e=addr= email,n=name= job name) are persisted as# SYSTAB_FLAGS=...comments in service files. Names are additionally stored as# SYSTAB_NAME=...comments. Service jobs are additionally tagged with# SYSTAB_TYPE=service.ExecStopPost=lines use$SERVICE_RESULT/$EXIT_STATUSfor status-aware messages. Unit fileprintfformat strings must use%%s(not%s) since systemd expands%sas a specifier before the shell runs. - Journal logs are queried with
USER_UNITORSYSLOG_IDENTIFIERto capture both systemd messages and command output. showStatususessystemctl show -p ActiveState,SubState(notis-active) for service jobs to correctly report states likeactivatingthatis-activewould misreport as inactive.daemon-reloadmust run beforeenable/startfor new service units; it is called inside_writeUnitFilesfor the service path.