6

in zsh, I can autocomplete hostnames from /etc/hosts, ie:

ssh f<TAB>

will offer completions for hosts starting with f.

This is configured in /usr/share/zsh/functions/Completion/Unix/_hosts:

local ipstrip='[:blank:]#[^[:blank:]]#'

zstyle -t ":completion:${curcontext}:hosts" use-ip && useip=yes
[[ -n $useip ]] && ipstrip=
if (( ${+commands[getent]} )); then
  _cache_hosts=(${(s: :)${(ps:\t:)${(f)~~"$(_call_program hosts getent hosts 2>/dev/null)"}##${~ipstrip}}})
else
  _cache_hosts=(${(s: :)${(ps:\t:)${${(f)~~"$(</etc/hosts)"}%%\#*}##${~ipstrip}}})
fi

....

_hosts=( "$_cache_hosts[@]" )

however, it only works if /etc/hosts file has the format 'IP' 'hostname', ie:

192.168.1.4      foo.mydomain.com

it will not work if IP is missing:

                 foo.mydomain.com

How can I modify the completion script, so that hostnames without IP are also completed?

Completion of hostnames without IP from /etc/hosts works fine in bash_completion. So I am just trying to get the same behavior on zsh.

  • 1
    What makes you think that /etc/hosts and the output of getent hosts actually work like that? – JdeBP Jul 6 '17 at 18:04
  • I put all the hostnames I want to complete into ~/.hosts and then set zstyle -e ':completion:*' hosts 'reply=($(< ~/.hosts))' – thrig Jul 6 '17 at 19:06
  • 3
    The express purpose of /etc/hosts is to associate IP addresses with hostnames. Each line (unless empty or a comment) must have one IP address followed by at least one hostname (the first one should be the canonical hostname, the others are aliases). Lines without IP addresses are not valid. – Adaephon Jul 6 '17 at 20:41
  • 2
    If you disable HashKnownHosts in your ssh configuration (~/.ssh/config or /etc/ssh/ssh_config), than zsh can also use the entries in ~/.ssh/known_hosts for completion (You may have to remove the the current entries in order to repopulate it with plain text host names). – Adaephon Jul 6 '17 at 20:48
3
+500

I'd recommend doing this, which would use your (and the system's) ssh known hosts file instead:

zstyle -e ':completion:*:(ssh|scp|sftp|rsh|rsync):hosts' hosts 'reply=(${=${${(f)"$(cat {/etc/ssh_,~/.ssh/known_}hosts(|2)(N) /dev/null)"}%%[# ]*}//,/ })'

If you're still wanting to use /etc/hosts instead:

strip='[:blank:]#[^[:blank:]]#'
zstyle -e ':completion:*:(ssh|scp|sftp|rsh|rsync):hosts' hosts 'reply=(${(s: :)${(ps:\t:)${${(f)~~"$(</etc/hosts)"}%%\#*}##${~strip}}})'

Best of luck!

  • No thanks, I don't want to use known hosts file. And regarding the second solution, how can I achieve that by modifying '/usr/share/zsh/functions/Completion/Unix/_hosts` ? I don't want to put this into my .zshrc. – 400 the Cat Jul 13 '17 at 12:14
  • You can replace the line _cache_hosts=(${(s: :)${(ps:\t:)${(f)~~"$(_call_program hosts getent hosts 2>/dev/null)"}##${~ipstrip}}}) in _hosts with _cache_hosts=(${(s: :)${(ps:\t:)${${(f)~~"$(</etc/hosts)"}%%\#*}##${~ipstrip}}}) I believe to achieve the desired effect. – ConstipatedNinja Jul 13 '17 at 16:03
0

Add a filter function to /usr/share/zsh/functions/Completion/Unix/_hosts:

provide_missing_ip() {
   while read x ; do 
      set -- $x
      if [ ! "$1" ] || [ "${1%%[^#]*}" ] || [ "$2" ] ; then
          echo "$x"
      else
          ip=`dig +short $2`
          [ "$ip" ] || ip="240.0.0.0"
          printf "%s\t%s\n" $1 $2
      fi
   done
}

How it works:

  1. If there's two entries, or a blank, or a comment, just output those.
  2. If there's just a hostname, try to provide an IP address with dig.
  3. Failing that, (which shouldn't happen, but let's say it's an intentionally bogus hostname for testing purposes), use a harmless dummy IP address of "240.0.0.0", (see Bandrami's answer to "IP address that is the equivalent of /dev/null").

Use the new function to parse the output of getent or < /etc/hosts in the troublesome completion code:

if (( ${+commands[getent]} )); then
  _cache_hosts=(${(s: :)${(ps:\t:)${(f)~~"$(_call_program hosts getent hosts 2>/dev/null | provide_missing_ip )"}##${~ipstrip}}})
else
  _cache_hosts=(${(s: :)${(ps:\t:)${${(f)~~"$(provide_missing_ip </etc/hosts)"}%%\#*}##${~ipstrip}}})
fi
-1

The hosts file must be in the format IP_address canonical_hostname [aliases...], so your approach should be to fix your hosts file, not to get one program snippet working around your broken file. Other programs may run in problems too, if your hosts is broken, so this would be the only sane solution to your use case. If it's a dynamical IP you are targeting, and you do not want to use any of the better workarounds provided above, consider writing a script which dynamically changes your hosts according to this changing address.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.