8

suppose I have following entry in my /etc/hosts

192.168.1.10      server1.mydomain.com

and I have a directory SERVER-FILES in current dir. I want to scp the directory SERVER-FILES somewhere. I type SE and use autocompletion to complete the directory name:

$ scp -rp SE<TAB>

This completion should be totally unambiguous. But zsh autocomplletion tries to be too smart, and treats hostnames case-insensitive, and thus attempts to match SE to hostnames:

$ scp -rp SE<TAB>
SERVER-FILES/
server1.mydomain.com

How can I disable this annoying feature, where zsh is trying to match hostnames case-insensitive, and therefore completes SE<TAB> to server1.mydomain.com` ?

UPDATE:

Based on suggestions from @zeppelin, I have changed the following line in the ssh completion file Unix/_ssh:

- compadd -M 'm:{a-zA-Z}={A-Za-z} r:|.=* r:|=*' "$@" $config_hosts
+ compadd "$@" $config_hosts

but that did not help. It has absolutely no effect.

And I don't understand the answer from @Tomasz Pala. My zsh completion is not case-insensitive.

Please sopmebody just tell me what I need to change in /usr/share/zsh/functions/Completion/Unix/_foo to change this behaviour.

UPDATE 2

I have finally narrowed the problem down, and found out why the solution from @Tomasz Pala did work for him, but not for me:

When I change the Unix/_hosts file on a newly setup machine/user account, the solution works.

scp -r SE<TAB>

The above command ignores server1.mydomain.com in /etc/hosts, and only offers local directory SERVER-FILES for completion.

But this does not work for me on my existing user account, because I have server.mydomain.com in my ~/.ssh/config. When I remove the entry, then everything works as desired.

But how can I make this hack work even with my current ~/.ssh/config ?

11
  • does this work for you? zstyle ':completion::complete:scp:*' hosts off Nov 13, 2016 at 12:22
  • scp could complete either a filename or a hostname at that position, so matches both the directory and the hostname.
    – thrig
    Nov 13, 2016 at 15:25
  • I am not trying to disable hostname completion. I just want to disable the nonsense feature of case insensitive hostname completion. Nov 14, 2016 at 9:03
  • I think that you would have to write your own matcher for that. As said before, at this argument position both hosts and files are possible and zsh proposition seems correct. You could start typing your argument with ./SE to tell zsh that you mean a file.
    – Kalavan
    Nov 16, 2016 at 7:53
  • 1
    @Marlon Richert - I have pasted the relevant parts of my .zshrc. Please see my UPDATE 2. I am not using any framework, just plain .zshrc. The .zshrc now contains the added rules for ssh and scp. Currently, when I type scp S<TAB> it completes server from my hosts file. Same for ssh S<TAB> where it also completes server. I would like to change that, so that scp -rp S<TAB> only completes local file matching S*, or if no such file exists, does not complete anything. Aug 4, 2020 at 3:56

4 Answers 4

4
+225

Second answer tries to explain that you need to do two things:

1_ make sure your general matching rules are not case-insensitive (matcher-list) - from the updated question it's not,

2_ change Unix/(Type/)_hosts (the actual location might vary, but not the Unix/_ssh - this one handles ~/.ssh/config hosts, see below) last 2 lines to:

_wanted hosts expl host \
   compadd -M 'm:{a-z}={A-Z} r:|.=* r:|=*' -a "$@" - _hosts

All of this was already summarized in my answer, so simply try doing this without reading all the rationale before. Also, since your global config is not case-insensitive, the @zeppelin's answer should also work, although it doesn't use $fpath and removes also small->CAPS matching of the hosts.

I did test this with your settings from the update and it works as expected.

Update: remember that zsh keeps it's functions loaded, so after modifying the _hosts you need to reload it either by logging in fresh, or:

unfunction _hosts
autoload -Uz _hosts

Also remember that zsh can have the scripts 'compiled' in zwc form (zcompile [file]) and if such file exists and is newer than the source it would be used instead.

Ad. update 2:

Handling the ~/.ssh/config defined hosts is actually pretty much the same as for _hosts - depending on your zsh version in either Unix/(Command/)_ssh or Unix/(Type/)_ssh_hosts change the

compadd -M 'm:{a-zA-Z}={A-Za-z} r:|.=* r:|=*' "$@" $config_hosts

line to

compadd -M 'm:{a-z}={A-Z} r:|.=* r:|=*' "$@" $config_hosts

5
  • thank you, but this does not work as I wanted: 1) touch SSS.TXT ; scp -rp S<TAB> still offers completion for server.domain.org from /etc/hosts. It should only offer completion of local file S*, if it exists. 2) ssh S<TAB> also offers completion server.domain.org. It should offer nothing, because I have server.domain.org in my /etc/hosts, and not SERVER.DOMAIN.ORG Aug 6, 2020 at 13:55
  • If so then either you don't use the modified _hosts file (I've updated my answer), or you're stuck in 1st point of that answer. To verify the former problem - simply put some bogus code or echo in the _hosts and see if this is the code that is actually being used (you might have some other directory included in $fpath which holds previous _hosts). As for the 1st problem: zstyle | grep -A1 matcher-list Aug 7, 2020 at 10:33
  • I have found out why your solution did not work fore me. Please see my UPDATE 2 for details. Jan 20, 2021 at 9:19
  • I'm glad you've narrowed this down to local config - I've just posted updated response which handles also these hosts. Jan 21, 2021 at 5:45
  • everything works perfectly now. Thank you for your help and patience. Jan 21, 2021 at 6:19
2
+100

I believe you are editing a wrong line.

AFAIK config_hosts in Unix/_ssh refers to the host entries in your ~./ssh/config, not /etc/hosts.

The completion rules for /etc/hosts are defined a bit earlier, in the following block:

# If users-hosts matches, we shouldn't complete anything else.
if [[ "$IPREFIX" == *@ ]]; then
  _combination -s '[:@]' my-accounts users-hosts "users=${IPREFIX/@}" hosts "$@" && return
else
  _combination -s '[:@]' my-accounts users-hosts \
    ${opt_args[-l]:+"users=${opt_args[-l]:q}"} hosts "$@" && return
fi

but this in turn just reuses the hosts style defined in Unix/_hosts

So if you edit the compadd definition at the end of the Unix/_hosts file like this:

#_wanted hosts expl host \
#    compadd -M 'm:{a-zA-Z}={A-Za-z} r:|.=* r:|=*' -a "$@" - _hosts
_wanted hosts expl host \
     compadd -a "$@" - _hosts

you should get the behavior you want.

P.S.

Please note that editing a system-wide completion files is not generally a very good practice, so you may want to just redefine hosts in your local ZSH config instead, e.g. by adding a function like that to your ~./zsh:

_hosts() { compadd $(getent hosts | tr -s ' ' '\t' | cut -f2) }
2
+50

There are two aspects of this question.

  1. the global zsh completion with case-insensitiveness. It's very popular, but IMHO pointless, to do it two-way. Since I don't type in caps usually (or by mistake), let's treat all the CAPS as intentional:

zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

mkdir DOC drone
ls D[tab] => DOC
ls d[tab] => drone DOC
ls Dr[tab] => [empty]

The problem with matcher-list is that this is called very early and cannot be replaced on per-command/argument basis.

And since matcher-list repeated values are being concatenated (quote from the documentation: "This option may be given more than once. In this case all match-specs given are concatenated with spaces between them to form the specification string to use"), you cannot override this later, e.g.

compdef '_hosts -M m:' foo

won't revoke the previous definition from _hosts file. So if you have {a-zA-Z}={A-Za-z} in your global configuration, you won't be able to clear this, even with matcher, e.g.:

zstyle ':completion:*:scp:*:*' matcher "m:{A-Z}={A-Z}"

won't help. Sorry, you need to start resolving this issue at global level. The trade-off solution could be to have two-pass global:

zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' 'm:{A-Z}={a-z}'

ls d[tab] => DOC drone
ls D[tab] => DOC
ls Dr[tab] => drone
  1. Second part of the problem is function-defined matcher-list. As stated above, this is not clearable, so you need to cope with this like already covered in @zeppelin answer, by changing zsh-provided file:

_hosts:

[....]
_wanted hosts expl host \
    compadd -M 'm:{a-z}={A-Z} r:|.=* r:|=*' -a "$@" - _hosts

Now, and with system-wide matcher-list 'm:{a-z}={A-Z}':

scp d[tab] => /directory/ DOC drone /remote host name/ docker /user/ daemon
scp D[tab] => DOC

The proposed solution with altering original completion files is not the right way to do, just like @zeppelin noted ...but creating own host-fetching function is also flawned. Proper solution is unfortunately up to zsh developers, who might either add a matcher-clearing option, define some new style or simply fix the completion function to not ignore case on it's own, just follow the global settings.

Yet even with this issue there is nice and clean solution - instead of modifying (probably some distro package provided) /usr/share/zsh/... one can put the fixed copy in /etc/zsh/functions and simply tell zsh to use this location: fpath=(/etc/zsh/functions $fpath). This way you avoid overwriting the file after system update.

Summarizing

  1. change your global matcher-list:

zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' 'm:{A-Z}={a-z}'

  1. put modified _hosts file in some /etc/zsh or $HOME location:

fpath=(/etc/zsh/functions $fpath)

/etc/zsh/functions/_hosts:

[....]
_wanted hosts expl host \
    compadd -M 'm:{a-z}={A-Z} r:|.=* r:|=*' -a "$@" - _hosts
2

You do not need to change any completion functions to get what you want. Just add this to your ~/.zshrc file:

# $PREFIX is the part of the current word that's to the left of the cursor.
# $SUFFIX is the part of the current word that's to the right of the cursor.
# Let's ignore all host completions that don't explicitly match what we've typed, 
# but allow for additional characters at the cursor position and at the end of what 
# we've typed. This makes the matching case sensitive.
# `(b)` escapes characters that are significant to globbing.
zstyle -e ':completion:*:hosts' ignored-patterns 'reply=(
  "^(${(b)PREFIX}*${(b)SUFFIX}*)"
)'

# But if that would lead to no results, then offer the ignored completions anyway.
zstyle ':completion:*' completer _expand _complete _ignored

I've tested this with the config and test cases you posted and it works.

Documentation:

6
  • @400theCat Have you tried this yet? Aug 5, 2020 at 10:22
  • can you please explain what these two lines do? Looks quite convoluted, and looks like this is not specific to scp/ssh. Aug 5, 2020 at 18:00
  • @400theCat I added more explanation and made the code specific to scp/ssh. Aug 6, 2020 at 10:57
  • This is not a solution to the given problem - this changes entire completion for the command, including local files: touch abc; scp *bc[tab] won't get expanded to abc. And obviously won't work for other commands using _hosts completion. Aug 6, 2020 at 13:30
  • 1
    Definitely:) now it targets the problem itself and is in fact a clever solution - instead of altering the set that is generated, filter out what's not expected, and only after that fallback into "magic" completions (like scp *host_pattern). Too bad OP is so inactive here, this answer deserves second half of the bounty... Aug 9, 2020 at 11:29

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .