Add -l option and align pipe separators in edit mode

Extract printCrontabContent() shared by both -e and new -l, collecting
job data into arrays first so column widths can be computed for aligned
pipe separators. -l prints the same crontab format to stdout without
opening an editor, useful for scripting and quick inspection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthias Johnson 2026-02-28 00:26:40 -07:00
parent 57d2c38d3c
commit 8368f1dcf7
2 changed files with 90 additions and 61 deletions

View file

@ -131,6 +131,9 @@ Disable/enable work the same as for timer jobs — disable stops the service and
### Managing jobs ### Managing jobs
```bash ```bash
# Print all jobs in crontab-like format to stdout (useful for scripting)
systab -l
# Edit all jobs in your $EDITOR (crontab-style) # Edit all jobs in your $EDITOR (crontab-style)
systab -e systab -e
@ -206,6 +209,7 @@ Management (accept hex ID or name):
-D <id|name> Disable a job -D <id|name> Disable a job
-E <id|name> Enable a disabled job -E <id|name> Enable a disabled job
-e Edit jobs in crontab-like format -e Edit jobs in crontab-like format
-l Print jobs in crontab-like format to stdout
-L [id|name] [filter] List job logs (optionally for a specific job and/or filtered) -L [id|name] [filter] List job logs (optionally for a specific job and/or filtered)
-S [id|name] Show status of all managed jobs (or a specific job) -S [id|name] Show status of all managed jobs (or a specific job)
-C Clean up completed one-time jobs -C Clean up completed one-time jobs

147
systab
View file

