Files
Linux_post_install/bin/pos-system-bank
T
Your Name 59a4c0e1df
gates / consistency-and-conventions (push) Successful in 26s
feat: add alias creation to pos system bank interactive menu
New 'alias' subcommand (+ POS_SUBCMDS row, menu option 6 'Manage
aliases'): create/update/list/remove bash aliases that run
'pos system bank run <name>', written into ~/.bashrc inside a managed
marker block (BASH_RC_FILE seam for tests, atomic tmp+mv rewrite).

- alias_valid_name enforces ^[a-zA-Z][a-zA-Z0-9_-]*$
- same-name create is an idempotent retarget; last alias removal cleans
  the whole block; outer ~/.bashrc aliases preserved byte-identically
- outer-alias collision refused (file untouched), PATH-shadow non-blocking
  warn, 'bank remove' drops aliases pointing at the removed command
- GNU sed '1,0p' edge case (block at line 1) fixed with guarded ranges;
  regex-quote literalization avoided via glob case payload match
- t-bank.sh +49 checks (71 -> 120); docs: POS.md, howto/system.md,
  tests/README.md, AGENT_TODO.md; make gen byte-idempotent
  (also refreshes the pos-communication-telegram-listener filetable
  line count so the previous commit's tree is gen-consistent)
2026-09-12 13:53:44 -04:00

648 lines
22 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# POS: system bank — Persistent command bank for saving and running shell commands
# POS_SUBCMDS: list add show run edit remove alias
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
source "$(dirname "$0")/../lib/menu-lib.sh" 2>/dev/null || source "$(dirname "$0")/menu-lib.sh"
source "$(dirname "$0")/../lib/bank-lib.sh" 2>/dev/null || source "$(dirname "$0")/bank-lib.sh"
usage() {
cat <<'EOF'
Usage: pos system bank [subcommand] [args]
Persistent command bank for saving and running shell commands.
Subcommands:
list List all saved commands
add <name> [desc] [cmd] Add a new command (interactive if args missing)
show <name> Show command details
run <name> [key=val ...] Run a command (interactive for missing params)
edit <name> Edit an existing command
remove <name> Remove a command
alias <name> [alias_name] Create/update a bash alias for a bank command
alias list List bash aliases in ~/.bashrc
alias remove <alias_name> Remove a bash alias
Interactive menu:
pos system bank (no args on a terminal)
Examples:
pos system bank add disk-info "Disk usage" "df -h && du -sh /mnt/12T"
pos system bank run disk-info
pos system bank run convert-video input=/path/to/video.mp4 quality=23 output=/path/out.mp4
EOF
exit 0
}
case "${1:-}" in
-h|--help) usage ;;
esac
# ── Interactive command picker ───────────────────────────────────
_pick_command() {
bank_load
local count="${#BANK_NAMES[@]}"
if [ "$count" -eq 0 ]; then
warn "Command bank is empty — add one with: pos system bank add <name> \"desc\" \"cmd\""
return 1
fi
local -a items=()
local i
for ((i = 0; i < count; i++)); do
if [ -n "${BANK_DESCS[$i]}" ]; then
items+=("${BANK_NAMES[$i]}${BANK_DESCS[$i]}")
else
items+=("${BANK_NAMES[$i]}")
fi
done
local choice
choice="$(menu_pick "Pick command" "${items[@]}")" || return 1
echo "${BANK_NAMES[$((choice - 1))]}"
}
# ── cmd_list ─────────────────────────────────────────────────────
cmd_list() {
bank_load
local count="${#BANK_NAMES[@]}"
if [ "$count" -eq 0 ]; then
log "Command bank is empty — add one with: pos system bank add <name> \"desc\" \"cmd\""
return 0
fi
echo
echo "COMMAND BANK"
echo
local i
for ((i = 0; i < count; i++)); do
if [ -n "${BANK_DESCS[$i]}" ]; then
printf ' %-25s %s\n' "${BANK_NAMES[$i]}" "${BANK_DESCS[$i]}"
else
printf ' %s\n' "${BANK_NAMES[$i]}"
fi
done
echo
}
# ── cmd_add ──────────────────────────────────────────────────────
cmd_add() {
local name="" desc="" cmd=""
case $# in
0) ;;
1) name="$1" ;;
2) name="$1"; desc="$2" ;;
*) name="$1"; desc="$2"; cmd="$3" ;;
esac
if [ -z "$name" ]; then
name="$(menu_ask_value "Name")" || return 0
fi
if [ -z "$desc" ]; then
desc="$(menu_ask_value "Description" "")" || return 0
fi
if [ -z "$cmd" ]; then
cmd="$(menu_read_value "Command")" || return 0
fi
bank_valid_name "$name" || err "Invalid name: $name (use letters, numbers, hyphens, underscores)"
[ -n "$cmd" ] || err "Command cannot be empty"
bank_load
local i
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
[[ "${BANK_NAMES[$i]}" == "$name" ]] && err "Command '$name' already exists (use 'pos system bank edit' to modify)"
done
bank_add "$name" "$desc" "$cmd"
ok "Saved: $name"
}
# ── cmd_show ─────────────────────────────────────────────────────
cmd_show() {
[ -n "${1:-}" ] || err "Usage: pos system bank show <name>"
local name="$1"
bank_load
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
local cmd_name cmd_desc cmd_cmd
cmd_name="${BANK_NAMES[$BANK_IDX]}"
cmd_desc="${BANK_DESCS[$BANK_IDX]}"
cmd_cmd="${BANK_CMDS[$BANK_IDX]}"
echo
echo "Name: $cmd_name"
echo
echo "Description:"
if [ -n "$cmd_desc" ]; then
echo "$cmd_desc"
else
echo " (none)"
fi
echo
echo "Command:"
echo
echo "$cmd_cmd"
local params
params="$(_extract_params "$cmd_cmd")"
if [ -n "$params" ]; then
echo
echo "Parameters:"
echo "$params" | while IFS= read -r p; do
echo " $p"
done
fi
echo
}
# ── cmd_run ──────────────────────────────────────────────────────
cmd_run() {
local name=""
local -a cli_params=()
if [ $# -eq 0 ]; then
name="$(_pick_command)" || return 0
else
name="$1"; shift
while [ $# -gt 0 ]; do
case "$1" in
*=*) cli_params+=("$1") ;;
*) err "Invalid parameter format: $1 (expected key=value)" ;;
esac
shift
done
fi
bank_load
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
local cmd_name cmd_desc cmd_cmd
cmd_name="${BANK_NAMES[$BANK_IDX]}"
cmd_desc="${BANK_DESCS[$BANK_IDX]}"
cmd_cmd="${BANK_CMDS[$BANK_IDX]}"
# Detect parameters
local params_str
params_str="$(_extract_params "$cmd_cmd")"
local -a params=()
if [ -n "$params_str" ]; then
while IFS= read -r p; do
params+=("$p")
done <<< "$params_str"
fi
# Build parameter values from CLI args
local -A param_values=()
for p in "${cli_params[@]}"; do
local key="${p%%=*}" val="${p#*=}"
param_values["$key"]="$val"
done
# Interactively prompt for missing parameters
local -a missing=()
for p in "${params[@]}"; do
[[ -z "${param_values[$p]+x}" ]] && missing+=("$p")
done
if [ ${#missing[@]} -gt 0 ]; then
for p in "${missing[@]}"; do
local val
val="$(menu_ask_value "$p" "")" || return 0
param_values["$p"]="$val"
done
fi
# Substitute parameters
local final_cmd="$cmd_cmd"
if [ ${#params[@]} -gt 0 ]; then
local -a sub_args=()
for p in "${params[@]}"; do
sub_args+=("${p}=${param_values[$p]:-}")
done
final_cmd="$(_substitute_params "$cmd_cmd" "${sub_args[@]}")"
fi
echo
log "Running: $final_cmd"
echo
eval "$final_cmd"
return $?
}
# ── cmd_edit ─────────────────────────────────────────────────────
cmd_edit() {
local name="${1:-}"
if [ -z "$name" ]; then
name="$(_pick_command)" || return 0
fi
bank_load
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
local old_name old_desc old_cmd
old_name="${BANK_NAMES[$BANK_IDX]}"
old_desc="${BANK_DESCS[$BANK_IDX]}"
old_cmd="${BANK_CMDS[$BANK_IDX]}"
echo
log "Editing: $old_name"
echo
local new_name new_desc new_cmd
new_name="$(menu_ask_value "New name" "$old_name")" || return 0
new_desc="$(menu_ask_value "New description" "$old_desc")" || return 0
new_cmd="$(menu_read_value "New command")" || return 0
[ -z "$new_name" ] && new_name="$old_name"
[ -z "$new_desc" ] && new_desc="$old_desc"
[ -z "$new_cmd" ] && new_cmd="$old_cmd"
if [ "$new_name" != "$old_name" ]; then
bank_valid_name "$new_name" || err "Invalid name: $new_name"
local i found=0
for ((i = 0; i < ${#BANK_NAMES[@]}; i++)); do
[[ "${BANK_NAMES[$i]}" == "$new_name" ]] && found=1
done
[[ "$found" -eq 1 ]] && err "A command named '$new_name' already exists"
bank_remove "$old_name"
bank_add "$new_name" "$new_desc" "$new_cmd"
else
bank_update "$new_name" "$new_desc" "$new_cmd"
fi
ok "Updated: $new_name"
}
# ── cmd_remove ───────────────────────────────────────────────────
cmd_remove() {
local name="${1:-}"
if [ -z "$name" ]; then
name="$(_pick_command)" || return 0
fi
bank_load
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
if [ -t 0 ]; then
confirm "Remove '$name'?" n || { log "Cancelled"; return 0; }
fi
bank_remove "$name"
# Drop bash aliases pointing at the removed command (best-effort upkeep;
# a malformed block is a warning, never a remove blocker).
if [ -f "$BASH_RC_FILE" ] && grep -qF "$ALIAS_START" "$BASH_RC_FILE"; then
if alias_block_parse; then
local -a keep=() i
local removed_alias=0
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
if [[ "${ALIAS_TARGETS[${ALIAS_NAMES[$i]}]:-}" == "$name" ]]; then
removed_alias=1
else
keep+=("${ALIAS_NAMES[$i]}")
fi
done
if [ "$removed_alias" -eq 1 ]; then
ALIAS_NAMES=("${keep[@]}")
alias_block_write
log "Removed alias pointing to '$name' from $BASH_RC_FILE"
fi
else
local rc=$?
[ "$rc" -eq 2 ] && warn "Alias block in $BASH_RC_FILE is malformed — left untouched"
fi
fi
ok "Removed: $name"
}
# ── Alias management ─────────────────────────────────────────────
# Bash aliases for bank commands live in a single managed block inside
# $BASH_RC_FILE (~/.bashrc by default), so they survive rebuilds and are easy
# to see/undo by hand. Every line has the exact shape:
# alias <alias_name>='pos system bank run <bank_name>'
# The block is written atomically (tmp + mv, preserving file permissions).
ALIAS_START='# >>> pos bank aliases (managed by pos system bank — do not hand-edit) <<<'
ALIAS_END='# <<< pos bank aliases (managed by pos system bank) <<<'
BASH_RC_FILE="${BASH_RC_FILE:-$HOME/.bashrc}"
# Valid bash alias names we manage: letters/digits/_/- (same rule as bank names).
alias_valid_name() {
[[ "$1" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]
}
# Parse the managed block from $BASH_RC_FILE into:
# ALIAS_NAMES — ordered array of alias names (file order, unique)
# ALIAS_TARGETS — assoc: alias_name → bank command name
# Returns: 0 = healthy block present, 1 = no block, 2 = malformed block.
alias_block_parse() {
declare -g -a ALIAS_NAMES
declare -g -A ALIAS_TARGETS
ALIAS_NAMES=()
ALIAS_TARGETS=()
[ -f "$BASH_RC_FILE" ] || return 1
local line in_block=0 starts=0 ends=0 rc=0
while IFS= read -r line; do
if [ "$in_block" -eq 0 ]; then
if [[ "$line" == "$ALIAS_START" ]]; then
in_block=1
starts=$((starts + 1))
[ "$starts" -gt 1 ] && rc=2
elif [[ "$line" == "$ALIAS_END" ]]; then
rc=2
fi
elif [[ "$line" == "$ALIAS_END" ]]; then
in_block=0
ends=$((ends + 1))
elif [[ "$line" =~ ^alias[[:space:]]+([a-zA-Z][a-zA-Z0-9_-]*)= ]]; then
# NOTE: no `'` inside the =~ regex (bash treats quotes there as
# quoting operators and would literalize the ( ) groups) — the
# quoted payload is matched with a glob `case` instead.
local a="${BASH_REMATCH[1]}"
local payload="${line#*=}"
case "$payload" in
"'pos system bank run "[a-zA-Z][a-zA-Z0-9_-]*"'")
local t="${payload%\'}"
t="${t#\'pos system bank run }"
if [[ " ${ALIAS_NAMES[*]:-} " == *" $a "* ]]; then
rc=2
continue
fi
ALIAS_NAMES+=("$a")
ALIAS_TARGETS["$a"]="$t"
;;
*) rc=2 ;;
esac
else
rc=2
fi
done < "$BASH_RC_FILE"
[ "$in_block" -eq 1 ] && rc=2
[ "$rc" -eq 0 ] && [ "$starts" -eq 0 ] && return 1
return "$rc"
}
# Require a healthy managed block (alias commands err on missing/malformed).
alias_block_require() {
local rc
if alias_block_parse; then
return 0
else
rc=$?
fi
if [ "$rc" -eq 1 ]; then
err "No alias block in $BASH_RC_FILE — add one with: pos system bank alias <name>"
fi
err "Alias block in $BASH_RC_FILE is malformed (markers or lines were hand-edited) — fix it manually, then retry"
}
# Scan for an alias NAME defined OUTSIDE the managed block (a collision we
# refuse to overwrite). Prints the offending line; rc 0 = found, 1 = clean.
alias_collision() {
local name="$1"
[ -f "$BASH_RC_FILE" ] || return 1
local line in_block=0
while IFS= read -r line; do
if [ "$in_block" -eq 0 ] && [[ "$line" == "$ALIAS_START" ]]; then
in_block=1
continue
fi
if [ "$in_block" -eq 1 ] && [[ "$line" == "$ALIAS_END" ]]; then
in_block=0
continue
fi
if [ "$in_block" -eq 0 ] && [[ "$line" =~ ^[[:space:]]*alias[[:space:]]+$name= ]]; then
printf '%s\n' "$line"
return 0
fi
done < "$BASH_RC_FILE"
return 1
}
# Rewrite $BASH_RC_FILE from ALIAS_NAMES + ALIAS_TARGETS. Callers guarantee the
# file is absent or has at most one healthy block. Removes the block entirely
# when ALIAS_NAMES is empty; appends a fresh block when none exists yet.
alias_block_write() {
local content="" start_line=0 end_line=0 n=0 line
local pre="" post="" dir perms tmp new_content=""
if [ -f "$BASH_RC_FILE" ]; then
content="$(< "$BASH_RC_FILE")"
perms="$(stat -c %a "$BASH_RC_FILE" 2>/dev/null || printf '600')"
while IFS= read -r line; do
n=$((n + 1))
if [ "$start_line" -eq 0 ] && [[ "$line" == "$ALIAS_START" ]]; then
start_line=$n
elif [ "$start_line" -gt 0 ] && [ "$end_line" -eq 0 ] && [[ "$line" == "$ALIAS_END" ]]; then
end_line=$n
fi
done <<< "$content"
if [ "$start_line" -gt 0 ] && [ "$end_line" -eq 0 ]; then
err "Alias block in $BASH_RC_FILE has no end marker — fix it manually, then retry"
fi
else
perms=600
fi
# No block and nothing to manage → never touch the file (nor create it).
if [ "$start_line" -eq 0 ] && [ "${#ALIAS_NAMES[@]}" -eq 0 ]; then
return 0
fi
if [ "$start_line" -gt 0 ]; then
# NOTE: sed "1,0p" is NOT empty in GNU sed (0 is a last-line sentinel,
# it prints line 1) — guard the ranges so edge blocks rewrite cleanly.
if [ "$start_line" -gt 1 ]; then
pre="$(printf '%s\n' "$content" | sed -n "1,$((start_line - 1))p")"
fi
if [ "$end_line" -lt "$n" ]; then
post="$(printf '%s\n' "$content" | sed -n "$((end_line + 1)),\$p")"
fi
else
pre="$(printf '%s\n' "$content")"
fi
if [ -n "$pre" ]; then
new_content+="$pre"
case "$new_content" in
*$'\n') ;;
*) new_content+=$'\n' ;;
esac
fi
if [ "${#ALIAS_NAMES[@]}" -gt 0 ]; then
new_content+="$ALIAS_START"$'\n'
local i
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
new_content+="alias ${ALIAS_NAMES[$i]}='pos system bank run ${ALIAS_TARGETS[${ALIAS_NAMES[$i]}]:-}'"$'\n'
done
new_content+="$ALIAS_END"$'\n'
fi
if [ -n "$post" ]; then
new_content+="$post"
case "$new_content" in
*$'\n') ;;
*) new_content+=$'\n' ;;
esac
fi
dir="$(dirname "$BASH_RC_FILE")"
mkdir -p "$dir" || err "Cannot create directory: $dir"
tmp="$(mktemp "${dir}/.pos-bank-aliases.XXXXXX")" || err "Cannot create temp file in $dir"
printf '%s' "$new_content" > "$tmp"
chmod "$perms" "$tmp"
mv "$tmp" "$BASH_RC_FILE"
}
# ── cmd_alias ────────────────────────────────────────────────────
cmd_alias() {
case "${1:-}" in
list) shift; cmd_alias_list "$@" ;;
remove) shift; cmd_alias_remove "$@" ;;
"") err "Usage: pos system bank alias <name> [alias_name] | list | remove <alias_name>" ;;
*) cmd_alias_add "$@" ;;
esac
}
cmd_alias_add() {
local name="${1:-}" alias_name="${2:-$1}"
[ -n "$name" ] || err "Usage: pos system bank alias <name> [alias_name]"
bank_valid_name "$name" || err "Invalid command name: $name"
alias_valid_name "$alias_name" || err "Invalid alias name: $alias_name (use letters, numbers, hyphens, underscores)"
bank_load
bank_find "$name" >/dev/null 2>&1 || err "Command not found: $name"
local rc
if alias_block_parse; then
rc=0
else
rc=$?
fi
[ "$rc" -eq 2 ] && err "Alias block in $BASH_RC_FILE is malformed (hand-edited?) — fix it manually, then retry"
if [ -f "$BASH_RC_FILE" ] && hit="$(alias_collision "$alias_name")"; then
err "Alias '$alias_name' is already defined outside the managed block in $BASH_RC_FILE: $hit"
fi
if command -v "$alias_name" >/dev/null 2>&1; then
warn "Note: '$alias_name' is also a command on PATH — the alias will shadow it in interactive shells"
fi
local found=0 i
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
[[ "${ALIAS_NAMES[$i]}" == "$alias_name" ]] && found=1
done
[ "$found" -eq 0 ] && ALIAS_NAMES+=("$alias_name")
ALIAS_TARGETS["$alias_name"]="$name"
alias_block_write
ok "Alias '$alias_name' → pos system bank run $name (in $BASH_RC_FILE)"
log "Run: source $BASH_RC_FILE (or open a new shell)"
}
cmd_alias_list() {
alias_block_require
if [ "${#ALIAS_NAMES[@]}" -eq 0 ]; then
log "No aliases in $BASH_RC_FILE — add one with: pos system bank alias <name>"
return 0
fi
echo
echo "BANK ALIASES (in $BASH_RC_FILE)"
echo
local i
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
printf ' %-25s → pos system bank run %s\n' "${ALIAS_NAMES[$i]}" "${ALIAS_TARGETS[${ALIAS_NAMES[$i]}]:-}"
done
echo
}
# ── Interactive alias picker ─────────────────────────────────────
_pick_alias() {
local rc
if alias_block_parse; then
rc=0
else
rc=$?
fi
[ "$rc" -eq 2 ] && err "Alias block in $BASH_RC_FILE is malformed — fix it manually"
[ "$rc" -eq 1 ] && err "No alias block in $BASH_RC_FILE — add one with: pos system bank alias <name>"
if [ "${#ALIAS_NAMES[@]}" -eq 0 ]; then
warn "No aliases yet — add one first"
return 1
fi
local -a items=()
local i
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
items+=("${ALIAS_NAMES[$i]} → pos system bank run ${ALIAS_TARGETS[${ALIAS_NAMES[$i]}]:-}")
done
local choice
choice="$(menu_pick "Pick alias" "${items[@]}")" || return 1
echo "${ALIAS_NAMES[$((choice - 1))]}"
}
cmd_alias_remove() {
local alias_name="${1:-}"
if [ -z "$alias_name" ]; then
if [ -t 0 ]; then
alias_name="$(_pick_alias)" || return 0
else
err "Usage: pos system bank alias remove <alias_name>"
fi
fi
alias_block_require
local found=0 i
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
[[ "${ALIAS_NAMES[$i]}" == "$alias_name" ]] && found=1
done
[ "$found" -eq 1 ] || err "Alias '$alias_name' not found (see: pos system bank alias list)"
if [ -t 0 ]; then
confirm "Remove alias '$alias_name'?" n || { log "Cancelled"; return 0; }
fi
local -a keep=()
for ((i = 0; i < ${#ALIAS_NAMES[@]}; i++)); do
[[ "${ALIAS_NAMES[$i]}" != "$alias_name" ]] && keep+=("${ALIAS_NAMES[$i]}")
done
ALIAS_NAMES=("${keep[@]}")
unset "ALIAS_TARGETS[$alias_name]"
alias_block_write
ok "Removed alias '$alias_name' from $BASH_RC_FILE"
log "Run: source $BASH_RC_FILE (or open a new shell)"
}
alias_menu() {
while true; do
local choice
choice="$(menu_run "Bank Aliases" \
"List aliases" \
"Add alias" \
"Remove alias")" || return 0
case "$choice" in
1) cmd_alias_list ;;
2)
local name alias_name
name="$(_pick_command)" || return 0
alias_name="$(menu_ask_value "Alias name" "$name")" || return 0
[ -z "$alias_name" ] && alias_name="$name"
cmd_alias_add "$name" "$alias_name"
;;
3)
local alias_name
alias_name="$(_pick_alias)" || return 0
cmd_alias_remove "$alias_name"
;;
esac
done
}
# ── Interactive menu ─────────────────────────────────────────────
run_menu() {
menu_guard || exit 1
while true; do
local choice
choice="$(menu_run "Command Bank" \
"List commands" \
"Add command" \
"Run command" \
"Edit command" \
"Remove command" \
"Manage aliases")" || return 0
case "$choice" in
1) cmd_list ;;
2) cmd_add ;;
3) cmd_run ;;
4) cmd_edit ;;
5) cmd_remove ;;
6) alias_menu ;;
esac
done
}
# ── Dispatch ─────────────────────────────────────────────────────
if [ "${1:-}" = "menu" ]; then
run_menu
exit 0
fi
for arg in "$@"; do
case "$arg" in
-h|--help) usage ;;
esac
done
if [ $# -eq 0 ] && [ -t 0 ]; then
run_menu
exit 0
fi
case "${1:-}" in
list) shift; cmd_list "$@" ;;
add) shift; cmd_add "$@" ;;
show) shift; cmd_show "$@" ;;
run) shift; cmd_run "$@" ;;
edit) shift; cmd_edit "$@" ;;
remove) shift; cmd_remove "$@" ;;
alias) shift; cmd_alias "$@" ;;
"") usage ;;
*) err "Unknown subcommand: $1 (see --help)" ;;
esac