fzf icon indicating copy to clipboard operation
fzf copied to clipboard

Feature - replace hostname completion with fzf

Open antofthy opened this issue 4 years ago • 4 comments

  • [x] I have read through the manual page (man fzf)
  • [x] I have the latest version of fzf
  • [x] I have searched through the existing issues

Info

  • OS
    • [x] Linux
    • [ ] Mac OS X
    • [ ] Windows
    • [ ] Etc.
  • Shell
    • [x] bash
    • [ ] zsh
    • [ ] fish

Problem / Steps to reproduce

Proof of concept for integrating "fzf" into bash completions (known_hosts in this case)

The following script that replaces declared the "_known_host_real()" function with one that uses "fzf", for all commands that does hostname completion (like "ssh")

It came about it as I have my own source of "hostnames" of accounts I use that is more accurate that the existing method (extracting them from "~/.ssh/known_hosts" sources. But I did not want to modify the system installed version, which gets updated from packages. My 'source' of hostnames is not included in the script below, but you can replace the call to the original function to be anything you want.

Currently I source this file from my ".bashrc" to replace the "_known_host_real" function. Now when I type "ssh {some partial hostname}" and hit 'TAB' to do hostname completion (as normal). With that "fzf" will pop up the hostname completions, about will abort is you press 'ESC'.

No need to type '**' or any other weird work around, as "fzf" is properly integrated, at least for "known hosts".

#!/bin/echo "Bash source Script!"
#
#  compgen_known_hosts_fzf.bash
#
# Replace "_known_hosts_real()" function with one that uses "fzf"
# that allows the user to interactively select the host they want to use.
#
# To activate source this script from your ".bashrc"
#
# Anthony Thysen <[email protected]>, 9 January 2021
#

# Rename the current "_known_hosts_real" function to "_known_hosts_original".
# The original is declared in "/usr/share/bash-completion/bash_completion",
# and is used by many completions, such as  ssh, rsych, etc.
if type -t _known_hosts_original >/dev/null; then
  : # function has already been redefined
else
  eval "$( echo "_known_hosts_original()";
           declare -f _known_hosts_real | tail -n +2 )"
  # for now make original use the renamed function
  _known_host_real() { _known_hosts_original "$@"; }
fi

