1# ssh(1) completion                                        -*- shell-script -*-
2
3_ssh_queries()
4{
5    COMPREPLY+=($(compgen -W \
6        "cipher cipher-auth help mac kex key key-cert key-plain key-sig
7         protocol-version compression sig
8         ciphers macs kexalgorithms pubkeyacceptedkeytypes
9         hostkeyalgorithms hostbasedkeytypes hostbasedacceptedkeytypes" \
10        -- "${cur,,}"))
11}
12
13_ssh_query()
14{
15    ${1:-ssh} -Q $2 2>/dev/null
16}
17
18_ssh_ciphers()
19{
20    local ciphers='$(_ssh_query "$1" cipher)'
21    [[ $ciphers ]] || ciphers="3des-cbc aes128-cbc aes192-cbc aes256-cbc
22        aes128-ctr aes192-ctr aes256-ctr arcfour128 arcfour256 arcfour
23        blowfish-cbc cast128-cbc"
24    COMPREPLY+=($(compgen -W "$ciphers" -- "$cur"))
25}
26
27_ssh_macs()
28{
29    local macs='$(_ssh_query "$1" mac)'
30    [[ $macs ]] || macs="hmac-md5 hmac-sha1 umac-64@openssh.com hmac-ripemd160
31        hmac-sha1-96 hmac-md5-96"
32    COMPREPLY+=($(compgen -W "$macs" -- "$cur"))
33}
34
35_ssh_options()
36{
37    local opts=(
38        AddKeysToAgent AddressFamily BatchMode BindAddress CanonicalDomains
39        CanonicalizeFallbackLocal CanonicalizeHostname CanonicalizeMaxDots
40        CanonicalizePermittedCNAMEs CASignatureAlgorithms CertificateFile
41        ChallengeResponseAuthentication CheckHostIP Ciphers ClearAllForwardings
42        Compression ConnectionAttempts ConnectTimeout ControlMaster ControlPath
43        ControlPersist DynamicForward EnableSSHKeysign EscapeChar
44        ExitOnForwardFailure FingerprintHash ForwardAgent ForwardX11
45        ForwardX11Timeout ForwardX11Trusted GatewayPorts GlobalKnownHostsFile
46        GSSAPIAuthentication GSSAPIClientIdentity GSSAPIDelegateCredentials
47        GSSAPIKeyExchange GSSAPIRenewalForcesRekey GSSAPIServerIdentity
48        GSSAPITrustDns HashKnownHosts Host HostbasedAuthentication
49        HostbasedKeyTypes HostKeyAlgorithms HostKeyAlias HostName
50        IdentitiesOnly IdentityAgent IdentityFile IgnoreUnknown Include IPQoS
51        KbdInteractiveAuthentication KbdInteractiveDevices KexAlgorithms
52        LocalCommand LocalForward LogLevel MACs
53        NoHostAuthenticationForLocalhost NumberOfPasswordPrompts
54        PasswordAuthentication PermitLocalCommand PKCS11Provider Port
55        PreferredAuthentications ProxyCommand ProxyJump ProxyUseFdpass
56        PubkeyAcceptedKeyTypes PubkeyAuthentication RekeyLimit RemoteCommand
57        RemoteForward RequestTTY RevokedHostKeys SendEnv ServerAliveCountMax
58        ServerAliveInterval SmartcardDevice StreamLocalBindMask
59        StreamLocalBindUnlink StrictHostKeyChecking SyslogFacility TCPKeepAlive
60        Tunnel TunnelDevice UpdateHostKeys UsePrivilegedPort User
61        UserKnownHostsFile VerifyHostKeyDNS VisualHostKey XAuthLocation)
62    local protocols=$(_ssh_query "$1" protocol-version)
63    if [[ -z $protocols || $protocols == *1* ]]; then
64        opts+=(Cipher CompressionLevel Protocol RhostsRSAAuthentication
65            RSAAuthentication)
66    fi
67
68    compopt -o nospace
69    local IFS=$' \t\n' reset=$(shopt -p nocasematch)
70    shopt -s nocasematch
71    local option
72    COMPREPLY=($(for option in "${opts[@]}"; do
73        [[ $option == "$cur"* ]] && printf '%s=\n' "$option"
74    done))
75    $reset
76}
77
78# Complete a ssh suboption (like ForwardAgent=y<tab>)
79# Two parameters: the string to complete including the equal sign, and
80# the ssh executable to invoke (optional).
81# Not all suboptions are completed.
82# Doesn't handle comma-separated lists.
83_ssh_suboption()
84{
85    # Split into subopt and subval
86    local prev=${1%%=*} cur=${1#*=}
87
88    case ${prev,,} in
89        batchmode | canonicaldomains | canonicalizefallbacklocal | \
90            challengeresponseauthentication | checkhostip | \
91            clearallforwardings | controlpersist | compression | enablesshkeysign | \
92            exitonforwardfailure | forwardagent | forwardx11 | forwardx11trusted | \
93            gatewayports | gssapiauthentication | gssapikeyexchange | \
94            gssapidelegatecredentials | gssapirenewalforcesrekey | gssapitrustdns | \
95            hashknownhosts | hostbasedauthentication | identitiesonly | \
96            kbdinteractiveauthentication | kbdinteractivedevices | \
97            nohostauthenticationforlocalhost | passwordauthentication | permitlocalcommand | \
98            proxyusefdpass | pubkeyauthentication | rhostsrsaauthentication | \
99            rsaauthentication | streamlocalbindunlink | \
100            tcpkeepalive | useprivilegedport | visualhostkey)
101            COMPREPLY=($(compgen -W 'yes no' -- "$cur"))
102            ;;
103        addkeystoagent)
104            COMPREPLY=($(compgen -W 'yes ask confirm no' -- "$cur"))
105            ;;
106        addressfamily)
107            COMPREPLY=($(compgen -W 'any inet inet6' -- "$cur"))
108            ;;
109        bindaddress)
110            _ip_addresses
111            ;;
112        canonicalizehostname)
113            COMPREPLY=($(compgen -W 'yes no always' -- "$cur"))
114            ;;
115        identityfile)
116            _ssh_identityfile
117            ;;
118        *file | identityagent | include | controlpath | revokedhostkeys | xauthlocation)
119            _filedir
120            ;;
121        casignaturealgorithms)
122            COMPREPLY=($(compgen -W '$(_ssh_query "$2" sig)' -- "$cur"))
123            ;;
124        cipher)
125            COMPREPLY=($(compgen -W 'blowfish des 3des' -- "$cur"))
126            ;;
127        ciphers)
128            _ssh_ciphers "$2"
129            ;;
130        controlmaster)
131            COMPREPLY=($(compgen -W 'yes ask auto autoask no' -- "$cur"))
132            ;;
133        compressionlevel)
134            COMPREPLY=($(compgen -W '{1..9}' -- "$cur"))
135            ;;
136        fingerprinthash)
137            COMPREPLY=($(compgen -W 'md5 sha256' -- "$cur"))
138            ;;
139        ipqos)
140            COMPREPLY=($(compgen -W 'af1{1..4} af2{2..3} af3{1..3} af4{1..3}
141                cs{0..7} ef lowdelay throughput reliability' -- "$cur"))
142            ;;
143        hostbasedkeytypes | hostkeyalgorithms)
144            COMPREPLY=($(compgen -W '$(_ssh_query "$2" key)' -- "$cur"))
145            ;;
146        kexalgorithms)
147            COMPREPLY=($(compgen -W '$(_ssh_query "$2" kex)' -- "$cur"))
148            ;;
149        loglevel)
150            COMPREPLY=($(compgen -W 'QUIET FATAL ERROR INFO VERBOSE DEBUG{,1,2,3}' -- "$cur"))
151            ;;
152        macs)
153            _ssh_macs "$2"
154            ;;
155        pkcs11provider)
156            _filedir so
157            ;;
158        preferredauthentications)
159            COMPREPLY=($(compgen -W 'gssapi-with-mic host-based publickey
160                keyboard-interactive password' -- "$cur"))
161            ;;
162        protocol)
163            local protocols=($(_ssh_query "$2" protocol-version))
164            [[ $protocols ]] || protocols=(1 2)
165            if ((${#protocols[@]} > 1)); then
166                COMPREPLY=($(compgen -W '${protocols[@]}' -- "$cur"))
167            fi
168            ;;
169        proxyjump)
170            _known_hosts_real -a ${configfile:+-F "$configfile"} -- "$cur"
171            ;;
172        proxycommand | remotecommand | localcommand)
173            COMPREPLY=($(compgen -c -- "$cur"))
174            ;;
175        pubkeyacceptedkeytypes)
176            COMPREPLY=($(compgen -W '$(_ssh_query "$2" key)' -- "$cur"))
177            ;;
178        requesttty)
179            COMPREPLY=($(compgen -W 'no yes force auto' -- "$cur"))
180            ;;
181        stricthostkeychecking)
182            COMPREPLY=($(compgen -W 'accept-new ask no off' -- "$cur"))
183            ;;
184        syslogfacility)
185            COMPREPLY=($(compgen -W 'DAEMON USER AUTH LOCAL{0..7}' -- "$cur"))
186            ;;
187        tunnel)
188            COMPREPLY=($(compgen -W 'yes no point-to-point ethernet' \
189                -- "$cur"))
190            ;;
191        updatehostkeys | verifyhostkeydns)
192            COMPREPLY=($(compgen -W 'yes no ask' -- "$cur"))
193            ;;
194    esac
195    return 0
196}
197
198# Try to complete -o SubOptions=
199#
200# Returns 0 if the completion was handled or non-zero otherwise.
201_ssh_suboption_check()
202{
203    # Get prev and cur words without splitting on =
204    local cureq=$(_get_cword :=) preveq=$(_get_pword :=)
205    if [[ $cureq == *=* && $preveq == -*o ]]; then
206        _ssh_suboption $cureq "$1"
207        return $?
208    fi
209    return 1
210}
211
212# Search COMP_WORDS for '-F configfile' or '-Fconfigfile' argument
213_ssh_configfile()
214{
215    set -- "${words[@]}"
216    while (($# > 0)); do
217        if [[ $1 == -F* ]]; then
218            if ((${#1} > 2)); then
219                configfile="$(dequote "${1:2}")"
220            else
221                shift
222                [[ ${1-} ]] && configfile="$(dequote "$1")"
223            fi
224            break
225        fi
226        shift
227    done
228}
229
230# With $1 set, look for public key files, else private
231# shellcheck disable=SC2120
232_ssh_identityfile()
233{
234    [[ -z $cur && -d ~/.ssh ]] && cur=~/.ssh/id
235    _filedir
236    if ((${#COMPREPLY[@]} > 0)); then
237        COMPREPLY=($(compgen -W '${COMPREPLY[@]}' \
238            -X "${1:+!}*.pub" -- "$cur"))
239    fi
240}
241
242_ssh()
243{
244    local cur prev words cword
245    _init_completion -n : || return
246
247    local configfile
248    _ssh_configfile
249
250    _ssh_suboption_check "$1" && return
251
252    local ipvx
253
254    case $prev in
255        -*b)
256            _ip_addresses
257            return
258            ;;
259        -*c)
260            _ssh_ciphers "$1"
261            return
262            ;;
263        -*[DeLpRW])
264            return
265            ;;
266        -*[EFS])
267            _filedir
268            return
269            ;;
270        -*i)
271            _ssh_identityfile
272            return
273            ;;
274        -*I)
275            _filedir so
276            return
277            ;;
278        -*J)
279            _known_hosts_real -a ${configfile:+-F "$configfile"} -- "$cur"
280            return
281            ;;
282        -*l)
283            COMPREPLY=($(compgen -u -- "$cur"))
284            return
285            ;;
286        -*m)
287            _ssh_macs "$1"
288            return
289            ;;
290        -*O)
291            COMPREPLY=($(compgen -W 'check forward cancel exit stop' -- "$cur"))
292            return
293            ;;
294        -*o)
295            _ssh_options "$1"
296            return
297            ;;
298        -*Q)
299            _ssh_queries "$1"
300            return
301            ;;
302        -*w)
303            _available_interfaces
304            return
305            ;;
306        -*4*)
307            ipvx=-4
308            ;;
309        -*6*)
310            ipvx=-6
311            ;;
312    esac
313
314    if [[ $cur == -F* ]]; then
315        cur=${cur#-F}
316        _filedir
317        # Prefix completions with '-F'
318        COMPREPLY=("${COMPREPLY[@]/#/-F}")
319        cur=-F$cur # Restore cur
320    elif [[ $cur == -* ]]; then
321        COMPREPLY=($(compgen -W '$(_parse_usage "$1")' -- "$cur"))
322    else
323        _known_hosts_real ${ipvx-} -a ${configfile:+-F "$configfile"} -- "$cur"
324
325        local args
326        _count_args
327        if ((args > 1)); then
328            compopt -o filenames
329            COMPREPLY+=($(compgen -c -- "$cur"))
330        fi
331    fi
332} &&
333    shopt -u hostcomplete && complete -F _ssh ssh slogin autossh sidedoor
334
335# sftp(1) completion
336#
337_sftp()
338{
339    local cur prev words cword
340    _init_completion || return
341
342    local configfile
343    _ssh_configfile
344
345    _ssh_suboption_check && return
346
347    local ipvx
348
349    case $prev in
350        -*[BDlPRs])
351            return
352            ;;
353        -*[bF])
354            _filedir
355            return
356            ;;
357        -*i)
358            _ssh_identityfile
359            return
360            ;;
361        -*c)
362            _ssh_ciphers
363            return
364            ;;
365        -*J)
366            _known_hosts_real -a ${configfile:+-F "$configfile"} -- "$cur"
367            return
368            ;;
369        -*o)
370            _ssh_options
371            return
372            ;;
373        -*S)
374            compopt -o filenames
375            COMPREPLY=($(compgen -c -- "$cur"))
376            return
377            ;;
378        -*4*)
379            ipvx=-4
380            ;;
381        -*6*)
382            ipvx=-6
383            ;;
384    esac
385
386    if [[ $cur == -F* ]]; then
387        cur=${cur#-F}
388        _filedir
389        # Prefix completions with '-F'
390        COMPREPLY=("${COMPREPLY[@]/#/-F}")
391        cur=-F$cur # Restore cur
392    elif [[ $cur == -* ]]; then
393        COMPREPLY=($(compgen -W '$(_parse_usage "$1")' -- "$cur"))
394    else
395        _known_hosts_real ${ipvx-} -a ${configfile:+-F "$configfile"} -- "$cur"
396    fi
397} &&
398    shopt -u hostcomplete && complete -F _sftp sftp
399
400# things we want to backslash escape in scp paths
401# shellcheck disable=SC2089
402_scp_path_esc='[][(){}<>"'"'"',:;^&!$=?`\\|[:space:]]'
403
404# Complete remote files with ssh.  If the first arg is -d, complete on dirs
405# only.  Returns paths escaped with three backslashes.
406# shellcheck disable=SC2120
407_scp_remote_files()
408{
409    local IFS=$'\n'
410
411    # remove backslash escape from the first colon
412    cur=${cur/\\:/:}
413
414    local userhost=${cur%%?(\\):*}
415    local path=${cur#*:}
416
417    # unescape (3 backslashes to 1 for chars we escaped)
418    # shellcheck disable=SC2090
419    path=$(command sed -e 's/\\\\\\\('$_scp_path_esc'\)/\\\1/g' <<<"$path")
420
421    # default to home dir of specified user on remote host
422    if [[ -z $path ]]; then
423        path=$(ssh -o 'Batchmode yes' $userhost pwd 2>/dev/null)
424    fi
425
426    local files
427    if [[ $1 == -d ]]; then
428        # escape problematic characters; remove non-dirs
429        # shellcheck disable=SC2090
430        files=$(ssh -o 'Batchmode yes' $userhost \
431            command ls -aF1dL "$path*" 2>/dev/null |
432            command sed -e 's/'$_scp_path_esc'/\\\\\\&/g' -e '/[^\/]$/d')
433    else
434        # escape problematic characters; remove executables, aliases, pipes
435        # and sockets; add space at end of file names
436        # shellcheck disable=SC2090
437        files=$(ssh -o 'Batchmode yes' $userhost \
438            command ls -aF1dL "$path*" 2>/dev/null |
439            command sed -e 's/'$_scp_path_esc'/\\\\\\&/g' -e 's/[*@|=]$//g' \
440                -e 's/[^\/]$/& /g')
441    fi
442    COMPREPLY+=($files)
443}
444
445# This approach is used instead of _filedir to get a space appended
446# after local file/dir completions, and -o nospace retained for others.
447# If first arg is -d, complete on directory names only.  The next arg is
448# an optional prefix to add to returned completions.
449_scp_local_files()
450{
451    local IFS=$'\n'
452
453    local dirsonly=false
454    if [[ ${1-} == -d ]]; then
455        dirsonly=true
456        shift
457    fi
458
459    if $dirsonly; then
460        COMPREPLY+=($(command ls -aF1dL $cur* 2>/dev/null |
461            command sed -e "s/$_scp_path_esc/\\\\&/g" -e '/[^\/]$/d' -e "s/^/${1-}/"))
462    else
463        COMPREPLY+=($(command ls -aF1dL $cur* 2>/dev/null |
464            command sed -e "s/$_scp_path_esc/\\\\&/g" -e 's/[*@|=]$//g' \
465                -e 's/[^\/]$/& /g' -e "s/^/${1-}/"))
466    fi
467}
468
469# scp(1) completion
470#
471_scp()
472{
473    local cur prev words cword
474    _init_completion -n : || return
475
476    local configfile
477    _ssh_configfile
478
479    _ssh_suboption_check && {
480        COMPREPLY=("${COMPREPLY[@]/%/ }")
481        return
482    }
483
484    local ipvx
485
486    case $prev in
487        -*c)
488            _ssh_ciphers
489            COMPREPLY=("${COMPREPLY[@]/%/ }")
490            return
491            ;;
492        -*F)
493            _filedir
494            compopt +o nospace
495            return
496            ;;
497        -*i)
498            _ssh_identityfile
499            compopt +o nospace
500            return
501            ;;
502        -*J)
503            _known_hosts_real -a ${configfile:+-F "$configfile"} -- "$cur"
504            return
505            ;;
506        -*[lP])
507            return
508            ;;
509        -*o)
510            _ssh_options
511            return
512            ;;
513        -*S)
514            compopt +o nospace -o filenames
515            COMPREPLY=($(compgen -c -- "$cur"))
516            return
517            ;;
518        -*4*)
519            ipvx=-4
520            ;;
521        -*6*)
522            ipvx=-6
523            ;;
524    esac
525
526    _expand || return
527
528    case $cur in
529        !(*:*)/* | [.~]*) ;; # looks like a path
530        *:*)
531            _scp_remote_files
532            return
533            ;;
534    esac
535
536    local prefix
537
538    if [[ $cur == -F* ]]; then
539        cur=${cur#-F}
540        prefix=-F
541    else
542        case $cur in
543            -*)
544                COMPREPLY=($(compgen -W '$(_parse_usage "${words[0]}")' \
545                    -- "$cur"))
546                COMPREPLY=("${COMPREPLY[@]/%/ }")
547                return
548                ;;
549            */* | [.~]*)
550                # not a known host, pass through
551                ;;
552            *)
553                _known_hosts_real ${ipvx-} -c -a \
554                    ${configfile:+-F "$configfile"} -- "$cur"
555                ;;
556        esac
557    fi
558
559    _scp_local_files "${prefix-}"
560} &&
561    complete -F _scp -o nospace scp
562
563# ex: filetype=sh
564