2025-09-14 11:29:21 +02:00
|
|
|
#!/usr/bin/env bash
|
2025-09-21 09:45:43 +02:00
|
|
|
# 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.
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# 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
|
2025-09-14 11:29:21 +02:00
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ---------- helpers ----------
|
|
|
|
|
|
2025-09-21 09:45:43 +02:00
|
|
|
# _bt_cfg_files: print existing profiles.ini files in precedence order (high → low).
|
|
|
|
|
# Arguments: none
|
|
|
|
|
# Output: absolute paths, one per line
|
2025-09-20 17:17:26 +02:00
|
|
|
_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"
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-21 09:45:43 +02:00
|
|
|
# _bt_list_profiles: list profile section names (excluding [default]) from all config files.
|
|
|
|
|
# Arguments: none
|
|
|
|
|
# Output: profile names, one per line (deduplicated)
|
2025-09-20 17:17:26 +02:00
|
|
|
_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
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-21 09:45:43 +02:00
|
|
|
# _bt_list_authorized_names: list stored accessor key names from the authorized store.
|
|
|
|
|
# Arguments: none
|
|
|
|
|
# Output: key names (basename without .pub), one per line
|
2025-09-20 17:17:26 +02:00
|
|
|
_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
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-21 09:45:43 +02:00
|
|
|
# _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.
|
2025-09-20 17:17:26 +02:00
|
|
|
_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
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-21 09:45:43 +02:00
|
|
|
# Predicates to distinguish which command is being completed
|
2025-09-20 17:17:26 +02:00
|
|
|
_bt_is_backtunnel_share() { [[ ${COMP_WORDS[0]} == backtunnel-share ]]; }
|
|
|
|
|
_bt_is_backtunnel_access(){ [[ ${COMP_WORDS[0]} == backtunnel-access ]]; }
|
|
|
|
|
|
|
|
|
|
# ---------- main completer ----------
|
2025-09-14 11:29:21 +02:00
|
|
|
|
2025-09-21 09:45:43 +02:00
|
|
|
# _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.
|
2025-09-14 11:29:21 +02:00
|
|
|
_backtunnel_complete() {
|
2025-09-20 17:17:26 +02:00
|
|
|
local cur prev
|
2025-09-14 11:29:21 +02:00
|
|
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
2025-09-20 17:17:26 +02:00
|
|
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
2025-09-14 11:29:21 +02:00
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# 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:
|
2025-09-14 11:29:21 +02:00
|
|
|
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
|
|
|
compopt -o filenames 2>/dev/null
|
2025-09-20 17:17:26 +02:00
|
|
|
mapfile -t COMPREPLY < <(compgen -d -- "$cur")
|
2025-09-14 11:29:21 +02:00
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# Offer "with"/"from" at position 2 depending on command
|
2025-09-14 11:29:21 +02:00
|
|
|
if [[ ${COMP_CWORD} -eq 2 ]]; then
|
2025-09-20 17:17:26 +02:00
|
|
|
if _bt_is_backtunnel_share; then
|
|
|
|
|
mapfile -t COMPREPLY < <(compgen -W "with" -- "$cur")
|
|
|
|
|
else
|
|
|
|
|
mapfile -t COMPREPLY < <(compgen -W "from" -- "$cur")
|
|
|
|
|
fi
|
2025-09-14 11:29:21 +02:00
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# 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")
|
2025-09-14 11:29:21 +02:00
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# 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")
|
2025-09-14 11:29:21 +02:00
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# 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")
|
2025-09-14 11:29:21 +02:00
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# 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")
|
2025-09-14 14:11:36 +02:00
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
if _bt_is_backtunnel_access && [[ ${COMP_CWORD} -ge 3 ]]; then
|
2025-09-14 14:11:36 +02:00
|
|
|
local opts="-p --port -m --mount-point -h --help"
|
2025-09-20 17:17:26 +02:00
|
|
|
mapfile -t COMPREPLY < <(compgen -W "$opts" -- "$cur")
|
2025-09-14 14:11:36 +02:00
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-14 11:29:21 +02:00
|
|
|
COMPREPLY=()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# Register for both commands
|
2025-09-14 11:29:21 +02:00
|
|
|
complete -F _backtunnel_complete backtunnel-share
|
2025-09-21 09:45:43 +02:00
|
|
|
complete -F _backtunnel_complete backtunnel-access
|