Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

[PATCH] Completion: Improve _man



Here's a *much* more complicated change. It enhances _man as follows:

* Complete all options to most major man variants
* More accurately match sections
* Better handle certain edge cases (see below)
* Show descriptions for sections (suggested by Mikael i think)

Notes:

* The function would previously use the provided section name as the leading
  part of a directory pattern, so that on Solaris for example if you gave it 3p
  it would also complete pages from 3pool, 3proc, &c. I couldn't think of a
  reason this functionality would be desirable (man won't show you a page from a
  section it's not in anyway), so i made it match exactly

* The function previously behaved kind of strangely when given multiple operands
  (where the first one is potentially an unknown section name). For example, if
  you do `man zz <TAB>` on macOS it just keeps completing the word 'fuzzy'
  because it wants to use zz as part of a glob to match page names. I changed
  that so that if the section name (given either as the first operand or as an
  argument to -s/-S) doesn't seem valid, it's treated as a page name, and
  subsequent completion possibilities are thus returned from all sections

  This can still be a problem — `man 2to3 <TAB>` doesn't work well, for example,
  because the function thinks 2to3 looks like a (Solaris-style) section name. I
  know this can be fixed, but see my last two notes :/

* It looks like someone put some special effort into making the function at
  least partially usable on AIX, so i tried to account for that platform as
  well... but i don't actually have access to it myself, so the extent of my
  testing there was just OSTYPE=aix

* I chose sections to describe pretty arbitrarily. Solaris variants have 9034234
  different sections and i didn't want to enumerate every single one, but some
  of them seem like they might come up a lot in regular use. It may be nice in
  the future to give separate tags to the described and undescribed ones, so
  that navigation is a little nicer for people with `group-name ''`

* There were a bunch of options whose purpose wasn't very clear to me, so i
  couldn't complete them as well as i wanted to. I put @todos where this was the
  case. If anyone knows how they actually work, feel free to change them
  accordingly

* There are some bits that were (or are now) redundant or otherwise not great —
  see the todo note someone left about the sects array for example — but i just
  left them alone because i was afraid of breaking something

* Could someone please review the way i return from _arguments? I don't think
  it's correct (because it doesn't account for prefix-needed?), but i've been
  looking at this for too long

I've tested this on several different platforms, but it's still a pretty major
alteration to a function that i imagine sees a lot of traffic, so i'm a little
nervous about it. If someone else could try it out that might be a good idea

dana


diff --git a/Completion/Unix/Command/_man b/Completion/Unix/Command/_man
index 67810e1dc..14e9e75b5 100644
--- a/Completion/Unix/Command/_man
+++ b/Completion/Unix/Command/_man
@@ -1,16 +1,169 @@
 #compdef man apropos whatis
 
