#!/usr/bin/env bash set -euo pipefail # POS: system alias — Manage persistent command aliases (wrapper scripts in ~/.local/bin/) # POS_SUBCMDS: create edit remove list show 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" # ── Paths & constants ────────────────────────────────────────── ENV_FILE="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}/aliases.env" # ── Core helpers ─────────────────────────────────────────────── # Load aliases from ENV_FILE into parallel arrays. # Loop vars use _a* prefix to avoid dynamic-scope collisions with callers. _alias_load() { _ALIAS_NAMES=(); _ALIAS_COMMANDS=() [ -f "$ENV_FILE" ] || return 0 local _an _ac while IFS='|' read -r _an _ac; do [[ "$_an" =~ ^[[:space:]]*# ]] && continue [[ -z "${_an// /}" ]] && continue _an="${_an## }"; _an="${_an%% }" [[ "$_an" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] || continue _ac="${_ac## }"; _ac="${_ac%% }" _ALIAS_NAMES+=("$_an") _ALIAS_COMMANDS+=("$_ac") done < <(grep -v '^[[:space:]]*#' "$ENV_FILE" | grep -v '^[[:space:]]*$' || true) } # Save parallel arrays back to ENV_FILE (atomic overwrite). _alias_save() { mkdir -p "$(dirname "$ENV_FILE")" { printf '%s\n' "# System aliases — managed by pos system alias (do not hand-edit)" printf '%s\n' "# Format: alias_name|command" local i for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do printf '%s|%s\n' "${_ALIAS_NAMES[$i]}" "${_ALIAS_COMMANDS[$i]}" done } >"$ENV_FILE" chmod 600 "$ENV_FILE" } # ── Wrapper scripts ──────────────────────────────────────────── _wrapper_path() { printf '%s/.local/bin/%s' "$HOME" "$1" } # Ownership test: line 2 must carry our generator marker. _alias_owned() { [ -f "$1" ] && sed -n '2p' "$1" 2>/dev/null | grep -q 'Managed by pos system alias' } # Render one wrapper to stdout (args: name command). _wrapper_render() { local name="$1" command="$2" cat <"$tmp" if cmp -s "$tmp" "$path" 2>/dev/null; then rm -f "$tmp" return 0 fi if ! bash -n "$tmp" 2>/dev/null; then warn "Wrapper for '$name' failed syntax check — keeping previous version" >&2 rm -f "$tmp" return 1 fi mv "$tmp" "$path" chmod 755 "$path" } # rc 0 iff ~/.local/bin is on PATH. _alias_check_path() { case ":$PATH:" in *":$HOME/.local/bin:"*) return 0 ;; *) return 1 ;; esac } # Two-way reconciliation on every invocation: # forward: each ENV entry → render-diff-install # reverse: owned wrappers whose name is not in ENV → deleted # plus: PATH guidance when owned wrappers exist but ~/.local/bin is absent _alias_sync() { _alias_load local bin_dir="${HOME}/.local/bin" i name f base match any=0 mkdir -p "$bin_dir" for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do _wrapper_install "${_ALIAS_NAMES[$i]}" "${_ALIAS_COMMANDS[$i]}" || : done for f in "$bin_dir"/*; do [ -f "$f" ] || continue _alias_owned "$f" || continue base="${f##*/}" match=0 for name in ${_ALIAS_NAMES[@]+"${_ALIAS_NAMES[@]}"}; do [ "$base" = "$name" ] && { match=1; break; } done [ "$match" -eq 1 ] || rm -f "$f" done if ! _alias_check_path; then for f in "$bin_dir"/*; do [ -f "$f" ] && _alias_owned "$f" && { any=1; break; } done if [ "$any" -eq 1 ]; then warn "~/.local/bin is not on your PATH — alias scripts will not resolve by name." warn " Fix now: export PATH=\"\$HOME/.local/bin:\$PATH\"" warn " Persist it: echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.profile" fi fi return 0 } _alias_find() { local name="$1" i for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do if [ "${_ALIAS_NAMES[$i]}" = "$name" ]; then echo "$i" return 0 fi done echo "-1" return 0 } _alias_name_valid() { [[ "$1" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]] } # Truncate a command string for display (inline pipes, redirects). _command_truncate() { local s="$1" max="${2:-42}" s="${s//$'\n'/ }" if [ ${#s} -gt "$max" ]; then printf '%s…' "${s:0:max}" else printf '%s' "$s" fi } # ── Non-interactive output ───────────────────────────────────── _alias_table() { local count=${#_ALIAS_NAMES[@]} i [ "$count" -eq 0 ] && return 0 printf ' %-16s %s\n' "Name" "Command" printf ' %-16s %s\n' "----------------" "----------------------------------------" for ((i = 0; i < count; i++)); do printf ' %-16s %s\n' "${_ALIAS_NAMES[$i]}" "$(_command_truncate "${_ALIAS_COMMANDS[$i]}" 60)" done } _alias_list() { printf 'Aliases (%d):\n' "${#_ALIAS_NAMES[@]}" _alias_table } _alias_show() { local idx idx="$(_alias_find "$1")" [ "$idx" = "-1" ] && err "Alias '$1' not found" local name="${_ALIAS_NAMES[$idx]}" command="${_ALIAS_COMMANDS[$idx]}" printf ' %-12s %s\n' "Alias:" "$name" printf ' %-12s %s\n' "Command:" "$command" if _alias_check_path; then printf ' %-12s %s\n' "Wrapper:" "$(_wrapper_path "$name")" else printf ' %-12s %s\n' "Wrapper:" "(not installed — ~/.local/bin not on PATH)" fi printf ' %-12s %s\n' "Test:" "$name" } # ── Interactive: main menu ───────────────────────────────────── _alias_menu() { menu_guard || return 1 while true; do { _alias_load if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then echo "${YELLOW}[!] No aliases defined yet — create one with option 1.${RESET}" else _alias_table printf ' %d alias(es)\n' "${#_ALIAS_NAMES[@]}" fi echo >&2 } >&2 local choice choice="$(menu_run "System Aliases" "Create new alias" "Edit existing alias" \ "Remove alias" "List aliases")" || return 0 case "$choice" in 1) _alias_create ;; 2) _alias_edit ;; 3) _alias_remove ;; 4) : ;; # List aliases — the loop's pre-render IS the current table esac done } # ── Interactive: create ──────────────────────────────────────── _alias_create() { local preset_name="${1:-}" section "Create System Alias" >&2 # Step 1: Alias name local name="$preset_name" while true; do if [ -z "$name" ]; then step 1 2 "Alias Name" >&2 name="$(menu_ask_value "Alias name" "")" || return 0 fi [ -z "$name" ] && { warn "Alias name cannot be empty" >&2; name=""; continue; } if ! _alias_name_valid "$name"; then warn "Invalid name '$name' — use letters, digits, hyphens, underscores (start with a letter)" >&2 name=""; continue fi _alias_load local existing existing="$(_alias_find "$name")" if [ "$existing" != "-1" ]; then warn "Alias '$name' already exists — use 'pos system alias edit $name' instead" >&2 [ -n "$preset_name" ] && return 1 name=""; continue fi # Collision: wrapper exists without our marker → refuse local wpath wpath="$(_wrapper_path "$name")" if [ -e "$wpath" ]; then _alias_owned "$wpath" || err "File '~/.local/bin/$name' already exists and was not created by pos system alias — pick another name" fi # Collision: name resolves to another binary on PATH → refuse if command -v "$name" >/dev/null 2>&1; then err "'$name' already exists on PATH as $(command -v "$name") — pick another name" fi break done # Step 2: Command local command="" while true; do step 2 2 "Command" >&2 command="$(menu_ask_value "Command to execute" "")" || return 0 [ -z "$command" ] && { warn "Command cannot be empty" >&2; continue; } [[ "$command" == *'|'* ]] || break warn "Command must not contain '|' characters" >&2 command="" done # Confirmation { echo "────────────────────────────────────────────" printf ' Create alias '\''%s'\''?\n' "$name" printf ' Command: %s\n' "$command" echo "────────────────────────────────────────────" } >&2 if ! confirm "Create alias '$name'?" y; then log "Aborted." >&2 return 0 fi _alias_load _ALIAS_NAMES+=("$name") _ALIAS_COMMANDS+=("$command") _alias_save _alias_sync log "Alias '$name' created." >&2 log "Test it: $name" >&2 log "Available immediately: $(_wrapper_path "$name")" >&2 } # ── Interactive: edit ────────────────────────────────────────── _alias_edit() { local preset_name="${1:-}" _alias_load if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then warn "No aliases to edit — create one first" >&2 return 0 fi local name="$preset_name" if [ -z "$name" ]; then section "Edit System Alias" >&2 local display_items=() i for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do local c="${_ALIAS_COMMANDS[$i]}" if [ ${#c} -gt 30 ]; then c="${c:0:30}…" fi display_items+=("${_ALIAS_NAMES[$i]} → ${c}") done local picked picked="$(menu_pick "Pick alias to edit" "${display_items[@]}")" || return 0 name="${_ALIAS_NAMES[$((picked - 1))]}" fi local idx idx="$(_alias_find "$name")" if [ "$idx" = "-1" ]; then err "Alias '$name' not found" fi # Show current values { echo " Current values for '$name':" printf ' Command: %s\n' "${_ALIAS_COMMANDS[$idx]}" echo >&2 } >&2 local new_command="${_ALIAS_COMMANDS[$idx]}" local changed=0 # Edit command step 1 1 "Command" >&2 local default_display="${_ALIAS_COMMANDS[$idx]}" [ ${#default_display} -gt 60 ] && default_display="${default_display:0:60}…" local tmp_command tmp_command="$(menu_ask_value "Command to execute" "$default_display")" || return 0 if [ -n "$tmp_command" ]; then if [[ "$tmp_command" == *'|'* ]]; then warn "Command must not contain '|' characters" >&2 return 0 fi if [ "$tmp_command" != "${_ALIAS_COMMANDS[$idx]}" ]; then new_command="$tmp_command" changed=1 fi fi # No changes? if [ "$changed" -eq 0 ]; then log "No changes — nothing to save." >&2 return 0 fi # Show diff summary { echo "────────────────────────────────────────────" printf ' Save changes to '\''%s'\''?\n' "$name" local tag_c [ "$new_command" = "${_ALIAS_COMMANDS[$idx]}" ] && tag_c="(unchanged)" || tag_c="(changed)" printf ' Command: %s %s\n' "$new_command" "$tag_c" echo "────────────────────────────────────────────" } >&2 if ! confirm "Save changes to '$name'?" y; then log "Discarded." >&2 return 0 fi _alias_load _ALIAS_COMMANDS[$idx]="$new_command" _alias_save _alias_sync log "Alias '$name' updated — the change is live on next invocation." >&2 } # ── Interactive: remove ──────────────────────────────────────── _alias_remove() { local preset_name="${1:-}" _alias_load if [ ${#_ALIAS_NAMES[@]} -eq 0 ]; then warn "No aliases to remove" >&2 return 0 fi local name="$preset_name" if [ -z "$name" ]; then section "Remove System Alias" >&2 local display_items=() i for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do local c="${_ALIAS_COMMANDS[$i]}" if [ ${#c} -gt 30 ]; then c="${c:0:30}…" fi display_items+=("${_ALIAS_NAMES[$i]} → ${c}") done local picked picked="$(menu_pick "Pick alias to remove" "${display_items[@]}")" || return 0 name="${_ALIAS_NAMES[$((picked - 1))]}" fi local idx idx="$(_alias_find "$name")" if [ "$idx" = "-1" ]; then err "Alias '$name' not found" fi # Show alias detail { echo " Alias: $name" printf ' Command: %s\n' "${_ALIAS_COMMANDS[$idx]}" echo >&2 } >&2 if ! confirm "Remove alias '$name'? This cannot be undone." n; then log "Cancelled." >&2 return 0 fi _alias_load local new_names=() new_commands=() i for ((i = 0; i < ${#_ALIAS_NAMES[@]}; i++)); do if [ "${_ALIAS_NAMES[$i]}" != "$name" ]; then new_names+=("${_ALIAS_NAMES[$i]}") new_commands+=("${_ALIAS_COMMANDS[$i]}") fi done _ALIAS_NAMES=("${new_names[@]+"${new_names[@]}"}") _ALIAS_COMMANDS=("${new_commands[@]+"${new_commands[@]}"}") _alias_save _alias_sync log "Alias '$name' removed — script deleted from $(_wrapper_path "$name")." >&2 log "If the name still autocompletes stale in this shell, run: hash -r" >&2 } # ── Usage ────────────────────────────────────────────────────── usage() { cat <<'EOF' Usage: pos system alias [subcommand] [args] Manage persistent command aliases — create, edit, remove, list, and show named aliases. Each alias maps a name to a shell command, materialized as an executable wrapper script in ~/.local/bin/. Subcommands: (no args) Interactive menu create [name] Create a new alias (interactive prompts) edit [name] Edit an existing alias (interactive, Enter = keep) remove [name] Remove an alias (with confirmation) list List all aliases (non-interactive, machine-readable) show Show one alias's details Activation: every alias is materialized as an executable script at ~/.local/bin/, synced automatically on every invocation. Changes are live on the next invocation. Options: -h|--help Show this help. Examples: pos system alias # interactive menu pos system alias list # show all aliases pos system alias create # interactive create pos system alias create restart-dns # create 'restart-dns' alias pos system alias edit restart-dns # edit the 'restart-dns' alias pos system alias remove restart-dns # remove 'restart-dns' (with confirm) pos system alias show restart-dns # show alias details EOF exit 0 } # ── Main dispatch ────────────────────────────────────────────── # Every subcommand syncs first: artifacts always equal ENV truth before any # subcommand logic runs. case "${1:-}" in -h|--help) usage ;; create) shift; _alias_sync; _alias_create "${1:-}" ;; edit) shift; _alias_sync; _alias_edit "${1:-}" ;; remove) shift; _alias_sync; _alias_remove "${1:-}" ;; list) _alias_sync; _alias_list ;; show) [ -n "${2:-}" ] || err "Usage: pos system alias show " _alias_sync _alias_show "$2" ;; "") _alias_sync; _alias_menu ;; *) err "Unknown subcommand '$1' (use -h for help)" ;; esac