Subject: PATCH: _su (was Re: Are completions in some way heavy?)
From: Oliver Kiddle <[email-masked]>
To: Zsh Workers <[email-masked]>
Date: Thu, 09 Jun 2016 15:48:19 +0200
List-Id: Zsh Workers List <zsh-workers.zsh.org>

On 6 Jun, Sebastian Gniazdowski wrote:
> But it doesn't complete user names when invoked as "su - <TAB>".

su completion is particularly tricky to get working properly in part
because of that single dash option. The patch is an attempt to improve
on it.

_su was also broken in other regards. In particular, Linux su got moved
to util-linux and removed from GNU coreutils quite a few years ago (I
dug into the history in case there was still a GNU su that needed
supporting). That change meant the Linux specifics were skipped.

I'm not too keen on completion functions producing messages like the
one for the user apparently having no shell. The function failing to
determine what it is is more likely so falling back to sh might be more
useful?

Before dispatching to _sh etc $words and $CURRENT need to be cut down
to remove su options and this wasn't being done. The usual use of
_arguments' two/three colon '*:: form isn't workable on Linux: su
options can be specified after the username and you need -- to pass
arbitrary shell options. I tried using _arguments -n and NORMARG and
concluded that that feature is badly broken. It only works in very
simple cases where all options precede all normal arguments but for that
it offers nothing you can't do by checking $line. It could be a useful
feature but I think I got something that works by comparing $#words to
$#line.

I couldn't find an online manpage for su on Mac OS X. Perhaps it doesn't
have an su. I have verified the function on Solaris, it just doesn't
need a section in the case statement.

Oliver

diff --git a/Completion/Unix/Command/_su b/Completion/Unix/Command/_su
index 057a413..73b27ee 100644
--- a/Completion/Unix/Command/_su
+++ b/Completion/Unix/Command/_su
@@ -2,79 +2,86 @@
 
 local -A opt_args
 local -a args context state line expl
-local shell=${words[(i)(-s|--shell=*)]} first='1:user name:_users'
-local usr=root
+local first='(-)${norm}:user name:_users'
+integer norm=1 strip
+local shell usr
 
-if _pick_variant gnu="Free Software Foundation" unix --version; then
-  args=(
-    '(--command)-c[pass command to shell]:command string:->command'
-    '(-c)--command=-[pass command to shell]:command string:->command'
-    '-f[pass -f to shell (csh)]'
-    '(--login)-l[use a login shell]'
-    '(-l)--login[use a login shell]'
-    '(-p --preserve-environment)-m[do not reset environment]'
-    '(-m --preserve-environment)-p[do not reset environment]'
-    '(-m -p)--preserve-environment[do not reset environment]'
-    '(--shell)-s[run the specified shell]:shell:->shell'
-    '(-s)--shell=-[run the specified shell]:shell:->shell'
-  )
-else
-  args=(
-    '-l[use a login shell]'
-    '-s[run the specified shell]:shell:->shell'
-  )
-  case $OSTYPE in
-  freebsd*)
-    args=(
+(( $words[(i)-(l|-login)] < CURRENT )) || args=( '-[use a login shell]' )
+case $OSTYPE in
+  linux*)
+    args=( -S $args
+      '(-c --command --session-command *)'{-c,--command=}'[pass command to shell]:command string:_cmdstring'
+      "(-c --command *)--session-command=[pass command to shell and don't create a new session]:command string:_cmdstring"
+      '(--fast -f)'{-f,--fast}'[pass -f to shell]'
+      '(-l --login -m -p --preserve-environment)'{-l,--login}'[use a login shell]'
+      '(-l --login -m -p --preserve-environment)'{-m,-p,--preserve-environment}"[don't reset environment]"
+      '(-s --shell)'{-s,--shell=}'[run the specified shell]:shell:->shells'
+      '(-)--help[display help information]'
+      '(-)--version[display version information]'
+    )
+    (( EUID )) || args+=(
+      '(-g --group)'{-g,--group=}'[specify primary group]:group:_groups'
+      \*{-G,--supp-group=}'[specify supplemental group]:group:_groups'
+    )
+    first="(--help --version)${first#???}"
+  ;;
+  *bsd*|dragonfly*)
+    args+=(
       '-c[use settings from specified login class]:class'
       '-f[if the invoked shell is csh, prevent it from reading .cshrc]'
-      '-l[use a login shell]'
-      '-m[do not reset environment]'
-      '-s[set the MAC label]'
+      '(-m)-l[use a login shell]'
+      "(-l)-m[don't reset environment]"
+    )
+  ;|
+  freebsd*) args+=( '-s[set the MAC label]' ) ;;
+  openbsd*)
+    args+=(
+      '(-K)-a[specify authentication type]:authentication type'
+      '(-a)-K[shorthand for -a passwd]'
+      '-s[run the specified shell]:shell:->shells'
+      '-L[loop until login succeeds]'
     )
   ;;
-  *) args+=( '-c[pass command to shell]:command string:->command' ) ;;
-  esac
-fi
+  netbsd*)
+    args+=(
+      '-d[use a login shell but retain current directory]'
+      "-K[don't use Kerberos]"
+    )
+  ;;
+esac
 
-if [[ $#words -ge 2 && $words[2] != -* && CURRENT -ne 2 ]]; then
-    usr=$words[2]
-    first=
+if (( $words[(i)-] < CURRENT )); then
+  args=( ${args:#*-(-login|l|)\[*} '1:-' )
+  norm=2
 fi
 
-[[ $words[shell] == -s ]] && ((shell++))
+_arguments $args ${(e)first} "*:shell arguments:= ->rest" && return
 
-if [[ CURRENT -ne shell && -n ${words[shell]} ]]; then
-    shell=${words[shell]#*=}
+usr=${line[norm]/--/root}
+if (( $#opt_args[(i)-(s|-shell)] )); then
+  shell=${(v)opt_args[(i)-(s|-shell)]}
+elif (( ${+commands[getent]} )); then
+  shell="${$(_call_program shells getent passwd $usr)##*:}"
 else
-    shell="${${(M@)${(@f)$(</etc/passwd)}:#$usr*}##*:}"
+  shell="${${(M@)${(@f)$(</etc/passwd)}:#$usr*}##*:}"
 fi
 
-[[ -z $first ]] && compset -n 2
-
-_arguments : $args[@] $first "*:${shell:t} arguments:->rest" && return
-
 case $state in
-    (command)
-        compset -q
-        _normal
-        return
-        ;;
-    (shell)
-        _wanted -C $context shells expl shell compadd ${(f)^"$(</etc/shells)"}(N)
-        return
-        ;;
-    (rest)
-        if [[ -z $shell || $shell = */(nologin|false) ]]; then
-            _arguments "-s[run the specified shell, $usr has no shell]" ||
-                _message "-s option required, $usr has no shell"
-            compstate[insert]=
-        else
-            # Something wrong here: doubles the file listing sometimes
-            _dispatch ${service}:${context} $shell $shell:t -default-
-            return
-        fi
-        ;;
+  shells)
+    _wanted -C $context shells expl shell compadd ${(f)^"$(</etc/shells)"}(N)
+    return
+  ;;
+  rest)
+    if [[ -z $shell || $shell = */(nologin|false) ]]; then
+      _message "-s option required, $usr has no shell"
+    else
+      (( strip = $#words - $#line + norm ))
+      (( CURRENT -= strip - 1 ))
+      words[2,strip]=()
+      _dispatch ${service}:${context} $shell $shell:t -default-
+      return
+    fi
+  ;;
 esac
 
 return 1