Compare commits

..

No commits in common. "0943a639cc1e19d08c1ac1fcfc80d1e9a87b0620" and "57d2c38d3c61257faab08860528c8ac97b7dc872" have entirely different histories.

5 changed files with 83 additions and 140 deletions

View file

@ -131,9 +131,6 @@ 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
@ -209,7 +206,6 @@ 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

View file

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

View file

@ -25,18 +25,6 @@ Type "systab -t daily -c '/home/user/backup.sh' -i"
Enter Enter
Sleep 2s Sleep 2s
# List jobs in crontab format without opening editor
Hide
Type "./demo/note.sh 'Listing jobs in crontab format (-l)'"
Enter
Sleep 500ms
Show
Sleep 1s
Type "systab -l"
Sleep 500ms
Enter
Sleep 2s
# Open edit mode with EDITOR=nano for visibility # Open edit mode with EDITOR=nano for visibility
Hide Hide
Type "./demo/note.sh 'Opening edit mode — add notifications, create and modify jobs'" Type "./demo/note.sh 'Opening edit mode — add notifications, create and modify jobs'"

169
systab
View file

@ -18,7 +18,6 @@ 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=""
@ -49,7 +48,6 @@ 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
@ -79,9 +77,6 @@ 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
@ -614,70 +609,6 @@ createJobFromEdit() {
echo "$_created_id" echo "$_created_id"
} }
# Print aligned job lines to stdout (no header/footer)
printCrontabContent() {
local -a id_fields schedules commands disabled
local job
while IFS= read -r job; do
[[ -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)
while IFS= read -r job; do
[[ -z "$job" ]] && continue
local service_file="$SYSTEMD_USER_DIR/${job}.service"
if [[ -f "$service_file" ]]; then
local id command flags id_field
id=$(jobId "$job")
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:s"
id_fields+=("$id_field")
schedules+=("service")
commands+=("$command")
if isJobEnabled "$job"; then disabled+=(false); else disabled+=(true); fi
fi
done < <(getManagedServiceJobs)
# Compute column widths; disabled lines account for the "# " prefix
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
# Print aligned lines
for i in "${!id_fields[@]}"; do
if ${disabled[$i]}; then
printf '# %-*s | %-*s | %s\n' $((max_id - 2)) "${id_fields[$i]}" "$max_sched" "${schedules[$i]}" "${commands[$i]}"
else
printf '%-*s | %-*s | %s\n' "$max_id" "${id_fields[$i]}" "$max_sched" "${schedules[$i]}" "${commands[$i]}"
fi
done
}
# List jobs in crontab-like format to stdout
listCrontab() {
printCrontabContent
}
# Edit jobs in crontab-like format # Edit jobs in crontab-like format
editJobs() { editJobs() {
local temp_file orig_file local temp_file orig_file
@ -686,16 +617,82 @@ editJobs() {
# shellcheck disable=SC2064 # shellcheck disable=SC2064
trap "rm -f '$temp_file' '$orig_file'" EXIT trap "rm -f '$temp_file' '$orig_file'" EXIT
# Build crontab content: jobs first, condensed hint at bottom # Build crontab content
{ {
printCrontabContent cat <<'HEADER'
cat <<'HINT' # systab jobs — edit schedule/command, enable/disable, add/remove lines
### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # Format: ID[:FLAGS] | SCHEDULE | COMMAND (pipe-separated)
### format: ID[:FLAGS] | SCHEDULE | COMMAND # # Remove a line to delete a job.
### add: new | daily | cmd # # Add a line with "new" as ID to create a job: new | daily | /path/to/cmd
### flags: n=name / i / e=addr / o[=N] / s # # Comment out a line to disable, uncomment to re-enable.
### comment out = disable / delete = remove # #
HINT # Flags (append to ID with ':'): s - service, i - notify desktop, e=addr - send email,
# o - include output (default 10 lines), o=N - include N lines of output,
# n=name = human-readable name (usable in place of hex ID)
# a1b2c3:n=backup,i | daily | cmd named job with desktop notification
# a1b2c3:i,e=user@host | daily | cmd email and desktop notificaton
# a1b2c3:s | service | cmd persistent service (no timer)
# new:s,n=name | service | cmd new persistent service
#
# Schedule formats (systemd OnCalendar):
# service persistent service (started on login, auto-restarted)
# hourly, daily, weekly, monthly, yearly
# *:0/15 every 15 minutes
# *-*-* 02:00:00 daily at 2am
# Mon *-*-* 09:00 every Monday at 9am
# *-*-01 00:00:00 first of every month
#########################
### add entries below ###
#########################
HEADER
local job
while IFS= read -r job; do
[[ -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)
if [[ -n "$flags" ]]; then
id_field="$id:$flags"
else
id_field="$id"
fi
if isJobEnabled "$job"; then
printf '%s | %s | %s\n' "$id_field" "$schedule" "$command"
else
printf '# %s | %s | %s\n' "$id_field" "$schedule" "$command"
fi
fi
done < <(getManagedUnits timer)
while IFS= read -r job; do
[[ -z "$job" ]] && continue
local service_file="$SYSTEMD_USER_DIR/${job}.service"
if [[ -f "$service_file" ]]; then
local id command flags id_field
id=$(jobId "$job")
command=$(getJobCommand "$service_file")
flags=$(grep "^# SYSTAB_FLAGS=" "$service_file" 2>/dev/null | sed 's/^# SYSTAB_FLAGS=//' || true)
if [[ -n "$flags" ]]; then
id_field="$id:$flags"
else
id_field="$id:s"
fi
if isJobEnabled "$job"; then
printf '%s | service | %s\n' "$id_field" "$command"
else
printf '# %s | service | %s\n' "$id_field" "$command"
fi
fi
done < <(getManagedServiceJobs)
} > "$temp_file" } > "$temp_file"
# Save original for diffing # Save original for diffing
@ -833,7 +830,7 @@ HINT
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 n_disabled=0 needs_reload=false local created=0 deleted=0 updated=0 enabled=0 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
@ -905,7 +902,7 @@ HINT
if ! $was_commented && $now_commented; then if ! $was_commented && $now_commented; then
disableJob "$jname" disableJob "$jname"
echo "Disabled: $id" echo "Disabled: $id"
n_disabled=$((n_disabled + 1)) disabled=$((disabled + 1))
needs_reload=true needs_reload=true
elif $was_commented && ! $now_commented; then elif $was_commented && ! $now_commented; then
enableJob "$jname" enableJob "$jname"
@ -976,7 +973,7 @@ HINT
fi fi
echo "" echo ""
echo "Summary: $created created, $updated updated, $deleted deleted, $enabled enabled, $n_disabled disabled" echo "Summary: $created created, $updated updated, $deleted deleted, $enabled enabled, $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)
@ -1185,7 +1182,7 @@ cleanJobs() {
# Parse command-line options # Parse command-line options
parseOptions() { parseOptions() {
while getopts "t:sc:f:n:im:oD:E:elLSCh" opt; do while getopts "t:sc:f:n:im:oD:E:eLSCh" opt; do
case $opt in case $opt in
t) opt_time="$OPTARG" ;; t) opt_time="$OPTARG" ;;
s) opt_service=true ;; s) opt_service=true ;;
@ -1209,7 +1206,6 @@ 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 ;;
@ -1251,17 +1247,16 @@ 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, -L, -S, and -C are mutually exclusive" error "Options -D, -E, -e, -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, -L, -S, and -C cannot be used with job creation options" error "Management options -D, -E, -e, -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
@ -1292,8 +1287,6 @@ 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

36
test.sh
View file

@ -404,46 +404,12 @@ assert_output "enable already enabled service" "Already enabled:" $SYSTAB -E "$i
# Edit mode shows service jobs with 'service' in schedule column # Edit mode shows service jobs with 'service' in schedule column
# (mirrors tape: EDITOR=nano systab -e shows "id:s | service | cmd") # (mirrors tape: EDITOR=nano systab -e shows "id:s | service | cmd")
edit_output=$(EDITOR=cat $SYSTAB -e 2>&1 || true) edit_output=$(EDITOR=cat $SYSTAB -e 2>&1 || true)
if [[ "$edit_output" == *"| service"* ]]; then if [[ "$edit_output" == *"| service |"* ]]; then
pass "edit mode shows service job with 'service' schedule" pass "edit mode shows service job with 'service' schedule"
else else
fail "edit mode shows service job with 'service' schedule" "not found in: $edit_output" fail "edit mode shows service job with 'service' schedule" "not found in: $edit_output"
fi fi
# -l prints crontab format to stdout
list_output=$($SYSTAB -l 2>&1)
if [[ "$list_output" == *"| service"* ]]; then
pass "-l prints service job with 'service' schedule"
else
fail "-l prints service job with 'service' schedule" "not found in: $list_output"
fi
if [[ "$list_output" == *"$id_recurring"* ]]; then
pass "-l includes timer job"
else
fail "-l includes timer job" "not found in: $list_output"
fi
# -l pipe separators are aligned (all first pipes at same column)
pipe_cols=()
while IFS= read -r line; do
[[ "$line" == *"|"* ]] || continue
# skip hint/separator lines (not job entries)
[[ "$line" =~ ^# ]] && [[ ! "$line" =~ ^#[[:space:]][0-9a-f]{6} ]] && continue
pipe_prefix="${line%%|*}"
pipe_cols+=("${#pipe_prefix}")
done <<< "$list_output"
if [[ ${#pipe_cols[@]} -gt 1 ]]; then
unique_cols=$(printf '%s\n' "${pipe_cols[@]}" | sort -u | wc -l)
if [[ "$unique_cols" -eq 1 ]]; then
pass "-l pipe separators are aligned"
else
fail "-l pipe separators are aligned" "first-pipe columns: ${pipe_cols[*]}"
fi
fi
assert_failure "-l and -e are mutually exclusive" $SYSTAB -l -e
assert_failure "-l cannot be used with job creation options" $SYSTAB -l -t daily -c "echo test"
# Mutually exclusive flags (mirrors tape design: -s conflicts with -t/-i/-m/-o) # Mutually exclusive flags (mirrors tape design: -s conflicts with -t/-i/-m/-o)
assert_failure "-s and -t are mutually exclusive" $SYSTAB -s -t daily -c "echo test" assert_failure "-s and -t are mutually exclusive" $SYSTAB -s -t daily -c "echo test"
assert_failure "-s and -i are mutually exclusive" $SYSTAB -s -i -c "echo test" assert_failure "-s and -i are mutually exclusive" $SYSTAB -s -i -c "echo test"