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 '16 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 '16 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 '16 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 '16 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 '20 at 3:56
5
+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 '20 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 '20 at 10:33
  • I have found out why your solution did not work fore me. Please see my UPDATE 2 for details. Jan 20 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 at 5:45
  • everything works perfectly now. Thank you for your help and patience. Jan 21 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 '20 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 '20 at 18:00
  • @400theCat I added more explanation and made the code specific to scp/ssh. Aug 6 '20 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 '20 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 '20 at 11:29

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.