@ -18,6 +18,7 @@ opt_notify=false
opt_email="" opt_email=""
opt_edit=false opt_edit=false
opt_list=false opt_list=false
opt_print_crontab=false
opt_clean=false opt_clean=false
opt_status=false opt_status=false
opt_disable="" opt_disable=""
@ -48,6 +49,7 @@ Management Options (accept hex ID or name):
-D <id|name> Disable a job -D <id|name> Disable a job
-E <id|name> Enable a disabled job -E <id|name> Enable a disabled job
-e Edit jobs in crontab-like format -e Edit jobs in crontab-like format
-l Print jobs in crontab-like format to stdout
-L [id|name] [filter] List job logs (optionally for a specific job and/or filtered) -L [id|name] [filter] List job logs (optionally for a specific job and/or filtered)
-S [id|name] Show status of all managed jobs (or a specific job) -S [id|name] Show status of all managed jobs (or a specific job)
-C Clean up completed one-time jobs -C Clean up completed one-time jobs
@ -77,6 +79,9 @@ EXAMPLES:
# Create a persistent service job # Create a persistent service job
$SCRIPT_NAME -s -n foobar -c "/usr/bin/foobar.sh" $SCRIPT_NAME -s -n foobar -c "/usr/bin/foobar.sh"
# Print jobs in crontab-like format (useful for scripting)
$SCRIPT_NAME -l
# Edit existing jobs (supports adding notifications via ID:flags syntax) # Edit existing jobs (supports adding notifications via ID:flags syntax)
$SCRIPT_NAME -e $SCRIPT_NAME -e
@ -609,17 +614,9 @@ createJobFromEdit() {
echo "$_created_id" echo "$_created_id"
} }
# Edit jobs in crontab-like format # Print crontab content (header + aligned job lines) to stdout
editJobs() { printCrontabContent() {
local temp_file orig_file cat <<'HEADER'
temp_file="${TMPDIR:-/dev/shm}/systab_edit_$$"
orig_file="${TMPDIR:-/dev/shm}/systab_orig_$$"
# shellcheck disable=SC2064
trap "rm -f '$temp_file' '$orig_file'" EXIT
# Build crontab content
{
cat <<'HEADER'
# systab jobs — edit schedule/command, enable/disable, add/remove lines # systab jobs — edit schedule/command, enable/disable, add/remove lines
# Format: ID[:FLAGS] | SCHEDULE | COMMAND (pipe-separated) # Format: ID[:FLAGS] | SCHEDULE | COMMAND (pipe-separated)
# Remove a line to delete a job. # Remove a line to delete a job.
@ -646,54 +643,78 @@ editJobs() {
######################### #########################
HEADER HEADER
local job local -a id_fields schedules commands disabled
while IFS= read -r job; do local job
[[ -z "$job" ]] && continue
local timer_file="$SYSTEMD_USER_DIR/${job}.timer" while IFS= read -r job; do
local service_file="$SYSTEMD_USER_DIR/${job}.service" [[ -z "$job" ]] && continue
local timer_file="$SYSTEMD_USER_DIR/${job}.timer"
local service_file="$SYSTEMD_USER_DIR/${job}.service"
if [[ -f "$timer_file" && -f "$service_file" ]]; then
local id schedule command flags id_field
id=$(jobId "$job")
schedule=$(grep "^OnCalendar=" "$timer_file" | cut -d= -f2-)
command=$(getJobCommand "$service_file")
flags=$(grep "^# SYSTAB_FLAGS=" "$service_file" 2>/dev/null | sed 's/^# SYSTAB_FLAGS=//' || true)
[[ -n "$flags" ]] && id_field="$id:$flags" || id_field="$id"
id_fields+=("$id_field")
schedules+=("$schedule")
commands+=("$command")
if isJobEnabled "$job"; then disabled+=(false); else disabled+=(true); fi
fi
done < <(getManagedUnits timer)
if [[ -f "$timer_file" && -f "$service_file" ]]; then while IFS= read -r job; do
local id schedule command flags id_field [[ -z "$job" ]] && continue
id=$(jobId "$job") local service_file="$SYSTEMD_USER_DIR/${job}.service"
schedule=$(grep "^OnCalendar=" "$timer_file" | cut -d= -f2-) if [[ -f "$service_file" ]]; then
command=$(getJobCommand "$service_file") local id command flags id_field
flags=$(grep "^# SYSTAB_FLAGS=" "$service_file" 2>/dev/null | sed 's/^# SYSTAB_FLAGS=//' || true) id=$(jobId "$job")
if [[ -n "$flags" ]]; then command=$(getJobCommand "$service_file")
id_field="$id:$flags" flags=$(grep "^# SYSTAB_FLAGS=" "$service_file" 2>/dev/null | sed 's/^# SYSTAB_FLAGS=//' || true)
else [[ -n "$flags" ]] && id_field="$id:$flags" || id_field="$id:s"
id_field="$id" id_fields+=("$id_field")
fi schedules+=("service")
if isJobEnabled "$job"; then commands+=("$command")
printf '%s | %s | %s\n' "$id_field" "$schedule" "$command" if isJobEnabled "$job"; then disabled+=(false); else disabled+=(true); fi
else fi
printf '# %s | %s | %s\n' "$id_field" "$schedule" "$command" done < <(getManagedServiceJobs)
fi
fi
done < <(getManagedUnits timer)
while IFS= read -r job; do # Compute column widths; disabled lines account for the "# " prefix
[[ -z "$job" ]] && continue local max_id=0 max_sched=0 i len
for i in "${!id_fields[@]}"; do
len=${#id_fields[$i]}
${disabled[$i]} && len=$((len + 2))
[[ $len -gt $max_id ]] && max_id=$len
len=${#schedules[$i]}
[[ $len -gt $max_sched ]] && max_sched=$len
done
local service_file="$SYSTEMD_USER_DIR/${job}.service" # Print aligned lines
if [[ -f "$service_file" ]]; then for i in "${!id_fields[@]}"; do
local id command flags id_field if ${disabled[$i]}; then
id=$(jobId "$job") printf '# %-*s | %-*s | %s\n' $((max_id - 2)) "${id_fields[$i]}" "$max_sched" "${schedules[$i]}" "${commands[$i]}"
command=$(getJobCommand "$service_file") else
flags=$(grep "^# SYSTAB_FLAGS=" "$service_file" 2>/dev/null | sed 's/^# SYSTAB_FLAGS=//' || true) printf '%-*s | %-*s | %s\n' "$max_id" "${id_fields[$i]}" "$max_sched" "${schedules[$i]}" "${commands[$i]}"
if [[ -n "$flags" ]]; then fi
id_field="$id:$flags" done
else }
id_field="$id:s"
fi # List jobs in crontab-like format to stdout
if isJobEnabled "$job"; then listCrontab() {
printf '%s | service | %s\n' "$id_field" "$command" printCrontabContent
else }
printf '# %s | service | %s\n' "$id_field" "$command"
fi # Edit jobs in crontab-like format
fi editJobs() {
done < <(getManagedServiceJobs) local temp_file orig_file
} > "$temp_file" temp_file="${TMPDIR:-/dev/shm}/systab_edit_$$"
orig_file="${TMPDIR:-/dev/shm}/systab_orig_$$"
# shellcheck disable=SC2064
trap "rm -f '$temp_file' '$orig_file'" EXIT
# Build crontab content
printCrontabContent > "$temp_file"
# Save original for diffing # Save original for diffing
cp "$temp_file" "$orig_file" cp "$temp_file" "$orig_file"
@ -830,7 +851,7 @@ HEADER
for id in "${!orig_jobs[@]}"; do all_orig_ids["$id"]=1; done for id in "${!orig_jobs[@]}"; do all_orig_ids["$id"]=1; done
for id in "${!orig_commented[@]}"; do all_orig_ids["$id"]=1; done for id in "${!orig_commented[@]}"; do all_orig_ids["$id"]=1; done
local created=0 deleted=0 updated=0 enabled=0 disabled=0 needs_reload=false local created=0 deleted=0 updated=0 enabled=0 n_disabled=0 needs_reload=false
# Deletions: IDs in original (active or commented) but absent from edited entirely # Deletions: IDs in original (active or commented) but absent from edited entirely
for id in "${!all_orig_ids[@]}"; do for id in "${!all_orig_ids[@]}"; do
@ -902,7 +923,7 @@ HEADER
if ! $was_commented && $now_commented; then if ! $was_commented && $now_commented; then
disableJob "$jname" disableJob "$jname"
echo "Disabled: $id" echo "Disabled: $id"
disabled=$((disabled + 1)) n_disabled=$((n_disabled + 1))
needs_reload=true needs_reload=true
elif $was_commented && ! $now_commented; then elif $was_commented && ! $now_commented; then
enableJob "$jname" enableJob "$jname"
@ -973,7 +994,7 @@ HEADER
fi fi
echo "" echo ""
echo "Summary: $created created, $updated updated, $deleted deleted, $enabled enabled, $disabled disabled" echo "Summary: $created created, $updated updated, $deleted deleted, $enabled enabled, $n_disabled disabled"
} }
# Build a job list from opt_jobid (single job) or getManagedUnits (all jobs) # Build a job list from opt_jobid (single job) or getManagedUnits (all jobs)
@ -1182,7 +1203,7 @@ cleanJobs() {
# Parse command-line options # Parse command-line options
parseOptions() { parseOptions() {
while getopts "t:sc:f:n:im:oD:E:eLSCh" opt; do while getopts "t:sc:f:n:im:oD:E:elLSCh" opt; do
case $opt in case $opt in
t) opt_time="$OPTARG" ;; t) opt_time="$OPTARG" ;;
s) opt_service=true ;; s) opt_service=true ;;
@ -1206,6 +1227,7 @@ parseOptions() {
D) opt_disable="$OPTARG" ;; D) opt_disable="$OPTARG" ;;
E) opt_enable="$OPTARG" ;; E) opt_enable="$OPTARG" ;;
e) opt_edit=true ;; e) opt_edit=true ;;
l) opt_print_crontab=true ;;
L) opt_list=true ;; L) opt_list=true ;;
S) opt_status=true ;; S) opt_status=true ;;
C) opt_clean=true ;; C) opt_clean=true ;;
@ -1247,16 +1269,17 @@ parseOptions() {
[[ -n "$opt_disable" ]] && manage_count=$((manage_count + 1)) [[ -n "$opt_disable" ]] && manage_count=$((manage_count + 1))
[[ -n "$opt_enable" ]] && manage_count=$((manage_count + 1)) [[ -n "$opt_enable" ]] && manage_count=$((manage_count + 1))
$opt_edit && manage_count=$((manage_count + 1)) $opt_edit && manage_count=$((manage_count + 1))
$opt_print_crontab && manage_count=$((manage_count + 1))
$opt_list && manage_count=$((manage_count + 1)) $opt_list && manage_count=$((manage_count + 1))
$opt_status && manage_count=$((manage_count + 1)) $opt_status && manage_count=$((manage_count + 1))
$opt_clean && manage_count=$((manage_count + 1)) $opt_clean && manage_count=$((manage_count + 1))
if [[ $manage_count -gt 1 ]]; then if [[ $manage_count -gt 1 ]]; then
error "Options -D, -E, -e, -L, -S, and -C are mutually exclusive" error "Options -D, -E, -e, -l, -L, -S, and -C are mutually exclusive"
fi fi
if [[ $manage_count -gt 0 ]] && { [[ -n "$opt_time$opt_command$opt_file" ]] || $opt_service; }; then if [[ $manage_count -gt 0 ]] && { [[ -n "$opt_time$opt_command$opt_file" ]] || $opt_service; }; then
error "Management options -D, -E, -e, -L, -S, and -C cannot be used with job creation options" error "Management options -D, -E, -e, -l, -L, -S, and -C cannot be used with job creation options"
fi fi
if [[ -n "$opt_command" && -n "$opt_file" ]]; then if [[ -n "$opt_command" && -n "$opt_file" ]]; then
@ -1287,6 +1310,8 @@ main() {
toggleJobById "$opt_enable" enable toggleJobById "$opt_enable" enable
elif $opt_edit; then elif $opt_edit; then
editJobs editJobs
elif $opt_print_crontab; then
listCrontab
elif $opt_list; then elif $opt_list; then
listLogs listLogs
elif $opt_status; then elif $opt_status; then