Use short ID as unit filename instead of timestamp
Unit files are now named systab_<6-char-hex> (e.g., systab_a1b2c3), so the ID is derived from the filename — no more SYSTAB_ID comments in unit files, no id-to-jobname mappings in edit mode. Removes generate_job_name, get_job_id, get_job_by_id, ensure_job_id. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b7e6a77ef5
commit
5e961d70f4
2 changed files with 24 additions and 112 deletions
|
|
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`systab` is a single-file Bash script that provides a cron/at/batch-like interface for systemd user timers. It creates, manages, and cleans up systemd `.service` and `.timer` unit files in `~/.config/systemd/user/`. Managed units are tagged with a `# SYSTAB_MANAGED` marker comment and a `# SYSTAB_ID=<hex>` short ID for human-friendly identification.
|
`systab` is a single-file Bash script that provides a cron/at/batch-like interface for systemd user timers. It creates, manages, and cleans up systemd `.service` and `.timer` unit files in `~/.config/systemd/user/`. Managed units are tagged with a `# SYSTAB_MANAGED` marker comment. Unit filenames use a 6-char hex ID (e.g., `systab_a1b2c3.timer`) which doubles as the human-facing job identifier.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ The script has two modes controlled by CLI flags:
|
||||||
- `-S`: Show timer status via `systemctl`, including short IDs.
|
- `-S`: Show timer status via `systemctl`, including short IDs.
|
||||||
- `-C`: Interactively clean up elapsed one-time timers (removes unit files from disk).
|
- `-C`: Interactively clean up elapsed one-time timers (removes unit files from disk).
|
||||||
|
|
||||||
Key functions: `parse_time` (time spec → OnCalendar), `create_job` (generates unit files), `edit_jobs` (crontab-style edit with diff-and-apply), `get_managed_services`/`get_managed_timers` (find tagged units), `ensure_job_id` (auto-assign IDs to legacy jobs), `clean_jobs` (remove elapsed one-time timers).
|
Key functions: `parse_time` (time spec → OnCalendar), `create_job` (generates unit files), `edit_jobs` (crontab-style edit with diff-and-apply), `get_managed_services`/`get_managed_timers` (find tagged units), `clean_jobs` (remove elapsed one-time timers).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|
|
||||||
128
systab
128
systab
|
|
@ -113,62 +113,14 @@ is_recurring() {
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate unique job name
|
# Generate a random 6-char hex ID
|
||||||
generate_job_name() {
|
generate_id() {
|
||||||
local timestamp rand
|
|
||||||
timestamp=$(date +%Y%m%d_%H%M%S)
|
|
||||||
rand=$(od -An -tx2 -N2 /dev/urandom | tr -d ' ')
|
|
||||||
echo "${SCRIPT_NAME}_${timestamp}_${rand}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate a random 6-char hex short ID
|
|
||||||
generate_short_id() {
|
|
||||||
od -An -tx1 -N3 /dev/urandom | tr -d ' \n'
|
od -An -tx1 -N3 /dev/urandom | tr -d ' \n'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract SYSTAB_ID from a job's timer file; empty if missing
|
# Extract short ID from job name (systab_<id> → <id>)
|
||||||
get_job_id() {
|
job_id() {
|
||||||
local job_name="$1"
|
echo "${1#"${SCRIPT_NAME}"_}"
|
||||||
local timer_file="$SYSTEMD_USER_DIR/${job_name}.timer"
|
|
||||||
if [[ -f "$timer_file" ]]; then
|
|
||||||
sed -n 's/^# SYSTAB_ID=//p' "$timer_file"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Find job name by short ID; empty if not found
|
|
||||||
get_job_by_id() {
|
|
||||||
local target_id="$1"
|
|
||||||
local job
|
|
||||||
while IFS= read -r job; do
|
|
||||||
[[ -z "$job" ]] && continue
|
|
||||||
local jid
|
|
||||||
jid=$(get_job_id "$job")
|
|
||||||
if [[ "$jid" == "$target_id" ]]; then
|
|
||||||
echo "$job"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
done < <(get_managed_timers)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ensure a managed job has a SYSTAB_ID; assign one if missing
|
|
||||||
ensure_job_id() {
|
|
||||||
local job_name="$1"
|
|
||||||
local existing_id
|
|
||||||
existing_id=$(get_job_id "$job_name")
|
|
||||||
if [[ -n "$existing_id" ]]; then
|
|
||||||
echo "$existing_id"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
local new_id
|
|
||||||
new_id=$(generate_short_id)
|
|
||||||
# Insert SYSTAB_ID line after the MARKER in both files
|
|
||||||
for ext in timer service; do
|
|
||||||
local unit_file="$SYSTEMD_USER_DIR/${job_name}.${ext}"
|
|
||||||
if [[ -f "$unit_file" ]]; then
|
|
||||||
sed -i "s/^$MARKER$/&\n# SYSTAB_ID=$new_id/" "$unit_file"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "$new_id"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get all managed service files
|
# Get all managed service files
|
||||||
|
|
@ -216,7 +168,7 @@ get_managed_timers() {
|
||||||
# Create systemd service and timer files
|
# Create systemd service and timer files
|
||||||
create_job() {
|
create_job() {
|
||||||
local job_name command_to_run time_spec
|
local job_name command_to_run time_spec
|
||||||
job_name=$(generate_job_name)
|
job_name="${SCRIPT_NAME}_$(generate_id)"
|
||||||
time_spec=$(parse_time "$opt_time")
|
time_spec=$(parse_time "$opt_time")
|
||||||
|
|
||||||
# Determine command to run
|
# Determine command to run
|
||||||
|
|
@ -238,15 +190,10 @@ create_job() {
|
||||||
# Create systemd user directory if needed
|
# Create systemd user directory if needed
|
||||||
mkdir -p "$SYSTEMD_USER_DIR"
|
mkdir -p "$SYSTEMD_USER_DIR"
|
||||||
|
|
||||||
# Generate short ID for human-friendly display
|
|
||||||
local short_id
|
|
||||||
short_id=$(generate_short_id)
|
|
||||||
|
|
||||||
# Create service file
|
# Create service file
|
||||||
local service_file="$SYSTEMD_USER_DIR/${job_name}.service"
|
local service_file="$SYSTEMD_USER_DIR/${job_name}.service"
|
||||||
cat > "$service_file" <<EOF
|
cat > "$service_file" <<EOF
|
||||||
$MARKER
|
$MARKER
|
||||||
# SYSTAB_ID=$short_id
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=$SCRIPT_NAME job: $job_name
|
Description=$SCRIPT_NAME job: $job_name
|
||||||
|
|
||||||
|
|
@ -286,7 +233,6 @@ EOF
|
||||||
local timer_file="$SYSTEMD_USER_DIR/${job_name}.timer"
|
local timer_file="$SYSTEMD_USER_DIR/${job_name}.timer"
|
||||||
cat > "$timer_file" <<EOF
|
cat > "$timer_file" <<EOF
|
||||||
$MARKER
|
$MARKER
|
||||||
# SYSTAB_ID=$short_id
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Timer for $SCRIPT_NAME job: $job_name
|
Description=Timer for $SCRIPT_NAME job: $job_name
|
||||||
|
|
||||||
|
|
@ -312,7 +258,7 @@ EOF
|
||||||
systemctl --user enable "$job_name.timer"
|
systemctl --user enable "$job_name.timer"
|
||||||
systemctl --user start "$job_name.timer"
|
systemctl --user start "$job_name.timer"
|
||||||
|
|
||||||
echo "Job created: $short_id ($job_name)"
|
echo "Job created: $(job_id "$job_name")"
|
||||||
echo "Next run: $(systemctl --user list-timers "$job_name.timer" --no-pager | tail -n 2 | head -n 1 | awk '{print $1, $2, $3}')"
|
echo "Next run: $(systemctl --user list-timers "$job_name.timer" --no-pager | tail -n 2 | head -n 1 | awk '{print $1, $2, $3}')"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,20 +270,18 @@ remove_job() {
|
||||||
rm -f "$SYSTEMD_USER_DIR/${job_name}.service" "$SYSTEMD_USER_DIR/${job_name}.timer"
|
rm -f "$SYSTEMD_USER_DIR/${job_name}.service" "$SYSTEMD_USER_DIR/${job_name}.timer"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create a job from edit mode (schedule + command, returns short ID)
|
# Create a job from edit mode (schedule + command, prints short ID)
|
||||||
create_job_from_edit() {
|
create_job_from_edit() {
|
||||||
local schedule="$1" command_to_run="$2"
|
local schedule="$1" command_to_run="$2"
|
||||||
local job_name short_id
|
local job_name
|
||||||
|
|
||||||
job_name=$(generate_job_name)
|
job_name="${SCRIPT_NAME}_$(generate_id)"
|
||||||
short_id=$(generate_short_id)
|
|
||||||
|
|
||||||
mkdir -p "$SYSTEMD_USER_DIR"
|
mkdir -p "$SYSTEMD_USER_DIR"
|
||||||
|
|
||||||
# Service file
|
# Service file
|
||||||
cat > "$SYSTEMD_USER_DIR/${job_name}.service" <<EOF
|
cat > "$SYSTEMD_USER_DIR/${job_name}.service" <<EOF
|
||||||
$MARKER
|
$MARKER
|
||||||
# SYSTAB_ID=$short_id
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=$SCRIPT_NAME job: $job_name
|
Description=$SCRIPT_NAME job: $job_name
|
||||||
|
|
||||||
|
|
@ -349,7 +293,6 @@ EOF
|
||||||
# Timer file
|
# Timer file
|
||||||
cat > "$SYSTEMD_USER_DIR/${job_name}.timer" <<EOF
|
cat > "$SYSTEMD_USER_DIR/${job_name}.timer" <<EOF
|
||||||
$MARKER
|
$MARKER
|
||||||
# SYSTAB_ID=$short_id
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Timer for $SCRIPT_NAME job: $job_name
|
Description=Timer for $SCRIPT_NAME job: $job_name
|
||||||
|
|
||||||
|
|
@ -373,7 +316,7 @@ EOF
|
||||||
systemctl --user enable "$job_name.timer"
|
systemctl --user enable "$job_name.timer"
|
||||||
systemctl --user start "$job_name.timer"
|
systemctl --user start "$job_name.timer"
|
||||||
|
|
||||||
echo "$short_id ($job_name)"
|
job_id "$job_name"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Edit jobs in crontab-like format
|
# Edit jobs in crontab-like format
|
||||||
|
|
@ -408,11 +351,11 @@ HEADER
|
||||||
local service_file="$SYSTEMD_USER_DIR/${job}.service"
|
local service_file="$SYSTEMD_USER_DIR/${job}.service"
|
||||||
|
|
||||||
if [[ -f "$timer_file" && -f "$service_file" ]]; then
|
if [[ -f "$timer_file" && -f "$service_file" ]]; then
|
||||||
local short_id schedule command
|
local id schedule command
|
||||||
short_id=$(ensure_job_id "$job")
|
id=$(job_id "$job")
|
||||||
schedule=$(grep "^OnCalendar=" "$timer_file" | cut -d= -f2-)
|
schedule=$(grep "^OnCalendar=" "$timer_file" | cut -d= -f2-)
|
||||||
command=$(grep "^ExecStart=" "$service_file" | sed 's/^ExecStart=.*-c //' | sed "s/^'//" | sed "s/'$//")
|
command=$(grep "^ExecStart=" "$service_file" | sed 's/^ExecStart=.*-c //' | sed "s/^'//" | sed "s/'$//")
|
||||||
printf '%s\t%s\t%s\n' "$short_id" "$schedule" "$command"
|
printf '%s\t%s\t%s\n' "$id" "$schedule" "$command"
|
||||||
fi
|
fi
|
||||||
done < <(get_managed_timers)
|
done < <(get_managed_timers)
|
||||||
} > "$temp_file"
|
} > "$temp_file"
|
||||||
|
|
@ -436,18 +379,6 @@ HEADER
|
||||||
# "new" lines are collected separately since there can be multiple
|
# "new" lines are collected separately since there can be multiple
|
||||||
declare -A orig_jobs edited_jobs
|
declare -A orig_jobs edited_jobs
|
||||||
declare -a new_jobs=()
|
declare -a new_jobs=()
|
||||||
declare -A id_to_jobname
|
|
||||||
|
|
||||||
# Build id -> jobname mapping from current managed timers
|
|
||||||
local job
|
|
||||||
while IFS= read -r job; do
|
|
||||||
[[ -z "$job" ]] && continue
|
|
||||||
local jid
|
|
||||||
jid=$(get_job_id "$job")
|
|
||||||
if [[ -n "$jid" ]]; then
|
|
||||||
id_to_jobname["$jid"]="$job"
|
|
||||||
fi
|
|
||||||
done < <(get_managed_timers)
|
|
||||||
|
|
||||||
# Parse a crontab line into id, sched, cmd (split on first two whitespace runs)
|
# Parse a crontab line into id, sched, cmd (split on first two whitespace runs)
|
||||||
parse_crontab_line() {
|
parse_crontab_line() {
|
||||||
|
|
@ -485,14 +416,11 @@ HEADER
|
||||||
# Deletions: IDs in original but not in edited
|
# Deletions: IDs in original but not in edited
|
||||||
for id in "${!orig_jobs[@]}"; do
|
for id in "${!orig_jobs[@]}"; do
|
||||||
if [[ -z "${edited_jobs[$id]+x}" ]]; then
|
if [[ -z "${edited_jobs[$id]+x}" ]]; then
|
||||||
local jname="${id_to_jobname[$id]:-}"
|
remove_job "${SCRIPT_NAME}_${id}"
|
||||||
if [[ -n "$jname" ]]; then
|
echo "Deleted: $id"
|
||||||
remove_job "$jname"
|
|
||||||
echo "Deleted: $id ($jname)"
|
|
||||||
deleted=$((deleted + 1))
|
deleted=$((deleted + 1))
|
||||||
needs_reload=true
|
needs_reload=true
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Creations: "new" lines from edited file
|
# Creations: "new" lines from edited file
|
||||||
|
|
@ -517,11 +445,7 @@ HEADER
|
||||||
# Updates: IDs in both but with changed values
|
# Updates: IDs in both but with changed values
|
||||||
for id in "${!edited_jobs[@]}"; do
|
for id in "${!edited_jobs[@]}"; do
|
||||||
if [[ -n "${orig_jobs[$id]+x}" && "${orig_jobs[$id]}" != "${edited_jobs[$id]}" ]]; then
|
if [[ -n "${orig_jobs[$id]+x}" && "${orig_jobs[$id]}" != "${edited_jobs[$id]}" ]]; then
|
||||||
local jname="${id_to_jobname[$id]:-}"
|
local jname="${SCRIPT_NAME}_${id}"
|
||||||
if [[ -z "$jname" ]]; then
|
|
||||||
warn "Cannot find job for ID '$id', skipping update"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
local old_entry="${orig_jobs[$id]}" new_entry="${edited_jobs[$id]}"
|
local old_entry="${orig_jobs[$id]}" new_entry="${edited_jobs[$id]}"
|
||||||
local old_sched new_sched old_cmd new_cmd
|
local old_sched new_sched old_cmd new_cmd
|
||||||
|
|
@ -552,7 +476,7 @@ HEADER
|
||||||
fi
|
fi
|
||||||
|
|
||||||
systemctl --user restart "${jname}.timer" 2>/dev/null || true
|
systemctl --user restart "${jname}.timer" 2>/dev/null || true
|
||||||
echo "Updated: $id ($jname)"
|
echo "Updated: $id"
|
||||||
updated=$((updated + 1))
|
updated=$((updated + 1))
|
||||||
needs_reload=true
|
needs_reload=true
|
||||||
fi
|
fi
|
||||||
|
|
@ -590,13 +514,7 @@ show_status() {
|
||||||
local timer_file="$SYSTEMD_USER_DIR/${job}.timer"
|
local timer_file="$SYSTEMD_USER_DIR/${job}.timer"
|
||||||
local service_file="$SYSTEMD_USER_DIR/${job}.service"
|
local service_file="$SYSTEMD_USER_DIR/${job}.service"
|
||||||
|
|
||||||
local short_id
|
echo "Job: $(job_id "$job")"
|
||||||
short_id=$(get_job_id "$job")
|
|
||||||
if [[ -n "$short_id" ]]; then
|
|
||||||
echo "Job: $job (ID: $short_id)"
|
|
||||||
else
|
|
||||||
echo "Job: $job"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get schedule from timer file
|
# Get schedule from timer file
|
||||||
if [[ -f "$timer_file" ]]; then
|
if [[ -f "$timer_file" ]]; then
|
||||||
|
|
@ -690,13 +608,7 @@ list_logs() {
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
for job in "${job_list[@]}"; do
|
for job in "${job_list[@]}"; do
|
||||||
local short_id
|
echo "=== Logs for $(job_id "$job") ==="
|
||||||
short_id=$(get_job_id "$job")
|
|
||||||
if [[ -n "$short_id" ]]; then
|
|
||||||
echo "=== Logs for $job (ID: $short_id) ==="
|
|
||||||
else
|
|
||||||
echo "=== Logs for $job ==="
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Show timer status
|
# Show timer status
|
||||||
if systemctl --user is-active "${job}.timer" &>/dev/null; then
|
if systemctl --user is-active "${job}.timer" &>/dev/null; then
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue