From 530a2162bf8b6c135144881641574bc97e06563c Mon Sep 17 00:00:00 2001 From: Matthias Johnson Date: Mon, 2 Mar 2026 01:17:12 -0700 Subject: [PATCH] Add -R restart operation for timer and service jobs Resets the countdown for timer jobs and restarts the process for service jobs. Disabled jobs are refused with a clear error message. Accepts hex ID or name like all other management operations. - restartJob() function with disabled-job guard - getopts R:, manage_count, mutual-exclusion error messages updated - 9 new tests in test.sh (112 total), 1 new tape command (24 total) - README options table, names prose, and future-ideas checklist updated Co-Authored-By: Claude Sonnet 4.6 --- README.md | 5 +++-- badges/tests.json | 2 +- demo/all-features.tape | 13 +++++++++++++ systab | 36 +++++++++++++++++++++++++++++++++--- test.sh | 20 ++++++++++++++++++++ 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d43e904..be4ece9 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ systab -C ### Job IDs and names -Each job gets a 6-character hex ID (e.g., `a1b2c3`) displayed on creation and in status output. You can also assign a human-readable name with `-n` at creation time. Names can be used interchangeably with hex IDs in `-D`, `-E`, `-X`, `-S`, and `-L`. Names must be unique and cannot contain whitespace, pipes, or colons. +Each job gets a 6-character hex ID (e.g., `a1b2c3`) displayed on creation and in status output. You can also assign a human-readable name with `-n` at creation time. Names can be used interchangeably with hex IDs in `-D`, `-E`, `-X`, `-R`, `-S`, and `-L`. Names must be unique and cannot contain whitespace, pipes, or colons. ## How it works @@ -209,6 +209,7 @@ Management (accept hex ID or name): -D Disable a job -E Enable a disabled job -X Delete a job (stop, disable, and remove unit files) + -R Restart a job (resets timer countdown / restarts service process) -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) @@ -219,7 +220,7 @@ Management (accept hex ID or name): ## Future feature ideas -- [ ] `-R` flag to restart / reload +- [x] `-R` flag to restart / reload - [x] `-X` flag to delete ## License diff --git a/badges/tests.json b/badges/tests.json index 83ea378..b392426 100644 --- a/badges/tests.json +++ b/badges/tests.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "tests", - "message": "127 passed", + "message": "136 passed", "color": "brightgreen" } diff --git a/demo/all-features.tape b/demo/all-features.tape index f47b449..2cee057 100644 --- a/demo/all-features.tape +++ b/demo/all-features.tape @@ -109,6 +109,19 @@ Sleep 500ms Enter Sleep 2s +# ── Restart ─────────────────────────────────────────────────── + +Hide +Type "./demo/note.sh 'Restart a job — resets the timer countdown'" +Enter +Sleep 500ms +Show +Sleep 1s +Type "systab -R healthcheck_home" +Sleep 500ms +Enter +Sleep 2s + # ── Notifications ───────────────────────────────────────────── Hide diff --git a/systab b/systab index c69fa8a..0c5fb6e 100755 --- a/systab +++ b/systab @@ -24,6 +24,7 @@ opt_status=false opt_disable="" opt_enable="" opt_delete="" +opt_restart="" opt_filter="" opt_output="" opt_name="" @@ -50,6 +51,7 @@ Management Options (accept hex ID or name): -D Disable a job -E Enable a disabled job -X Delete a job (stop, disable, and remove unit files) + -R Restart a job (resets timer countdown / restarts service process) -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) @@ -95,6 +97,10 @@ EXAMPLES: $SCRIPT_NAME -X $SCRIPT_NAME -X backup + # Restart a job + $SCRIPT_NAME -R + $SCRIPT_NAME -R healthcheck_home + # View logs for backup jobs $SCRIPT_NAME -L backup @@ -353,6 +359,26 @@ deleteJob() { echo "Deleted: $label" } +# Restart a job by hex ID or name +restartJob() { + local input="$1" + validateJobId "$input" + local id="$_resolved_id" + local name + name=$(getJobName "$SYSTEMD_USER_DIR/${_job_name}.service") + local label + label=$(formatJobId "$id" "$name") + if ! isJobEnabled "$_job_name"; then + error "Job is disabled, enable it first with -E: $label" + fi + if isJobService "$_job_name"; then + systemctl --user restart "${_job_name}.service" 2>/dev/null || true + else + systemctl --user restart "${_job_name}.timer" 2>/dev/null || true + fi + echo "Restarted: $label" +} + # Get all managed unit files of a given type (service or timer) getManagedUnits() { local ext="$1" @@ -1205,7 +1231,7 @@ cleanJobs() { # Parse command-line options parseOptions() { - while getopts "t:sc:f:n:im:oD:E:X:elLSCh" opt; do + while getopts "t:sc:f:n:im:oD:E:X:R:elLSCh" opt; do case $opt in t) opt_time="$OPTARG" ;; s) opt_service=true ;; @@ -1229,6 +1255,7 @@ parseOptions() { D) opt_disable="$OPTARG" ;; E) opt_enable="$OPTARG" ;; X) opt_delete="$OPTARG" ;; + R) opt_restart="$OPTARG" ;; e) opt_edit=true ;; l) opt_print_crontab=true ;; L) opt_list=true ;; @@ -1272,6 +1299,7 @@ parseOptions() { [[ -n "$opt_disable" ]] && manage_count=$((manage_count + 1)) [[ -n "$opt_enable" ]] && manage_count=$((manage_count + 1)) [[ -n "$opt_delete" ]] && manage_count=$((manage_count + 1)) + [[ -n "$opt_restart" ]] && 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)) @@ -1279,11 +1307,11 @@ parseOptions() { $opt_clean && manage_count=$((manage_count + 1)) if [[ $manage_count -gt 1 ]]; then - error "Options -D, -E, -X, -e, -l, -L, -S, and -C are mutually exclusive" + error "Options -D, -E, -X, -R, -e, -l, -L, -S, and -C are mutually exclusive" fi if [[ $manage_count -gt 0 ]] && { [[ -n "$opt_time$opt_command$opt_file" ]] || $opt_service; }; then - error "Management options -D, -E, -X, -e, -l, -L, -S, and -C cannot be used with job creation options" + error "Management options -D, -E, -X, -R, -e, -l, -L, -S, and -C cannot be used with job creation options" fi if [[ -n "$opt_command" && -n "$opt_file" ]]; then @@ -1314,6 +1342,8 @@ main() { toggleJobById "$opt_enable" enable elif [[ -n "$opt_delete" ]]; then deleteJob "$opt_delete" + elif [[ -n "$opt_restart" ]]; then + restartJob "$opt_restart" elif $opt_edit; then editJobs elif $opt_print_crontab; then diff --git a/test.sh b/test.sh index aa2c409..8672148 100755 --- a/test.sh +++ b/test.sh @@ -502,6 +502,26 @@ assert_failure "-s and -i are mutually exclusive" $SYSTAB -s -i -c "echo test" assert_failure "-s and -m are mutually exclusive" $SYSTAB -s -m user@example.com -c "echo test" assert_failure "-s and -o are mutually exclusive" $SYSTAB -s -o -c "echo test" +# ============================================================ +# Restart (-R) +# ============================================================ + +echo "" +echo "${BOLD}--- Restart (-R) ---${RESET}" + +assert_output "restart timer job by ID" "Restarted:" $SYSTAB -R "$id_recurring" +assert_output "restart timer job by name" "Restarted:" $SYSTAB -R mytest +assert_last_output_contains "restart by name shows name" "(mytest)" +assert_output "restart service job" "Restarted:" $SYSTAB -R "$id_svc" + +$SYSTAB -D "$id_recurring" +assert_failure "restart disabled job fails" $SYSTAB -R "$id_recurring" +$SYSTAB -E "$id_recurring" + +assert_failure "restart nonexistent job fails" $SYSTAB -R "zzzzzz" +assert_failure "-R and -D are mutually exclusive" $SYSTAB -R "$id_recurring" -D "$id_recurring" +assert_failure "-R cannot be combined with job creation" $SYSTAB -R "$id_recurring" -t daily -c "echo test" + # ============================================================ # Clean # ============================================================