Add -o flag to include job output in notifications
Fetches the last N lines of journal output (default 10) via journalctl and includes them in desktop/email notification bodies. Supports CLI (-o 5) and edit mode (o or o=5 flag syntax). Also fixes systemd %s specifier expansion in email printf format strings (must use %%s so systemd passes %s through to the shell). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c3c534f7ce
commit
67528374cd
3 changed files with 46 additions and 12 deletions
|
|
@ -18,11 +18,11 @@ No build step. The script requires `bash`, `systemctl`, and optionally `notify-s
|
|||
|
||||
The script has two modes controlled by CLI flags:
|
||||
|
||||
- **Job creation** (`-t <time> [-c <cmd> | -f <script> | stdin]`): Generates a systemd `.service` + `.timer` pair with a 6-char hex short ID, reloads the daemon, and enables/starts the timer. Time specs are parsed via `parse_time` which handles natural language (`every 5 minutes`), `date -d` relative/absolute times, and raw systemd OnCalendar values. One-time jobs get `Persistent=false` and `RemainAfterElapse=no` (auto-unload after firing). All jobs log stdout/stderr to the journal via `SyslogIdentifier`. Notifications (`-i` desktop, `-m` email) use `ExecStopPost` so they fire on both success and failure with status-aware icons/messages. Notification flags are persisted in the service file as a `# SYSTAB_FLAGS=` comment.
|
||||
- **Job creation** (`-t <time> [-c <cmd> | -f <script> | stdin]`): Generates a systemd `.service` + `.timer` pair with a 6-char hex short ID, reloads the daemon, and enables/starts the timer. Time specs are parsed via `parse_time` which handles natural language (`every 5 minutes`), `date -d` relative/absolute times, and raw systemd OnCalendar values. One-time jobs get `Persistent=false` and `RemainAfterElapse=no` (auto-unload after firing). All jobs log stdout/stderr to the journal via `SyslogIdentifier`. Notifications (`-i` desktop, `-m` email, `-o <lines>` include output) use `ExecStopPost` so they fire on both success and failure with status-aware icons/messages. The `-o` flag fetches the last N lines of journal output (default 10) and includes them in the notification body. Notification flags are persisted in the service file as a `# SYSTAB_FLAGS=` comment.
|
||||
|
||||
- **Management** (`-P`, `-R`, `-E`, `-L`, `-S`, `-C` — mutually exclusive):
|
||||
- `-P <id>` / `-R <id>`: Pause (stop+disable) or resume (enable+start) a job's timer.
|
||||
- `-E`: Opens `$EDITOR` with a pipe-separated crontab (`ID[:FLAGS] | SCHEDULE | COMMAND`). Notification flags are appended to the ID with `:` (`i` = desktop, `e=addr` = email, comma-separated for both). On save, diffs against the original to apply creates (ID=`new`), deletes (removed lines), updates (changed schedule/command/flags), and pause/resume (comment/uncomment lines).
|
||||
- `-E`: Opens `$EDITOR` with a pipe-separated crontab (`ID[:FLAGS] | SCHEDULE | COMMAND`). Notification flags are appended to the ID with `:` (`i` = desktop, `e=addr` = email, `o` = output 10 lines, `o=N` = output N lines, comma-separated). On save, diffs against the original to apply creates (ID=`new`), deletes (removed lines), updates (changed schedule/command/flags), and pause/resume (comment/uncomment lines).
|
||||
- `-L [id] [filter]`: Query `journalctl` logs for managed jobs (both unit messages and command output). Optional job ID to filter to a single job.
|
||||
- `-S [id]`: Show timer status via `systemctl`, including short IDs and disabled state. Optional job ID to show a single job.
|
||||
- `-C`: Interactively clean up elapsed one-time timers (removes unit files from disk).
|
||||
|
|
@ -44,6 +44,6 @@ There are no automated tests. Test manually with systemd user timers:
|
|||
## 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. Notification flags use `:` after the ID (e.g., `a1b2c3:i,e=user@host`).
|
||||
- Edit mode uses `|` as the field delimiter (not tabs or spaces) to allow multi-word schedules. Notification flags use `:` after the ID (e.g., `a1b2c3:i,o,e=user@host`).
|
||||
- Notification flags are persisted as `# SYSTAB_FLAGS=...` comments in service files and as `ExecStopPost=` lines using `$SERVICE_RESULT`/`$EXIT_STATUS` for status-aware messages.
|
||||
- Journal logs are queried with `USER_UNIT` OR `SYSLOG_IDENTIFIER` to capture both systemd messages and command output.
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ systab -t "in 1 hour" -c "make build" -i
|
|||
|
||||
# With email notification (via sendmail)
|
||||
systab -t "every day at 6am" -c "df -h" -m user@example.com
|
||||
|
||||
# Include last 10 lines of output in notification
|
||||
systab -t "every day at 6am" -c "df -h" -i -o 10
|
||||
```
|
||||
|
||||
### Managing jobs
|
||||
|
|
@ -121,7 +124,7 @@ g7h8i9:e=user@host | weekly | ~/backup.sh
|
|||
- Delete a line to remove a job
|
||||
- Add a line with `new` as the ID to create a job: `new | every 5 minutes | echo hello`
|
||||
- Comment out a line (`#`) to pause, uncomment to resume
|
||||
- Append notification flags after the ID with `:` — `i` for desktop, `e=addr` for email, comma-separated for both (e.g., `a1b2c3:i,e=user@host`)
|
||||
- Append notification flags after the ID with `:` — `i` for desktop, `e=addr` for email, `o` for output (default 10 lines), `o=N` for custom count, comma-separated (e.g., `a1b2c3:i,o,e=user@host`)
|
||||
|
||||
### Job IDs
|
||||
|
||||
|
|
@ -142,6 +145,7 @@ Job Creation:
|
|||
-f <script> Script file to execute (reads stdin if neither -c nor -f)
|
||||
-i Send desktop notification on completion (success/failure)
|
||||
-m <email> Send email notification to address (via sendmail)
|
||||
-o <lines> Include last N lines of job output in notifications (default: 10)
|
||||
|
||||
Management:
|
||||
-P <id> Pause (disable) a job
|
||||
|
|
|
|||
46
systab
46
systab
|
|
@ -21,6 +21,7 @@ opt_status=false
|
|||
opt_pause=""
|
||||
opt_resume=""
|
||||
opt_filter=""
|
||||
opt_output=""
|
||||
opt_jobid=""
|
||||
|
||||
usage() {
|
||||
|
|
@ -35,6 +36,7 @@ Job Creation Options:
|
|||
-f <script> Script file to execute
|
||||
-i Send desktop notification on completion (success/failure)
|
||||
-m <email> Send email notification to address (via sendmail)
|
||||
-o <lines> Include last N lines of job output in notifications (default: 10)
|
||||
|
||||
Management Options:
|
||||
-P <id> Pause (disable) a job
|
||||
|
|
@ -252,6 +254,10 @@ build_flags_string() {
|
|||
if $opt_notify; then
|
||||
flags="i"
|
||||
fi
|
||||
if [[ -n "$opt_output" ]]; then
|
||||
[[ -n "$flags" ]] && flags+=","
|
||||
if [[ "$opt_output" == "10" ]]; then flags+="o"; else flags+="o=$opt_output"; fi
|
||||
fi
|
||||
if [[ -n "$opt_email" ]]; then
|
||||
[[ -n "$flags" ]] && flags+=","
|
||||
flags+="e=$opt_email"
|
||||
|
|
@ -264,31 +270,51 @@ parse_flags() {
|
|||
local flags="$1"
|
||||
_notify_flag=false
|
||||
_email_addr=""
|
||||
_output_lines=""
|
||||
IFS=',' read -ra parts <<< "$flags"
|
||||
for part in "${parts[@]}"; do
|
||||
case "$part" in
|
||||
i) _notify_flag=true ;;
|
||||
o) _output_lines=10 ;;
|
||||
o=*) _output_lines="${part#o=}" ;;
|
||||
e=*) _email_addr="${part#e=}" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Write ExecStopPost notification lines to a service file
|
||||
# Usage: write_notify_lines <short_id> <notify_flag> <email_addr> <file>
|
||||
# Usage: write_notify_lines <short_id> <notify_flag> <email_addr> <file> [output_lines] [job_name]
|
||||
write_notify_lines() {
|
||||
local short_id="$1" notify="$2" email="$3" file="$4"
|
||||
local output_lines="${5-}" job_name="${6-}"
|
||||
local out_cmd=""
|
||||
if [[ -n "$output_lines" && -n "$job_name" ]]; then
|
||||
out_cmd="out=\$(journalctl --user SYSLOG_IDENTIFIER=$job_name -n $output_lines --no-pager -o cat); "
|
||||
fi
|
||||
if [[ "$notify" == true ]]; then
|
||||
cat >> "$file" <<EOF
|
||||
if [[ -n "$out_cmd" ]]; then
|
||||
cat >> "$file" <<EOF
|
||||
ExecStopPost=/bin/sh -c 'if [ "\$SERVICE_RESULT" = success ]; then icon=dialog-information; s=completed; else icon=dialog-error; s="failed (\$EXIT_STATUS)"; fi; ${out_cmd}body=\$(printf "Job $short_id: %%s\n%%s" "\$s" "\$out"); notify-send -i "\$icon" "systab" "\$body" || true'
|
||||
EOF
|
||||
else
|
||||
cat >> "$file" <<EOF
|
||||
ExecStopPost=/bin/sh -c 'if [ "\$SERVICE_RESULT" = success ]; then icon=dialog-information; s=completed; else icon=dialog-error; s="failed (\$EXIT_STATUS)"; fi; notify-send -i "\$icon" "systab" "Job $short_id: \$s" || true'
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$email" ]]; then
|
||||
local mailer
|
||||
mailer=$(command -v sendmail || command -v msmtp || true)
|
||||
[[ -n "$mailer" ]] || { warn "No sendmail or msmtp found, skipping email notification"; return; }
|
||||
cat >> "$file" <<EOF
|
||||
ExecStopPost=/bin/sh -c 'if [ "\$SERVICE_RESULT" = success ]; then s=completed; else s="failed (\$EXIT_STATUS)"; fi; printf "Subject: systab: $short_id %s\\n\\n%s at %s\\n" "\$s" "\$s" "\$(date)" | $mailer $email'
|
||||
if [[ -n "$out_cmd" ]]; then
|
||||
cat >> "$file" <<EOF
|
||||
ExecStopPost=/bin/sh -c 'if [ "\$SERVICE_RESULT" = success ]; then s=completed; else s="failed (\$EXIT_STATUS)"; fi; ${out_cmd}printf "Subject: systab: $short_id %%s\\n\\n%%s at %%s\\n\\n%%s\\n" "\$s" "\$s" "\$(date)" "\$out" | $mailer $email'
|
||||
EOF
|
||||
else
|
||||
cat >> "$file" <<EOF
|
||||
ExecStopPost=/bin/sh -c 'if [ "\$SERVICE_RESULT" = success ]; then s=completed; else s="failed (\$EXIT_STATUS)"; fi; printf "Subject: systab: $short_id %%s\\n\\n%%s at %%s\\n" "\$s" "\$s" "\$(date)" | $mailer $email'
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +348,7 @@ EOF
|
|||
if [[ -n "$flags" ]]; then
|
||||
echo "# SYSTAB_FLAGS=$flags" >> "$service_file"
|
||||
parse_flags "$flags"
|
||||
write_notify_lines "$short_id" "$_notify_flag" "$_email_addr" "$service_file"
|
||||
write_notify_lines "$short_id" "$_notify_flag" "$_email_addr" "$service_file" "$_output_lines" "$job_name"
|
||||
fi
|
||||
|
||||
# Timer file
|
||||
|
|
@ -417,8 +443,11 @@ edit_jobs() {
|
|||
# Add a line with "new" as ID to create a job: new | daily | /path/to/cmd
|
||||
# Comment out a line to disable, uncomment to re-enable.
|
||||
#
|
||||
# Notification flags (append to ID with ':'): i = desktop, e=addr = email
|
||||
# Notification flags (append to ID with ':'): i = desktop, e=addr = email,
|
||||
# o = include output (default 10 lines), o=N = include N lines of output
|
||||
# a1b2c3:i | daily | cmd desktop notification
|
||||
# a1b2c3:i,o | daily | cmd desktop with last 10 lines of output
|
||||
# a1b2c3:i,o=5 | daily | cmd desktop with last 5 lines of output
|
||||
# a1b2c3:e=user@host | daily | cmd email notification
|
||||
# a1b2c3:i,e=user@host | daily | cmd both
|
||||
#
|
||||
|
|
@ -711,7 +740,7 @@ HEADER
|
|||
if [[ -n "$new_flags" ]]; then
|
||||
echo "# SYSTAB_FLAGS=$new_flags" >> "$service_file"
|
||||
parse_flags "$new_flags"
|
||||
write_notify_lines "$id" "$_notify_flag" "$_email_addr" "$service_file"
|
||||
write_notify_lines "$id" "$_notify_flag" "$_email_addr" "$service_file" "$_output_lines" "$jname"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
@ -912,13 +941,14 @@ clean_jobs() {
|
|||
|
||||
# Parse command-line options
|
||||
parse_options() {
|
||||
while getopts "t:c:f:im:P:R:ELSCh" opt; do
|
||||
while getopts "t:c:f:im:o:P:R:ELSCh" opt; do
|
||||
case $opt in
|
||||
t) opt_time="$OPTARG" ;;
|
||||
c) opt_command="$OPTARG" ;;
|
||||
f) opt_file="$OPTARG" ;;
|
||||
i) opt_notify=true ;;
|
||||
m) opt_email="$OPTARG" ;;
|
||||
o) opt_output="$OPTARG" ;;
|
||||
P) opt_pause="$OPTARG" ;;
|
||||
R) opt_resume="$OPTARG" ;;
|
||||
E) opt_edit=true ;;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue