- Fix bash arithmetic crash on MobaXterm/Cygwin: $(date +%s) was returning CR-tainted values landing in $(( )) operands - Mouse mode off by default; opt in via LARRY_MOUSE=1 or /mouse on - Comprehensive CR-safety sweep across lib/*.sh and larry.sh — every command-substitution result, file read, and user input that feeds an arithmetic context, case dispatcher, or path/header is now CR-stripped at the source New shared helper lib/cygwin-safe.sh defines three primitives: coerce_int VAL [DEFAULT] — for arithmetic / integer-test operands strip_cr VAL — for case patterns, regex tests, paths, headers read_clean VAR [PROMPT] — read -r wrapper that strips CR pre-assign Hardened call sites (14 files, 60+ patch points): - larry.sh: status-line date/tput, 3 y/N approvals, auth menu, API key - lib/oauth.sh: cmd_login + cmd_refresh date+%s captures - lib/nc-engine.sh: 5 y/N action prompts + find|wc arithmetic - lib/nc-msgs.sh: parse_time_ms (4 date sites) + meta-TSV time + MSG_COUNT - lib/nc-regression.sh: tr|wc count + hl7-diff ?-fallback arithmetic - lib/nc-smat-diff.sh: A_COUNT/B_COUNT/DIFFS_TOTAL - lib/nc-insert-protocol.sh: every awk-emitted line number → head/tail math - lib/journal.sh: _next_seq wc -l arithmetic - lib/lessons.sh: _next_id/_count + 2 y/N prompts - lib/hl7-sanitize.sh: cmd_count + clear-table y/N - lib/ssh-helper.sh: 4 local+remote wc -c integer compares - lib/nc-find.sh, lib/nc-table.sh, lib/nc-document.sh, larry-rollback.sh Reproduces the exact error Bryan hit: bash: ...: arithmetic syntax error: invalid arithmetic operator (error token is "") lib/cygwin-safe.sh added to MANIFEST so it auto-syncs on next launch. Co-Authored-By: Clover (Claude Opus 4.7) <noreply@anthropic.com>
132 lines
4.5 KiB
Bash
Executable File
132 lines
4.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# larry-rollback.sh — restore files modified by Larry-Anywhere from journal backups.
|
|
#
|
|
# Usage:
|
|
# larry-rollback.sh --list # show every journal entry, newest first
|
|
# larry-rollback.sh --list --session SESSION # show entries for one session
|
|
# larry-rollback.sh --session SESSION # roll back ALL entries in a session (newest first)
|
|
# larry-rollback.sh --last N # roll back the N most recent entries
|
|
# larry-rollback.sh --entry ENTRY_ID # roll back one specific entry (e.g. 2026-05-26-.../004)
|
|
# larry-rollback.sh --target /path/to/file # roll back all entries that touched this file (newest first)
|
|
# larry-rollback.sh --dry-run # show what would be rolled back without doing it
|
|
#
|
|
# Y/N confirm on every restoration unless --yes is passed.
|
|
set -u
|
|
set -o pipefail
|
|
|
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
|
JOURNAL_ROOT="$LARRY_HOME/journal"
|
|
JOURNAL_INDEX="$JOURNAL_ROOT/index.tsv"
|
|
|
|
DRY=0
|
|
YES=0
|
|
MODE=""
|
|
SESSION=""
|
|
N=""
|
|
ENTRY_ID=""
|
|
TARGET=""
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--list) MODE="list" ;;
|
|
--session) shift; SESSION="$1"; [ -z "$MODE" ] && MODE="session" ;;
|
|
--last) shift; N="$1"; MODE="last" ;;
|
|
--entry) shift; ENTRY_ID="$1"; MODE="entry" ;;
|
|
--target) shift; TARGET="$1"; MODE="target" ;;
|
|
--dry-run) DRY=1 ;;
|
|
--yes|-y) YES=1 ;;
|
|
-h|--help) sed -n '2,16p' "$0"; exit 0 ;;
|
|
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -f "$JOURNAL_INDEX" ] || { echo "no journal (no writes have been made yet)" >&2; exit 1; }
|
|
|
|
C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'; C_RESET=$'\033[0m'
|
|
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'
|
|
|
|
_list_filter() {
|
|
local ses="$1"
|
|
awk -F'\t' -v s="$ses" '
|
|
NR==1 { next }
|
|
s=="" || $2==s { print }
|
|
' "$JOURNAL_INDEX" | tail -r 2>/dev/null || awk -F'\t' -v s="$ses" '
|
|
NR==1 { next }
|
|
s=="" || $2==s { print }
|
|
' "$JOURNAL_INDEX" | awk '{a[NR]=$0} END{for(i=NR;i>=1;i--) print a[i]}'
|
|
}
|
|
|
|
if [ "$MODE" = "list" ]; then
|
|
printf '%stimestamp session-id seq target%s\n' "$C_BOLD" "$C_RESET"
|
|
_list_filter "$SESSION" | awk -F'\t' '{printf " %-26s %-28s %-4s %s\n", $1, $2, $3, $4}'
|
|
exit 0
|
|
fi
|
|
|
|
build_targets() {
|
|
case "$MODE" in
|
|
session)
|
|
[ -n "$SESSION" ] || { echo "--session needs a value" >&2; exit 2; }
|
|
_list_filter "$SESSION"
|
|
;;
|
|
last)
|
|
[ -n "$N" ] || { echo "--last needs N" >&2; exit 2; }
|
|
_list_filter "" | head -n "$N"
|
|
;;
|
|
entry)
|
|
local ses seq
|
|
ses="${ENTRY_ID%/*}"; seq="${ENTRY_ID##*/}"; seq="${seq%%_*}"
|
|
awk -F'\t' -v s="$ses" -v sq="$seq" '$2==s && $3==sq' "$JOURNAL_INDEX"
|
|
;;
|
|
target)
|
|
awk -F'\t' -v t="$TARGET" '$4==t' "$JOURNAL_INDEX" | awk '{a[NR]=$0} END{for(i=NR;i>=1;i--) print a[i]}'
|
|
;;
|
|
*)
|
|
echo "specify --list, --session, --last N, --entry ID, or --target PATH" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
ENTRIES=$(build_targets)
|
|
[ -n "$ENTRIES" ] || { echo "(no matching journal entries)"; exit 0; }
|
|
|
|
printf '%sWill roll back %s entr(y/ies):%s\n' "$C_BOLD" "$(printf '%s\n' "$ENTRIES" | wc -l | tr -d ' ')" "$C_RESET"
|
|
printf '%s' "$ENTRIES" | awk -F'\t' '{printf " %s %-26s %s\n", $3, $1, $4}'
|
|
echo ""
|
|
|
|
if [ "$DRY" = "1" ]; then
|
|
echo "(dry-run; no changes)"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$YES" != "1" ]; then
|
|
printf '%sProceed?%s [y/N]: ' "$C_BOLD" "$C_RESET"
|
|
read -r ans </dev/tty || ans=""
|
|
# v0.7.5: strip CR so `Y\r` from a Cygwin pty matches `^[Yy]$`.
|
|
ans="${ans//$'\r'/}"
|
|
[[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; exit 1; }
|
|
fi
|
|
|
|
printf '%s\n' "$ENTRIES" | while IFS=$'\t' read -r ts ses seq target action orig_sha new_sha backup diffp; do
|
|
[ -z "$target" ] && continue
|
|
if [ "$action" = "create" ]; then
|
|
if [ -e "$target" ]; then
|
|
cp -p "$target" "$target.larry-prerollback.$(date +%s)"
|
|
rm -f "$target" && printf ' %s✓ deleted%s %s (was newly created)\n' "$C_GREEN" "$C_RESET" "$target"
|
|
else
|
|
printf ' %s○ already absent%s %s\n' "$C_DIM" "$C_RESET" "$target"
|
|
fi
|
|
else
|
|
if [ -f "$backup" ]; then
|
|
cp -p "$target" "$target.larry-prerollback.$(date +%s)" 2>/dev/null || true
|
|
cp -p "$backup" "$target" && printf ' %s✓ restored%s %s ← %s\n' "$C_GREEN" "$C_RESET" "$target" "$backup"
|
|
else
|
|
printf ' %s✗ backup missing for%s %s (looked at %s)\n' "$C_RED" "$C_RESET" "$target" "$backup"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "Pre-rollback copies left at <target>.larry-prerollback.<ts> in case you want to redo. Clean up at your leisure."
|