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 <noreply@anthropic.com>
This commit is contained in:
Matthias Johnson 2026-03-02 01:17:12 -07:00
parent 8e45f7917c
commit 530a2162bf
5 changed files with 70 additions and 6 deletions

View file

@ -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 <id|name> Disable a job
-E <id|name> Enable a disabled job
-X <id|name> Delete a job (stop, disable, and remove unit files)
-R <id|name> 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

View file

@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"label": "tests",
"message": "127 passed",
"message": "136 passed",
"color": "brightgreen"
}

View file

@ -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

36
systab
View file

@ -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 <id|name> Disable a job
-E <id|name> Enable a disabled job
-X <id|name> Delete a job (stop, disable, and remove unit files)
-R <id|name> 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 <id>
$SCRIPT_NAME -X backup
# Restart a job
$SCRIPT_NAME -R <id>
$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

20
test.sh
View file

@ -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
# ============================================================