#!/usr/bin/env bash # Name: BackTunnel Bash Completion # Summary: Programmable completion for backtunnel-share and backtunnel-access. # Description: # Provides contextual tab-completion for BackTunnel CLI tools, including positional # scaffolding ("with"/"from", "for", duration suggestions), @profile expansion, # SSH host suggestions from known_hosts and ~/.ssh/config, key-name and *.pub completion, # and directory completion for mount paths. # # Usage: # - Source in your shell (for current session): # source /path/to/backtunnel.bash # - Install system-wide or in your completion.d directory (auto-sourced by bash-completion). # # Dependencies: # - bash with programmable completion (bash-completion recommended) # - awk (for profile parsing) # # Compatibility: # - Targets Bash completion v1 (COMP_WORDS, COMPREPLY, compgen, compopt). # # Notes: # - Reads profiles from ${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/profiles.ini, # /etc/backtunnel/profiles.ini, and /usr/share/backtunnel/profiles.ini when present. # - Best-effort parsing and host extraction; hashed known_hosts entries are ignored. # BackTunnel bash completion for: # - backtunnel-share # - backtunnel-access # # Features: # - Positional scaffolding: "with"/"from" / "for" and duration suggestions # - @profile completion from profiles.ini (user, system, packaged) # - --allow-key completes *.pub files # - --allow-known completes names from ~/.config/backtunnel/authorized/*.pub # - SSH host completion from known_hosts and ssh config Host entries # - Path completion for first positional and --invite-mount / --mount-point # ---------- helpers ---------- # _bt_cfg_files: print existing profiles.ini files in precedence order (high → low). # Arguments: none # Output: absolute paths, one per line _bt_cfg_files() { local u="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/profiles.ini" local s="/etc/backtunnel/profiles.ini" local p="/usr/share/backtunnel/profiles.ini" [[ -f "$u" ]] && printf '%s\n' "$u" [[ -f "$s" ]] && printf '%s\n' "$s" [[ -f "$p" ]] && printf '%s\n' "$p" } # _bt_list_profiles: list profile section names (excluding [default]) from all config files. # Arguments: none # Output: profile names, one per line (deduplicated) _bt_list_profiles() { # Output section names excluding [default] local f while IFS= read -r f; do awk ' /^\[[^]]+\]/ { name=$0; sub(/^\[/,"",name); sub(/\]$/,"",name); if (name != "default") print name }' "$f" done < <(_bt_cfg_files) 2>/dev/null | sort -u } # _bt_list_authorized_names: list stored accessor key names from the authorized store. # Arguments: none # Output: key names (basename without .pub), one per line _bt_list_authorized_names() { # From ~/.config/backtunnel/authorized/*.pub → basename w/o .pub local d="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/authorized" [[ -d "$d" ]] || return 0 local f shopt -s nullglob for f in "$d"/*.pub; do f="${f##*/}"; printf '%s\n' "${f%.pub}" done } # _bt_list_ssh_hosts: gather SSH host candidates from known_hosts and ~/.ssh/config. # Arguments: none # Output: hostnames (best-effort), one per line # Notes: skips hashed known_hosts entries and wildcards; filters out plain IP literals. _bt_list_ssh_hosts() { # Collect hosts from known_hosts and ~/.ssh/config Host entries (best effort) local out=() local kh="$HOME/.ssh/known_hosts" local cfg="$HOME/.ssh/config" local f if [[ -f "$kh" ]]; then # First field may be comma-separated list; ignore IP-literals in brackets # and strip hashed or bracketed entries. awk -F'[ ,]' ' { if ($1 ~ /^\|1\|/) next; # hashed, skip host=$1 gsub(/^\[/,"",host); gsub(/\]$/,"",host) if (host ~ /^[0-9.]+$/) next; # plain IPv4 if (host ~ /^[0-9a-fA-F:]+$/) next; # plain IPv6 if (host ~ /^\*/) next; # wildcard print host }' "$kh" fi if [[ -f "$cfg" ]]; then awk ' tolower($1)=="host" { for (i=2; i<=NF; i++) if ($i !~ /\*/ ) print $i }' "$cfg" fi } # Predicates to distinguish which command is being completed _bt_is_backtunnel_share() { [[ ${COMP_WORDS[0]} == backtunnel-share ]]; } _bt_is_backtunnel_access(){ [[ ${COMP_WORDS[0]} == backtunnel-access ]]; } # ---------- main completer ---------- # _backtunnel_complete: top-level completion function for both commands. # Arguments: # Uses global completion variables: COMP_WORDS, COMP_CWORD # Output: # Sets COMPREPLY array with candidates appropriate to the current cursor position. _backtunnel_complete() { local cur prev cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" # Common positional flow: # share: [0]cmd [1]/path [2]with [3]remote|@prof [4]for [5]duration ... # access: [0]cmd [1]/path [2]from [3]remote ... # # Provide directory completion for the first positional: if [[ ${COMP_CWORD} -eq 1 ]]; then compopt -o filenames 2>/dev/null mapfile -t COMPREPLY < <(compgen -d -- "$cur") return 0 fi # Offer "with"/"from" at position 2 depending on command if [[ ${COMP_CWORD} -eq 2 ]]; then if _bt_is_backtunnel_share; then mapfile -t COMPREPLY < <(compgen -W "with" -- "$cur") else mapfile -t COMPREPLY < <(compgen -W "from" -- "$cur") fi return 0 fi # For backtunnel-share: position 3 (REMOTE | @profile) if _bt_is_backtunnel_share && [[ ${COMP_CWORD} -eq 3 && ${COMP_WORDS[2]} == "with" ]]; then # Build candidates: @profiles + hosts from ssh files local profs hosts profs=$( _bt_list_profiles 2>/dev/null | sed 's/^/@/' ) hosts=$( _bt_list_ssh_hosts 2>/dev/null ) mapfile -t COMPREPLY < <(compgen -W "$profs $hosts" -- "$cur") return 0 fi # For backtunnel-share: position 4 expects "for" if _bt_is_backtunnel_share && [[ ${COMP_CWORD} -eq 4 && ${COMP_WORDS[2]} == "with" ]]; then mapfile -t COMPREPLY < <(compgen -W "for" -- "$cur") return 0 fi # For backtunnel-share: position 5 duration if _bt_is_backtunnel_share && [[ ${COMP_CWORD} -eq 5 && ${COMP_WORDS[2]} == "with" && ${COMP_WORDS[4]} == "for" ]]; then mapfile -t COMPREPLY < <(compgen -W "10m 30m 1h 2h 6h 12h 1d 2d" -- "$cur") return 0 fi # For backtunnel-access: position 3 remote (after "from") if _bt_is_backtunnel_access && [[ ${COMP_CWORD} -eq 3 && ${COMP_WORDS[2]} == "from" ]]; then local hosts hosts=$( _bt_list_ssh_hosts 2>/dev/null ) mapfile -t COMPREPLY < <(compgen -W "$hosts" -- "$cur") return 0 fi # ----- Option-value specific completions ----- case "$prev" in -p|--tunnel-port|--port) mapfile -t COMPREPLY < <(compgen -W "2222 4422 5522 6622" -- "$cur") return 0 ;; -l|--local-ssh-port) mapfile -t COMPREPLY < <(compgen -W "22 2222" -- "$cur") return 0 ;; --invite-mount|-m|--mount-point) compopt -o filenames 2>/dev/null mapfile -t COMPREPLY < <(compgen -d -- "$cur") return 0 ;; --allow-key) # complete .pub files compopt -o filenames 2>/dev/null # shellcheck disable=SC2207 COMPREPLY=($(compgen -f -- "$cur")) # filter to *.pub only (keep if user is typing a dir) local i out=() for i in "${COMPREPLY[@]}"; do [[ -d "$i" || "$i" == *.pub ]] && out+=("$i") done COMPREPLY=("${out[@]}") return 0 ;; --allow-known) mapfile -t COMPREPLY < <(compgen -W "$(_bt_list_authorized_names)" -- "$cur") return 0 ;; esac # ----- Generic options after positionals ----- if _bt_is_backtunnel_share && [[ ${COMP_CWORD} -ge 5 ]]; then local opts=" -p --tunnel-port -l --local-ssh-port -i --invite --invite-mount --invite-file --qr --allow-key --allow-known -h --help" mapfile -t COMPREPLY < <(compgen -W "$opts" -- "$cur") return 0 fi if _bt_is_backtunnel_access && [[ ${COMP_CWORD} -ge 3 ]]; then local opts="-p --port -m --mount-point -h --help" mapfile -t COMPREPLY < <(compgen -W "$opts" -- "$cur") return 0 fi COMPREPLY=() } # Register for both commands complete -F _backtunnel_complete backtunnel-share complete -F _backtunnel_complete backtunnel-access