+# Notes:
+# - Solaris is seemingly the only OS that doesn't allow the `man n page` syntax;
+#   you must use `man -s n page`
+# - We assume that Linux distributions are using either man-db or mandoc
+# - @todo Option exclusivity isn't super accurate
+# - @todo Solaris man accepts a single hyphen as the first option to disable
+#   paging (like AIX's -c); we don't support that
+# - @todo Linux apropos/whatis take options; we don't complete them yet
+
 _man() {
-  local dirs expl mrd awk
+  local dirs expl mrd awk variant noinsert
+  local -a context line state state_descr args modes
+  local -aU sects
+  local -A opt_args val_args sect_descs
 
-  if (( $words[(I)-M] == (( $CURRENT - 1 )) )); then
-    _directories && return 0
-  fi
+  if [[ $service == man ]]; then
+    # We'll treat all mandoc-based systems (Alpine, various Illumos distros,
+    # etc.) as OpenBSD
+    _pick_variant -r variant openbsd='-S subsection' $OSTYPE ---
+
+    modes=(
+      -f -K -k -l -R -w -W
+      --apropos
+      --global-apropos
+      --local-file
+      --location
+      --location-cat
+      --recode
+      --whatis
+      --where
+      --where-cat
+    )
+    [[ $variant == darwin* ]] && modes+=( -t )
+
+    args=(
+      "(${(j< >)modes})"{-f,--whatis}'[display short description (like whatis)]'
+      "(${(j< >)modes})"{-k,--apropos}'[search for keyword (like apropos)]'
+      '(-M --manpath)'{-M+,--manpath=}'[specify manual search path]:manual search path:_sequence -s\: _directories'
+    )
+    if [[ $variant == (darwin|dragonfly|freebsd|linux)* ]]; then
+      args+=(
+        '(-a -S -s --all --sections)'{-a,--all}'[display all matching pages]'
+        '(-P --pager)'{-P+,--pager=}'[specify output pager]:pager:_path_commands'
+        # @todo Could enumerate these
+        '(-p --preprocessor)'{-p+,--preprocessor=}'[specify roff preprocessor sequence]:preprocessor sequence'
+      )
+    else
+      args+=( '(-s)-a[display all matching pages]' )
+    fi
+    [[ $variant == (aix|solaris)* ]] || args+=(
+      '(-C --config-file)'{-C+,--config-file=}'[specify configuration file]:configuration file:_files'
+      "(${(j< >)modes})"{-w,--path,--where}'[display file locations]'
+    )
+    [[ $variant == (aix|netbsd|openbsd)* ]] || args+=(
+      # @todo FreeBSD allows this to be given multiple times
+      '(-d --debug)'{-d,--debug}'[display debugging information]'
+    )
+    [[ $variant == (darwin|dragonfly|freebsd|linux|solaris|aix)* ]] && args+=(
+      '(-7 -H -t --ascii --html --troff)'{-t,--troff}'[format man page using troff]'
+    )
+    [[ $variant == (darwin|linux)* ]] && args+=(
+      "(${(j< >)modes})"{-K,--global-apropos}'[search for keyword in all pages]'
+      '(-m --systems)'{-m+,--systems=}'[search manual of specified system]:operating system'
+    )
+    [[ $variant == (darwin|dragonfly|freebsd)* ]] && args+=(
+      '(: -)-h[display help information]'
+      '(-a)-S+[specify manual sections to search]: :->sects'
+    )
+    [[ $variant == (dragonfly|freebsd)* ]] && args+=(
+      # @todo Could enumerate these
+      '-m[search manual of specified architecture]:architecture'
+      '-o[use non-localized man pages]'
+    )
+    [[ $variant == (netbsd|openbsd)* ]] && args+=(
+      '-c[disable paging]'
+      '-m[augment manual search path]:manual search path:_sequence -s\: _directories'
+      '(-a)-s+[specify manual section to search]: :->sects'
+    )
+    [[ $variant == linux* ]] && args+=(
+      '(: -)'{-\?,--help}'[display help information]'
+      '(-7 -t -H -T -Z --ascii --html --troff --troff-device --ditroff)'{-7,--asci}'[translate man pages for 7-bit terminal]'
+      '(-D --default)'{-D,--default}'[reset man to default options]'
+      # @todo Could enumerate these
+      '(-E --encoding)'{-E+,--encoding=}'[specify output encoding]:encoding'
+      '(-e --extension)'{-e+,--extension=}'[specify sub-extension]:sub-extension'
+      '(-H --html)'{-H-,--html=-}'[produce HTML output for specified browser]::Web browser:_path_commands'
+      '(-i -I --ignore-case --match-case)'{-i,--ignore-case}'[search case-insensitively]'
+      '(-i -I --ignore-case --match-case)'{-I,--match-case}'[search case-sensitively]'
+      '(-L --locale)'{-L+,--locale=}'[specify locale]:locale:_locales'
+      "(${(j< >)modes})"{-l+,--local-file=}'[format and display specified file]:*:::manual file:_files'
+      "!(${(j< >)modes})"{--location,--location-cat}
+      '--names-only[match only page names (with --regex or --wildcard)]'
+      '(--nh --no-hyphenation)'{--nh,--no-hyphenation}'[disable hyphenation]'
+      '(--nj --no-justification)'{--nj,--no-justification}'[disable justification]'
+      '--no-subpages[do not combine pairs of page names into single page name]'
+      # @todo Could enumerate these
+      "(${(j< >)modes})"{-R+,--recode=}'[output man page in specified encoding]:encoding'
+      '(-r --prompt)'{-r+,--prompt=}'[specify prompt for less]:less prompt'
+      '(-a --all --wildcard)--regex[treat page name as regular expression]'
+      '(-a -S -s --all --sections)'{-S+,-s+,--sections=}'[specify manual sections to search]: :->sects'
+      # @todo Could enumerate these
+      '(-T -t --troff --troff-device)'{-T-,--troff-device=-}'[specify roff output device]::roff output device'
+      '(-u --update)'{-u,--update}'[update database caches]'
+      '(: -)--usage[display brief usage information]'
+      '(: -)'{-V,--version}'[display version information]'
+      "(${(j< >)modes})"{-W,--where-cat}'[display cat file locations]'
+      '--warnings=[enable specified groff warnings]:groff warnings'
+      '(-a --all --regex)--wildcard[treat page name as shell glob]'
+      # @todo Could enumerate these
+      '(-X --gxditview)'{-X-,--gxditview=-}'[display output in gxditview using specified DPI (default: 75)]::DPI'
+      # @todo Post-process how?
+      '(-t --troff -Z --ditroff)'{-Z,--ditroff}'[post-process output for chosen device]'
+    )
+    [[ $variant == darwin* ]] && args+=(
+      # We use _files here because browsers are usually in /Applications, which
+      # typically isn't in PATH
+      '-B+[specify browser to use for HTML files]:Web browser:_files'
+      '-c[reformat source man page]'
+      # @todo -d should be exclusive with this above
+      '(-d)-D[display man page along with debugging information]'
+      '(-D -F --preformat)'{-F,--preformat}'[format man page only (do not display)]'
+      '-H+[specify command to render HTML as text]:HTML pager:_path_commands'
+      # --help and --version are undocumented but functional
+      '(: -)--help[display help information]'
+      # -s is also undocumented; it's provided for compatibility with Solaris
+      '!(-S)-s+: :->sects'
+      '(: -)'{-v,--version}'[display version information]'
+      "(${(j< >)modes})-W[display file locations, one per line, with no other information]"
+    )
+    [[ $variant == netbsd* ]] && args+=(
+      '-h[display only synopsis lines]'
+      '(: -)-p[display manual search path]'
+      '-S+[display only man pages with file names matching specified string]:search string'
+    )
+    [[ $variant == openbsd* ]] && args+=(
+      "(${(j< >)modes})-l+[format and display specified file]:*:::manual file:_files"
+      # @todo Could enumerate these
+      '-S[search manual of specified architecture]:architecture'
+    )
+    [[ $variant == solaris* ]] && args+=(
+      "(${(j< >)modes})-l[display file locations]"
+      '-r[format man page only (do not display)]'
+      '(-a)-s+[specify manual sections to search]: :->sects'
+      # @todo Does this in fact want a file path?
+      '-T+[format man page using specified macro package]:macro package:_files'
+    )
+    [[ $variant == aix* ]] && args+=(
+      '-c[display man page using cat]'
+      '-F[display only first matching entry]'
+      '-m[only search paths specified by -M/MANPATH]'
+      '-r[search remotely]'
+    )
 
-  if [[ $service == man ]] && (( $words[(I)-l] + $words[(I)--local-file] )); then
-    _files || return 0
+    # Strip (most) long options from non-Linux platforms
+    if [[ $variant == darwin* ]]; then
+      args=( ${(M)args:#((#s)|*\))(\*|)(-[^-]|--(help|path|pref|vers))*} )
+    elif [[ $variant != linux* ]]; then
+      args=( ${(M)args:#((#s)|*\))(\*|)-[^-]*} )
+    fi
   fi
 
+  _arguments -s -S : $args '*::: :->man' && return 0
+  [[ -n $state ]] || return 1
+
   if (( ! $#_manpath )); then
     local mp
     mp=( ${(s.:.)$(manpath 2>/dev/null)} )
@@ -23,14 +176,16 @@ _man() {
   fi
 
   (( $#_manpath )) ||
-      _manpath=( /usr/man(-/) /(opt|usr)/(pkg|dt|share|X11R6|local)/(cat|)man(-/) )
+  _manpath=( /usr/man(-/) /(opt|usr)/(pkg|dt|share|X11R6|local)/(cat|)man(-/) )
 
-  integer index=$words[(I)-M]
-  if (( index )); then
-    local opt
-    opt=$words[index+1]
-    _manpath=($opt)
-  fi
+  # Override man path
+  [[ -n ${opt_args[-M]} ]] &&
+  _manpath=( ${(s<:>)opt_args[-M]} )
+
+  # Augment man path
+  [[ $variant == (netbsd|openbsd)* ]] &&
+  [[ -n ${opt_args[-m]} ]] &&
+  _manpath+=( ${(s<:>)opt_args[-m]} )
 
   # `sman' is the SGML manual directory for Solaris 7.
   # 1M is system administrator commands on SVR4
@@ -44,52 +199,158 @@ _man() {
   # $sect_dirname is from the filesystem, the "3" in "/usr/share/man/man3"
   # These are used by _man_pages
   local sect sect_dirname
-  if [[ $OSTYPE = solaris* ]]; then
-    sect=${${words[(R)-s*]#-s}:-$words[$words[(i)-s]+1]}
-    sect="${sect//,/|}"
-  elif [[ -n ${sect:=$words[$words[(i)-S]+1]} || -n ${sect:=$MANSECT} ]]; then
-    sect="${sect//:/|}"
-    sect="${sect//,/|}"
-  elif (( CURRENT > 2 )); then
-    case $words[2] in
-      (-a) sect='*';;
-      (-*) ;;
-      (*)  sect=$words[2];;
-    esac
+
+  # Take care: We can't use the sections from these options until we've finished
+  # completing them; otherwise (e.g.) -s1:<TAB> will give no results
+  if
+    [[ $service != man ]] || [[ $state == sects ]] || (( $+opt_args[-a] ))
+  then
+    sect='*'
+  elif
+    [[ $variant == (darwin|linux)* ]] &&
+    [[ -n ${opt_args[(i)-S|-s|--sections]} ]]
+  then
+    noinsert=1
+    sect=${opt_args[${opt_args[(i)-S|-s|--sections]}]//[:,]/|}
+  elif
+    [[ $variant == (netbsd|openbsd|solaris)* ]] && (( $+opt_args[-s] ))
+  then
+    noinsert=1
+    sect=${opt_args[-s]//,/|}
+  elif [[ $variant == (dragonfly|freebsd)* ]] && (( $+opt_args[-S] )); then
+    noinsert=1
+    sect=${opt_args[-S]//:/|}
+  elif (( CURRENT > 1 )) && [[ $variant != solaris* ]]; then
+    noinsert=1
+    sect=$words[1]
+  elif [[ -n ${sect:=$MANSECT} ]]; then
+    sect=${sect//:/|}
   fi
 
-  if [[ $sect = (<->*|1M|l|n) || $sect = *\|* ]]; then
-    () {
-      local -a sects=( ${(s.|.)sect} )
-      if [[ $sect != (l|n) ]]; then
-        sects=( ${sects%%[^0-9]#} )
-      fi
-      dirs=( $^_manpath/(sman|man|cat)${^sects}*/ )
-    }
-    if [[ $sect == *\|* ]]; then sect="($sect)"; fi
+  # Colons may have been escaped
+  sect=${(Q)sect}
+
+  if [[ $sect = (<->*|[lnopx]) || $sect = *\|* ]]; then
+    sects=( ${(s.|.)sect} )
+    dirs=( $^_manpath/(sman|man|cat)${^sects}/ )
+    sect=${(j<|>)sects}
+    [[ $sect == *'|'* ]] && sect="($sect)"
     awk="\$2 == \"$sect\" {print \$1}"
   else
+    sect=
     dirs=( $^_manpath/(sman|man|cat)*/ )
     awk='{print $1}'
   fi
+
+  # Ignore directories with no pages inside
+  dirs=( ${^dirs}(#qFN) )
+
   # Solaris 11 and on have a man-index directory that doesn't contain manpages
   dirs=( ${dirs:#*/man-index/} )
-  if [[ $OSTYPE = solaris* && ( $words[CURRENT] = -s* || $words[CURRENT-1] == -s ) ]]; then
-    [[ $words[CURRENT] = -s* ]] && compset -P '-s'
-    sects=( ${(o)${dirs##*(man|cat)}%/} )
-    _wanted sections expl 'section' compadd -a sects
-  elif zstyle -t ":completion:${curcontext}:manuals" separate-sections; then
-    typeset -U sects
-    local ret=1
+  sects=( ${(o)${dirs##*(man|cat)}%/} )
+
+  # If we've got this far, we can build our look-up table for descriptions of
+  # the more common sections. Unless otherwise labelled, the more specific ones
+  # come from Solaris or one of its variants
+  (( $#sects )) && () {
+    sect_descs=(
+      0        'library headers'
+      1        'general commands'
+      1cups    'CUPS commands'
+      1m       'maintenance commands'
+      1openssl 'OpenSSL commands'
+      2        'system calls'
+      3        'library functions'
+      3c       'C library functions'
+      3curses  'curses library functions'
+      3elf     'ELF library functions'
+      3f       'Fortran library functions'
+      3lua     'Lua features' # NetBSD
+      3mail    'mailbox library functions'
+      3openssl 'OpenSSL library functions'
+      3pam     'PAM library functions'
+      3pool    'pool configuration library functions'
+      3proc    'process control library functions'
+      3x11     'Xlib functions'
+      3xcurses 'curses library functions [X/Open]'
+      4        'devices and drivers'
+      5        'file formats and conventions'
+      3openssl 'OpenSSL configuration files'
+      6        'games'
+      7        'miscellanea'
+      8        'maintenance commands and procedures'
+      9        'kernel features'
+      9lua     'Lua kernel bindings' # NetBSD
+      l        'local documentation' # AIX, etc.
+      n        'new documentation' # AIX, etc.
+      o        'old documentation' # AIX, etc.
+      p        'public documentation' # AIX, etc.
+      x        'X11 features'
+    )
+
+    # Add POSIX variants
+    for 1 in ${(k)sect_descs}; do
+      [[ $1 == <-> ]] || continue
+      sect_descs+=( "${1}p" "${sect_descs[$1]} [POSIX]" )
+    done
+
+    # Add OS-specific stuff that's too risky for or overrides the general list
+    [[ $OSTYPE == darwin*  ]] && sect_descs+=( n 'Tcl/Tk features' )
+    [[ $OSTYPE == openbsd* ]] && sect_descs+=( 3p 'Perl features' )
+    [[ $OSTYPE == solaris* ]] && sect_descs+=(
+      1t  'Tcl/Tk features'
+      3m  'mathematical library functions'
+      4   'file formats and conventions'
+      5   'miscellanea'
+      7   'special files'
+      7d  'devices'
+      7fs 'file systems'
+      7i  'ioctl requests'
+      7m  'STREAMS modules'
+      7p  'protocols'
+      9e  'driver entry points'
+      9f  'driver functions'
+      9p  'driver properties'
+      9s  'driver data structures'
+    )
+  }
 
-    sects=( ${(o)${dirs##*(man|cat)}%/} )
+  [[ $state == sects ]] && {
+    local s
+    local -a specs
+
+    (( $#sects )) || {
+      _message 'manual section'
+      return 1
+    }
+
+    # Build specs from descriptions
+    for s in $sects; do
+      specs+=( "${s}[${(b)sect_descs[$s]}]" )
+    done
+
+    if [[ $variant == (darwin|dragonfly|freebsd|linux)* ]]; then
+      _values -s : 'manual section' $specs
+    elif [[ $variant == solaris* ]]; then
+      _values -s , 'manual section' $specs
+    else
+      _values 'manual section' $specs
+    fi
+    return
+  }
+
+  if zstyle -t ":completion:${curcontext}:manuals" separate-sections; then
+    local d ret=1
 
     (( $#sects )) || return 1
 
     _tags manuals.${^sects}
     while _tags; do
       for sect_dirname in $sects; do
-        _requested manuals.$sect_dirname expl "manual page, section $sect_dirname" _man_pages &&
+        d=$sect_dirname
+        (( $+sect_descs[$d] )) && d+=" (${sect_descs[$d]})"
+
+        _requested manuals.$sect_dirname expl "manual page, section $d" _man_pages &&
             ret=0
       done
       (( ret )) || return 0
@@ -113,7 +374,7 @@ _man_pages() {
   local pages sopt
 
   # What files corresponding to manual pages can end in.
-  local suf='.((?|<->*)(|.gz|.bz2|.Z|.lzma))'
+  local suf='.((?|<->*|ntcl)(|.gz|.bz2|.Z|.lzma))'
 
   if [[ $PREFIX$SUFFIX = */* ]]; then
     # Easy way to test for versions of man that allow file names.
@@ -138,8 +399,8 @@ _man_pages() {
   # beginning with .<->: that handles problem cases like files called
   # `POSIX.1.5'.
 
-  [[ $OSTYPE = solaris* ]] && sopt='-s '
-  if ((CURRENT > 2)) ||
+  [[ $variant = solaris* ]] && sopt='-s '
+  if ((CURRENT > 1 || noinsert)) ||
       ! zstyle -t ":completion:${curcontext}:manuals.$sect_dirname" insert-sections
   then
     compadd "$@" - ${pages%$~suf}



Messages sorted by: Reverse Date, Date, Thread, Author