Introduce `backtunnel-umount` as a portable unmount helper, preferring `fusermount3`, `fusermount`, or `umount`. Add `BACKTUNNEL_HOSTKEY_POLICY` for configurable host key handling in `backtunnel-share` and `backtunnel-access`. Update TUIs for remote folder prompts and mount point handling. Enhance bash completion for TUI commands with directory suggestions. Revamp terminal selection logic in `backtunnel-open-term` to prioritize modern emulators like wezterm. Extend tests with scaffolds for host key policy and unmount behavior. Update README with new scripts, workflows, features, and troubleshooting tips.
270 lines
8.8 KiB
Bash
270 lines
8.8 KiB
Bash
#!/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
|
|
|
|
# Minimal TUI completers: complete only the first positional as a directory
|
|
_backtunnel_access_tui_complete() {
|
|
local cur
|
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
compopt -o filenames 2>/dev/null
|
|
mapfile -t COMPREPLY < <(compgen -d -- "$cur")
|
|
else
|
|
COMPREPLY=()
|
|
fi
|
|
}
|
|
_backtunnel_share_tui_complete() {
|
|
local cur
|
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
compopt -o filenames 2>/dev/null
|
|
mapfile -t COMPREPLY < <(compgen -d -- "$cur")
|
|
else
|
|
COMPREPLY=()
|
|
fi
|
|
}
|
|
|
|
# Register TUI completion
|
|
complete -F _backtunnel_access_tui_complete backtunnel-access-tui
|
|
complete -F _backtunnel_share_tui_complete backtunnel-share-tui |