feat: -f/-v/-e flags, 5-field TSV with tags in field 2

This commit is contained in:
Lewis Wynne 2026-04-02 15:46:16 +01:00
parent 6b7fef5d2c
commit 2b36640567
9 changed files with 76 additions and 58 deletions

66
nag
View file

@ -304,16 +304,16 @@ do
-h|--help)
_SUBCOMMAND="help"
;;
-e|--edit)
-e)
_SUBCOMMAND="edit"
;;
--version)
-v)
_SUBCOMMAND="version"
;;
--debug)
_USE_DEBUG=1
;;
--yes)
-f)
_YES=1
;;
*)
@ -511,8 +511,8 @@ _next_id() {
#
# Description:
# Extract a field from a TSV alarm line.
# Fields: 1=id, 2=timestamp, 3=rule, 4=message.
# For field 4 (message), returns everything after the 3rd tab.
# Fields: 1=id, 2=tags, 3=timestamp, 4=rule, 5=message.
# For field 5 (message), returns everything after the 4th tab.
_get_alarm_field() {
local _line="${1}"
local _field="${2}"
@ -522,7 +522,7 @@ _get_alarm_field() {
do
_rest="${_rest#*$'\t'}"
done
if (( _field == 4 ))
if (( _field == 5 ))
then
printf "%s" "${_rest}"
else
@ -938,7 +938,7 @@ _parse_time() {
# Description:
# Check whether a systemd user timer for `nag check` is active.
# If not, prompt the user to install one (or install automatically
# when --yes is set). Falls back to cron if systemd is unavailable.
# when -f is set). Falls back to cron if systemd is unavailable.
_prompt_timer() {
# Already running?
if systemctl --user is-active nag.timer &>/dev/null
@ -1056,9 +1056,9 @@ Usage:
${_ME} version show version
Options:
-e, --edit Edit alarms file directly.
--yes Skip all prompts.
--version Show version.
-e Edit alarms file directly.
-f Skip all prompts.
-v Show version.
Environment:
NAG_DIR Alarm storage directory (default: ~/.local/share/nag)
@ -1072,7 +1072,7 @@ HEREDOC
describe "version" <<HEREDOC
Usage:
${_ME} ( version | --version )
${_ME} ( version | -v )
Description:
Display the current program version.
@ -1105,9 +1105,9 @@ list() {
return 0
fi
# Sort alarms by timestamp (field 2).
# Sort alarms by timestamp (field 3).
local -a _sorted
IFS=$'\n' _sorted=($(printf "%s\n" "${_ALARMS[@]}" | sort -t$'\t' -k2 -n))
IFS=$'\n' _sorted=($(printf "%s\n" "${_ALARMS[@]}" | sort -t$'\t' -k3 -n))
IFS=$'\n\t'
local _today _today_epoch
@ -1118,9 +1118,10 @@ list() {
for _line in "${_sorted[@]}"
do
[[ -n "${_line}" ]] || continue
local _id _timestamp _rule _message _human_time _rule_display
local _id _tags _timestamp _rule _message _human_time _rule_display _tag_display
_id="${_line%%$'\t'*}"; local _rest="${_line#*$'\t'}"
_tags="${_rest%%$'\t'*}"; _rest="${_rest#*$'\t'}"
_timestamp="${_rest%%$'\t'*}"; _rest="${_rest#*$'\t'}"
_rule="${_rest%%$'\t'*}"
_message="${_rest#*$'\t'}"
@ -1135,7 +1136,14 @@ list() {
_rule_display=""
fi
printf "[%s] %s%s — %s\\n" "${_id}" "${_human_time}" "${_rule_display}" "${_message}"
if [[ -n "${_tags}" ]]
then
_tag_display=" [${_tags//,/, }]"
else
_tag_display=""
fi
printf "[%s]%s %s%s — %s\\n" "${_id}" "${_tag_display}" "${_human_time}" "${_rule_display}" "${_message}"
done
}
@ -1215,11 +1223,12 @@ skip() {
for _line in "${_ALARMS[@]:-}"
do
[[ -n "${_line}" ]] || continue
local _id _timestamp _rule _message
local _id _tags _timestamp _rule _message
_id="$(_get_alarm_field "${_line}" 1)"
_timestamp="$(_get_alarm_field "${_line}" 2)"
_rule="$(_get_alarm_field "${_line}" 3)"
_message="$(_get_alarm_field "${_line}" 4)"
_tags="$(_get_alarm_field "${_line}" 2)"
_timestamp="$(_get_alarm_field "${_line}" 3)"
_rule="$(_get_alarm_field "${_line}" 4)"
_message="$(_get_alarm_field "${_line}" 5)"
if [[ "${_id}" == "${_target_id}" ]]
then
@ -1228,7 +1237,7 @@ skip() {
then
local _next_ts _human_time
_next_ts="$(_next_occurrence "${_rule}" "${_timestamp}")"
_new_alarms+=("$(printf "%s\t%s\t%s\t%s" "${_id}" "${_next_ts}" "${_rule}" "${_message}")")
_new_alarms+=("$(printf "%s\t%s\t%s\t%s\t%s" "${_id}" "${_tags}" "${_next_ts}" "${_rule}" "${_message}")")
_format_time "${_next_ts}"
_human_time="${REPLY}"
printf "Skipped. Next: %s\\n" "${_human_time}"
@ -1284,11 +1293,12 @@ check() {
do
[[ -n "${_line}" ]] || continue
local _id _timestamp _rule _message
local _id _tags _timestamp _rule _message
_id="$(_get_alarm_field "${_line}" 1)"
_timestamp="$(_get_alarm_field "${_line}" 2)"
_rule="$(_get_alarm_field "${_line}" 3)"
_message="$(_get_alarm_field "${_line}" 4)"
_tags="$(_get_alarm_field "${_line}" 2)"
_timestamp="$(_get_alarm_field "${_line}" 3)"
_rule="$(_get_alarm_field "${_line}" 4)"
_message="$(_get_alarm_field "${_line}" 5)"
if (( _timestamp <= _now ))
then
@ -1317,7 +1327,7 @@ check() {
# Repeating: reschedule.
local _next_ts
_next_ts="$(_next_occurrence "${_rule}" "${_timestamp}")"
_new_alarms+=("$(printf "%s\t%s\t%s\t%s" "${_id}" "${_next_ts}" "${_rule}" "${_message}")")
_new_alarms+=("$(printf "%s\t%s\t%s\t%s\t%s" "${_id}" "${_tags}" "${_next_ts}" "${_rule}" "${_message}")")
fi
# One-shot: drop it (don't add to _new_alarms).
else
@ -1371,7 +1381,7 @@ at() {
# _next_id calls _read_alarms in a subshell, so _ALARMS isn't populated here.
_read_alarms
_ALARMS+=("$(printf "%s\t%s\t\t%s" "${_id}" "${_timestamp}" "${_message}")")
_ALARMS+=("$(printf "%s\t\t%s\t\t%s" "${_id}" "${_timestamp}" "${_message}")")
_write_alarms
_release_lock
@ -1433,7 +1443,7 @@ every() {
# _next_id calls _read_alarms in a subshell, so _ALARMS isn't populated here.
_read_alarms
_ALARMS+=("$(printf "%s\t%s\t%s\t%s" "${_id}" "${_timestamp}" "${_rules_str}" "${_message}")")
_ALARMS+=("$(printf "%s\t\t%s\t%s\t%s" "${_id}" "${_timestamp}" "${_rules_str}" "${_message}")")
_write_alarms
_release_lock
@ -1483,7 +1493,7 @@ unmute() {
describe "edit" <<HEREDOC
Usage:
${_ME} ( edit | -e | --edit )
${_ME} ( edit | -e )
Description:
Open the alarms file in \$EDITOR (falls back to \$VISUAL, then vi).

View file

@ -8,10 +8,10 @@ load test_helper
[[ "${output}" =~ "[1] Tomorrow, 3pm — take a break" ]]
[ -f "${NAG_DIR}/alarms" ]
[ "$(wc -l < "${NAG_DIR}/alarms")" -eq 1 ]
# Verify TSV structure: id<TAB>timestamp<TAB><TAB>message
# Verify TSV structure: id<TAB><empty-tags><TAB>timestamp<TAB><empty-rule><TAB>message
local _line
_line="$(cat "${NAG_DIR}/alarms")"
[[ "${_line}" =~ ^1$'\t'[0-9]+$'\t'$'\t'take\ a\ break$ ]]
[[ "${_line}" =~ ^1$'\t'$'\t'[0-9]+$'\t'$'\t'take\ a\ break$ ]]
}
@test "at is the implicit subcommand" {
@ -68,13 +68,27 @@ load test_helper
run_nag at "${_last_month}" "annual thing"
[ "${status}" -eq 0 ]
local _ts
_ts="$(cut -f2 "${NAG_DIR}/alarms")"
_ts="$(cut -f3 "${NAG_DIR}/alarms")"
local _ts_year _next_year
_ts_year="$(date -d "@${_ts}" +%Y)"
_next_year="$(date -d "+1 year" +%Y)"
[ "${_ts_year}" = "${_next_year}" ]
}
@test "-f flag suppresses prompts" {
run "${_NAG}" -f at "tomorrow 3pm" "flagged alarm"
[ "${status}" -eq 0 ]
[[ "${output}" =~ "flagged alarm" ]]
}
@test "at creates alarm with 5-field TSV (empty tags)" {
run_nag at "tomorrow 3pm" "tagged test"
[ "${status}" -eq 0 ]
local _line
_line="$(cat "${NAG_DIR}/alarms")"
[[ "${_line}" =~ ^1$'\t'$'\t'[0-9]+$'\t'$'\t'tagged\ test$ ]]
}
@test "at without message fails" {
run_nag at "tomorrow 3pm"
[ "${status}" -eq 1 ]

View file

@ -4,7 +4,7 @@ load test_helper
@test "check fires expired one-shot and removes it" {
local _past_ts=$(( $(date +%s) - 60 ))
write_alarm "$(printf "1\t%s\t\tpast alarm" "${_past_ts}")"
write_alarm "$(printf "1\t\t%s\t\tpast alarm" "${_past_ts}")"
# Record what was fired.
local _fired="${NAG_DIR}/fired"
@ -27,7 +27,7 @@ SCRIPT
@test "check reschedules expired repeating alarm" {
local _past_ts=$(( $(date +%s) - 60 ))
write_alarm "$(printf "1\t%s\tday\tdaily alarm" "${_past_ts}")"
write_alarm "$(printf "1\t\t%s\tday\tdaily alarm" "${_past_ts}")"
run_nag check
[ "${status}" -eq 0 ]
@ -35,7 +35,7 @@ SCRIPT
# Alarm should still exist with a future timestamp.
[ -s "${NAG_DIR}/alarms" ]
local _new_ts
_new_ts="$(cut -f2 "${NAG_DIR}/alarms" | head -1)"
_new_ts="$(cut -f3 "${NAG_DIR}/alarms" | head -1)"
local _now
_now="$(date +%s)"
(( _new_ts > _now ))
@ -43,7 +43,7 @@ SCRIPT
@test "check does not fire future alarms" {
local _future_ts=$(( $(date +%s) + 3600 ))
write_alarm "$(printf "1\t%s\t\tfuture alarm" "${_future_ts}")"
write_alarm "$(printf "1\t\t%s\t\tfuture alarm" "${_future_ts}")"
run_nag check
[ "${status}" -eq 0 ]
@ -56,8 +56,8 @@ SCRIPT
@test "check fires all missed alarms" {
local _past1=$(( $(date +%s) - 120 ))
local _past2=$(( $(date +%s) - 60 ))
write_alarm "$(printf "1\t%s\t\tmissed one" "${_past1}")"
write_alarm "$(printf "2\t%s\t\tmissed two" "${_past2}")"
write_alarm "$(printf "1\t\t%s\t\tmissed one" "${_past1}")"
write_alarm "$(printf "2\t\t%s\t\tmissed two" "${_past2}")"
local _fired="${NAG_DIR}/fired"
export NAG_CMD="${NAG_DIR}/recorder"
@ -80,7 +80,7 @@ SCRIPT
@test "check silently drops stale one-shot (older than 15 min)" {
local _stale_ts=$(( $(date +%s) - 1800 )) # 30 minutes ago
write_alarm "$(printf "1\t%s\t\tstale alarm" "${_stale_ts}")"
write_alarm "$(printf "1\t\t%s\t\tstale alarm" "${_stale_ts}")"
local _fired="${NAG_DIR}/fired"
export NAG_CMD="${NAG_DIR}/recorder"
@ -102,7 +102,7 @@ SCRIPT
@test "check silently reschedules stale repeating alarm" {
local _stale_ts=$(( $(date +%s) - 1800 )) # 30 minutes ago
write_alarm "$(printf "1\t%s\tday\tstale repeater" "${_stale_ts}")"
write_alarm "$(printf "1\t\t%s\tday\tstale repeater" "${_stale_ts}")"
local _fired="${NAG_DIR}/fired"
export NAG_CMD="${NAG_DIR}/recorder"
@ -121,6 +121,6 @@ SCRIPT
# Should be rescheduled to a future time.
[ -s "${NAG_DIR}/alarms" ]
local _new_ts
_new_ts="$(cut -f2 "${NAG_DIR}/alarms" | head -1)"
_new_ts="$(cut -f3 "${NAG_DIR}/alarms" | head -1)"
(( _new_ts > $(date +%s) ))
}

View file

@ -23,9 +23,3 @@ load test_helper
[[ "$output" == *"test alarm"* ]]
}
@test "--edit flag invokes edit" {
write_alarm "1 9999999999 test alarm"
EDITOR="cat" run_nag --edit
[[ "$status" -eq 0 ]]
[[ "$output" == *"test alarm"* ]]
}

View file

@ -10,7 +10,7 @@ load test_helper
[[ "${output}" =~ "standup meeting" ]]
# Verify TSV has rule in field 3.
local _rule
_rule="$(cut -f3 "${NAG_DIR}/alarms")"
_rule="$(cut -f4 "${NAG_DIR}/alarms")"
[ "${_rule}" = "weekday" ]
}
@ -26,7 +26,7 @@ load test_helper
run_nag every weekend "tomorrow 3pm" relax
[ "${status}" -eq 0 ]
local _ts _dow
_ts="$(cut -f2 "${NAG_DIR}/alarms")"
_ts="$(cut -f3 "${NAG_DIR}/alarms")"
_dow="$(date -d "@${_ts}" +%u)"
# Day-of-week should be 6 (Sat) or 7 (Sun).
[[ "${_dow}" == "6" || "${_dow}" == "7" ]]

View file

@ -32,7 +32,7 @@ load test_helper
run_nag help version
[ "${status}" -eq 0 ]
[[ "${output}" =~ "Usage:" ]]
[[ "${output}" =~ "( version | --version )" ]]
[[ "${output}" =~ "( version | -v )" ]]
}
@test "help stop shows stop usage" {

View file

@ -70,7 +70,7 @@ load test_helper
@test "list shows Today for alarm due today" {
local _ts=$(( $(date -d "$(date +%Y-%m-%d)" +%s) + 82800 ))
write_alarm "$(printf '1\t%s\t\ttoday alarm' "${_ts}")"
write_alarm "$(printf '1\t\t%s\t\ttoday alarm' "${_ts}")"
run_nag
[ "${status}" -eq 0 ]
[[ "${output}" =~ "Today,".*"today alarm" ]]
@ -78,7 +78,7 @@ load test_helper
@test "list shows Tomorrow for alarm due tomorrow" {
local _ts=$(( $(date -d "$(date +%Y-%m-%d)" +%s) + 86400 + 43200 ))
write_alarm "$(printf '1\t%s\t\ttomorrow alarm' "${_ts}")"
write_alarm "$(printf '1\t\t%s\t\ttomorrow alarm' "${_ts}")"
run_nag
[ "${status}" -eq 0 ]
[[ "${output}" =~ "Tomorrow,".*"tomorrow alarm" ]]
@ -87,7 +87,7 @@ load test_helper
@test "list shows This <day> for alarm 3 days away" {
local _ts=$(( $(date -d "$(date +%Y-%m-%d)" +%s) + 86400 * 3 + 43200 ))
local _day="$(date -d "@${_ts}" +%A)"
write_alarm "$(printf '1\t%s\t\tthis alarm' "${_ts}")"
write_alarm "$(printf '1\t\t%s\t\tthis alarm' "${_ts}")"
run_nag
[ "${status}" -eq 0 ]
[[ "${output}" =~ "This ${_day},".*"this alarm" ]]
@ -96,7 +96,7 @@ load test_helper
@test "list shows Next <day> for alarm 10 days away" {
local _ts=$(( $(date -d "$(date +%Y-%m-%d)" +%s) + 86400 * 10 + 43200 ))
local _day="$(date -d "@${_ts}" +%A)"
write_alarm "$(printf '1\t%s\t\tnext alarm' "${_ts}")"
write_alarm "$(printf '1\t\t%s\t\tnext alarm' "${_ts}")"
run_nag
[ "${status}" -eq 0 ]
[[ "${output}" =~ "Next ${_day},".*"next alarm" ]]
@ -105,7 +105,7 @@ load test_helper
@test "list shows date for alarm 30 days away" {
local _ts=$(( $(date -d "$(date +%Y-%m-%d)" +%s) + 86400 * 30 + 43200 ))
local _date="$(date -d "@${_ts}" "+%a %b %-d")"
write_alarm "$(printf '1\t%s\t\tfar alarm' "${_ts}")"
write_alarm "$(printf '1\t\t%s\t\tfar alarm' "${_ts}")"
run_nag
[ "${status}" -eq 0 ]
[[ "${output}" =~ "${_date},".*"far alarm" ]]

View file

@ -9,7 +9,7 @@ load test_helper
[[ "${output}" =~ "Skipped" ]]
[ -s "${NAG_DIR}/alarms" ]
local _ts _now
_ts="$(cut -f2 "${NAG_DIR}/alarms")"
_ts="$(cut -f3 "${NAG_DIR}/alarms")"
_now="$(date +%s)"
(( _ts > _now ))
}

View file

@ -15,7 +15,7 @@ teardown() {
}
run_nag() {
run "${_NAG}" --yes "$@"
run "${_NAG}" -f "$@"
}
write_alarm() {