# Define a replacement known hosts function that calls "fzf"
_known_hosts_fzf() {
  local suffix prefix
  local OPTIND=1
  local user
  #printf >&2 "_known_hosts_anthony "; printf >&2 '%s ' "$@"; echo >&2
  while getopts "ac46F:p:" flag "$@"; do
      case $flag in
        a) ;;   # aliases from ssh config -- ignore
        c) suffix=':' ;;
        F) configfile=$OPTARG ;;
        p) prefix=$OPTARG ;;
        4) ;;   # ip4 addresses - ignore
        6) ;;   # ip6 addresses - ignore
      esac
  done
  [[ $# -lt $OPTIND ]] &&
    echo "error: $FUNCNAME: missing mandatory argument CWORD"
  cur=${!OPTIND}; let "OPTIND += 1"
  [[ $# -ge $OPTIND ]] &&
    echo "error: $FUNCNAME("$@"): unprocessed arguments:"\
      $(while [[ $# -ge $OPTIND ]]; do printf '%s\n' ${!OPTIND}; shift; done)

  # Extract user name from current word
  [[ $cur == *@* ]] && user=${cur%@*}@ && cur=${cur#*@}

  # Get the original collection of known hosts (no limitations)
  # This can be replaced with whatever source of hostnames you like!
  _known_hosts_original -- ''

  # Add prefix, user and suffix
  for (( i=0; i < ${#COMPREPLY[@]}; i++ )); do
    COMPREPLY[i]="$prefix$user${COMPREPLY[i]}$suffix"
  done

  # Now use "fzf" to do the completion
  # If user aborts (ESC) then print the current query string
  COMPREPLY=( $(
    printf '%s\n' "${COMPREPLY[@]}" |
      FZF_DEFAULT_OPTS='--bind esc:print-query+abort' \
        fzf -q "$cur" -m -i -1 -0
  ) )
  printf '\e[5n'
}

# To redraw line after fzf closes (EG: handle the "printf '\e[5n'" above)
bind '"\e[0n": redraw-current-line'

# Now replace the function with our "fzf" version (if available)...
if type -t fzf >/dev/null; then
  _known_hosts_real() { _known_hosts_fzf "$@"; }
fi

antofthy avatar Jan 09 '21 05:01 antofthy

It came about it as I have my own source of "hostnames" of accounts I use that is more acculturate that the existing method (extracting them from "~/.ssh/known_hosts" sources.

This sounds cool. I second this. (#2366)


Currently I source this file from my ".bashrc" to replace the "_known_host_real" function. now when I type "ssh {some partial hostname}" and hit 'TAB' to do hostname completion (as normal). With that "fzf" will pop up the hostname completions, about will abort is you press 'ESC'.

No need to type '**' or any other weird work around, as "fzf" is properly integrated, at least for "known hosts".

You can probably accomplish this by sourcing this

_fzf_complete_host_notrigger() { FZF_COMPLETION_TRIGGER='' _fzf_host_completion; }

somewhere, as described in https://github.com/junegunn/fzf/wiki/Examples-(completion)#bash-custom-trigger-less-completion

moshiba avatar Feb 21 '21 03:02 moshiba

You can probably accomplish this by sourcing this

_fzf_complete_host_notrigger() { FZF_COMPLETION_TRIGGER='' _fzf_host_completion; }

somewhere, as described in https://github.com/junegunn/fzf/wiki/Examples-(completion)#bash-custom-trigger-less-completion

Nice... didn't know about that, though that does not allow you to use a different source of hostnames, without replacing the original function, anyway!

antofthy avatar Aug 11 '21 01:08 antofthy

Nice, yet I personally would rather want that something like this would be just optional.

I mean some people may want to have their _known_host_real() replaced, but other may rather want to keep that and have the fzf host completion separate from it (and use either of both depending on circumstances).

calestyo avatar Sep 25 '23 00:09 calestyo

Rather than use the the function _known_hosts_fzf() in the above script to replace _known_hosts_real() function, you can use it to set the completion for specific commands. This is actually what I am currently doing now..

It could be better as it assumes ssh command options. BUT it does what I need. for good enough.

PS: r is my personal wrapper around ssh, while v is r but running it in a new xterm

#!/bin/echo "Bash source Script!"
#
# compgen_find_acct.bash
#
# Gather a list of personal hostnames I have accounts on, for completion
# of wrappered commands that make use of these accounts.
#
# This also uses "fzf" to allow the user to interactively select the host they
# want to use.
#
# Anthony Thysen <[email protected]>, 9 January 2021
#
###
#
# FUTURE: more specific "command completion" including options.
#

_find_acct() {
  local cur prev words cword
  _init_completion -n : || return  # do basic var/file completions

  # Do option handle options as per ssh
  if [[ $cur == -* || $prev == -* ]]; then
    type -t _ssh >/dev/null ||
      source /usr/share/bash-completion/completions/ssh
    _ssh 'ssh' "$cur" 'ssh'
    return
  fi

  local user= hosts    # user prefix and hostlist

  # Extract user name from current cur
  [[ $cur == *@* ]] && user=${cur%@*}@ && cur=${cur#*@}

  # My sources of hosts....
  # Start with the host aliases I have defined
  hosts="$( sed 's/#.*//;/^DOMAIN=/d;s/^[^ ]* *//;/^$/d;s/  */\n/g;' \
                ~/lib/host_aliases )"
  # now add my account list, or otherwise the SSH known hosts
  if [ -f ~/misc/dist_accts ]; then
    # Use my distribution accounts list (prefered)
    hosts="$hosts"$'\n'$(awk '/^\]/{print $3}' ~/misc/dist_accts)
  elif [ -f ~/.ssh/known_hosts ]; then
    # Use the ssh known_hosts from this machine (first 'short' name only)
    hosts="$hosts"$'\n'$(
        sed '/^#/d;  s/[[, :].*//; s/\.$//; s/_.*//;' ~/.ssh/known_hosts*
      )
  fi
  
  # selection using "fzf" against available hostnames (tab to select multiple)
  # If user aborts (ESC) then abort and return to original query string
  if type -t fzf >/dev/null; then
    COMPREPLY=( $(
      FZF_DEFAULT_OPTS='--bind esc:print-query+abort' \
        fzf -q "$cur" -m -i -1 -0 <<<"$hosts"
    ) )
    printf '\e[5n'
    return
  fi

  # extract matching hosts using old simplier methods

  # Normal Method - match start only
  #[ "X$COMPREPLY" = "X" ] &&
  #  COMPREPLY=( $( echo "$hosts" | grep -v '-' | egrep "^$cur" ) )

  # try word start for 'cur' in hostnames ('-' separated)
  [ "X$COMPREPLY" = "X" ] &&
     COMPREPLY=( $( echo "$hosts" | egrep "\<$cur" ) )

  # try any match
  [ "X$COMPREPLY" = "X" ] &&
    COMPREPLY=( $( echo "$hosts" | egrep "$cur" ) )

  [ $# -ne 3 ] && echo "${COMPREPLY[@]}"  # direct call output

  # COMPREPLY=( $(compgen -f $cur) )

}

# set the commands that will use this completion.
complete -o nospace -F _find_acct r
complete -o nospace -F _find_acct v
complete -o nospace -F _find_acct cssh
complete -o nospace -F _find_acct do_dist
complete -o nospace -F _find_acct hostinfo

antofthy avatar Sep 29 '23 01:09 antofthy