17397739 pkg should emit a brief help message for unknown subcommands s12b73
authorYiteng Zhang <yiteng.zhang@oracle.com>
Fri, 24 Apr 2015 14:42:07 -0700
changeset 3198 e3f25a90d21c
parent 3197 6699b44d5dee
child 3199 ee8403e8122b
17397739 pkg should emit a brief help message for unknown subcommands
src/client.py
src/man/pkg.1
src/modules/misc.py
src/tests/cli/t_pkg_help.py
--- a/src/client.py	Thu Apr 23 15:55:54 2015 -0700
+++ b/src/client.py	Fri Apr 24 14:42:07 2015 -0700
@@ -143,7 +143,8 @@
         # program name on all platforms.
         logger.error(ws + pkg_cmd + text_nows)
 
-def usage(usage_error=None, cmd=None, retcode=EXIT_BADOPT, full=False):
+def usage(usage_error=None, cmd=None, retcode=EXIT_BADOPT, full=False,
+    verbose=False, unknown_cmd=None):
         """Emit a usage message and optionally prefix it with a more
             specific error message.  Causes program to exit. """
 
@@ -397,27 +398,57 @@
                 sys.exit(retcode)
 
         elif not full:
-                # The full usage message isn't desired.
-                logger.error(_("Try `pkg --help or -?' for more information."))
+                # The full list of subcommands isn't desired.
+                known_words = ["help"]
+                known_words.extend(basic_cmds)
+                known_words.extend(w for w in advanced_cmds if w)
+                candidates = misc.suggest_known_words(unknown_cmd, known_words)
+                if candidates:
+                        # Suggest correct subcommands if we can.
+                        words = ", ". join(candidates)
+                        logger.error(_("Did you mean:\n    {0}\n").format(words))
+                logger.error(_("For a full list of subcommands, run: pkg help"))
                 sys.exit(retcode)
 
-        logger.error(_("""\
+        if verbose:
+                # Display a verbose usage message of subcommands.
+                logger.error(_("""\
 Usage:
         pkg [options] command [cmd_options] [operands]
 """))
-        logger.error(_("Basic subcommands:"))
-        print_cmds(basic_cmds, basic_usage)
-
-        logger.error(_("\nAdvanced subcommands:"))
-        print_cmds(advanced_cmds, adv_usage)
-
-        logger.error(_("""
+                logger.error(_("Basic subcommands:"))
+                print_cmds(basic_cmds, basic_usage)
+
+                logger.error(_("\nAdvanced subcommands:"))
+                print_cmds(advanced_cmds, adv_usage)
+
+                logger.error(_("""
 Options:
         -R dir
         --help or -?
 
 Environment:
         PKG_IMAGE"""))
+        else:
+                # Display the full list of subcommands.
+                logger.error(_("""\
+Usage:    pkg [options] command [cmd_options] [operands]"""))
+                logger.error(_("The following commands are supported:"))
+                logger.error(_("""
+Package Information  : list           search         info      contents
+Package Transitions  : update         install        uninstall
+                       history        exact-install
+Package Maintenance  : verify         fix            revert
+Publishers           : publisher      set-publisher  unset-publisher
+Package Configuration: mediator       set-mediator   unset-mediator
+                       facet          change-facet
+                       variant        change-variant
+Image Constraints    : avoid          unavoid        freeze    unfreeze
+Image Configuration  : refresh        rebuild-index  purge-history
+                       property       set-property   add-property-value
+                       unset-property remove-property-value
+Miscellaneous        : image-create   dehydrate      rehydrate
+For more info, run: pkg help <command>"""))
         sys.exit(retcode)
 
 def get_fmri_args(api_inst, pargs, cmd=None):
@@ -5196,9 +5227,14 @@
                                 if sub in cmds and \
                                     sub not in ["help", "-?", "--help"]:
                                         usage(retcode=0, full=False, cmd=sub)
+                                elif sub == "-v":
+                                        # Only display the long usage message
+                                        # in the verbose mode.
+                                        usage(retcode=0, full=True,
+                                            verbose=True)
                                 elif sub not in ["help", "-?", "--help"]:
                                         usage(_("unknown subcommand "
-                                            "'{0}'").format(sub), full=True)
+                                            "'{0}'").format(sub), unknown_cmd=sub)
                                 else:
                                         usage(retcode=0, full=True)
                         else:
@@ -5209,11 +5245,11 @@
                 usage(retcode=0, cmd=subcommand, full=False)
         if subcommand and subcommand not in cmds:
                 usage(_("unknown subcommand '{0}'").format(subcommand),
-                    full=True)
+                    unknown_cmd=subcommand)
         if show_usage:
                 usage(retcode=0, full=True)
         if not subcommand:
-                usage(_("no subcommand specified"))
+                usage(_("no subcommand specified"), full=True)
         if runid is not None:
                 try:
                         runid = int(runid)
--- a/src/man/pkg.1	Thu Apr 23 15:55:54 2015 -0700
+++ b/src/man/pkg.1	Fri Apr 24 14:42:07 2015 -0700
@@ -7,11 +7,11 @@
 
 <refentry id="pkg-1">
 <refmeta><refentrytitle>pkg</refentrytitle><manvolnum>1</manvolnum>
-<refmiscinfo class="date">12 Nov 2014</refmiscinfo>
+<refmiscinfo class="date">22 Apr 2015</refmiscinfo>
 <refmiscinfo class="sectdesc">&man1;</refmiscinfo>
 <refmiscinfo class="software">&release;</refmiscinfo>
 <refmiscinfo class="arch">generic</refmiscinfo>
-<refmiscinfo class="copyright">Copyright (c) 2007, 2014, Oracle and/or its affiliates. All rights reserved.</refmiscinfo>
+<refmiscinfo class="copyright">Copyright (c) 2007, 2015, Oracle and/or its affiliates. All rights reserved.</refmiscinfo>
 </refmeta>
 <refnamediv>
 <refname>pkg</refname><refpurpose>Image Packaging System retrieval client</refpurpose>
@@ -152,7 +152,7 @@
 <synopsis>/usr/bin/pkg rebuild-index</synopsis>
 <synopsis>/usr/bin/pkg update-format</synopsis>
 <synopsis>/usr/bin/pkg version</synopsis>
-<synopsis>/usr/bin/pkg help</synopsis>
+<synopsis>/usr/bin/pkg help [-v]</synopsis>
 <synopsis>/usr/bin/pkg image-create [-FPUfz] [--force]
     [--full | --partial | --user] [--zone]
     [-c <replaceable>ssl_cert</replaceable>] [-k <replaceable>ssl_key</replaceable>]
@@ -1650,9 +1650,13 @@
 This string is not guaranteed to be comparable in any fashion between versions.</para>
 </listitem>
 </varlistentry>
-<varlistentry><term><command>pkg help</command></term>
-<listitem><para>Display a usage message.</para>
+<varlistentry><term><command>pkg help</command> [<option>v</option>]</term>
+<listitem><para>Display a full list of subcommands.</para>
 </listitem>
+<listitem><varlistentry><term><option>v</option></term>
+<listitem><para>Display a verbose usage message of subcommands.</para>
+</listitem>
+</varlistentry></listitem>
 </varlistentry>
 <varlistentry><term><command>pkg image-create</command> [<option>FPUfz</option>] [<option>-force</option>] [<option>-full</option> | <option>-partial</option> | <option>-user</option>] [<option>-zone</option>] [<option>c</option> <replaceable>ssl_cert</replaceable>] [<option>k</option> <replaceable> ssl_key</replaceable>] [<option>g</option> <replaceable>path_or_uri</replaceable> | <option>-origin</option> <replaceable>path_or_uri</replaceable>]... [<option>m</option> <replaceable>uri</replaceable> | <option>-mirror</option> <replaceable>uri</replaceable>]... [<option>-facet</option> <replaceable>facet_name</replaceable>=(<literal>True</literal>|<literal>False</literal>)]... [<option>-no-refresh</option>] [<option>-set-property</option> <replaceable>name_of_property</replaceable>=<replaceable>value</replaceable>] [<option>-variant</option> <replaceable>variant_name</replaceable>=<replaceable>value</replaceable>]... [(<option>p</option> | <option>-publisher</option>) [<replaceable>name</replaceable>=]<replaceable>repo_uri</replaceable>] <replaceable>dir</replaceable></term>
 <listitem><para>At the location given by <replaceable>dir</replaceable>, create an image suitable for package operations. Images created by using the <command>image-create</command> subcommand are not bootable. Most users should use the <option>-be-name</option> or <option>-require-new-be</option> options with <command>pkg</command> commands, or use the <command>beadm</command> or <command>zoneadm</command> commands to create images. The <command>pkg image-create</command> command is used for tasks such as maintaining packages and operating system distributions.</para>
--- a/src/modules/misc.py	Thu Apr 23 15:55:54 2015 -0700
+++ b/src/modules/misc.py	Fri Apr 24 14:42:07 2015 -0700
@@ -57,6 +57,7 @@
 import zlib
 
 from collections import defaultdict
+from operator import itemgetter
 
 from stat import S_IFMT, S_IMODE, S_IRGRP, S_IROTH, S_IRUSR, S_IRWXU, \
     S_ISBLK, S_ISCHR, S_ISDIR, S_ISFIFO, S_ISLNK, S_ISREG, S_ISSOCK, \
@@ -2764,3 +2765,70 @@
                         last_line = line
                         yield line
 
+def _min_edit_distance(word1, word2):
+        """Calculate the minimal edit distance for converting word1 to word2,
+        based on Wagner-Fischer algorithm."""
+
+        m = len(word1)
+        n = len(word2)
+
+        # dp[i][j] stands for the edit distance between two strings with
+        # length i and j, i.e., word1[0,...,i-1] and word2[0,...,j-1]
+        dp = [[0 for i in range(n+1)] for j in range(m+1)]
+
+        ins_cost = 1.0
+        del_cost = 1.0
+        rep_cost = 1.0
+        for i in range(m+1):
+                dp[i][0] = del_cost * i
+        for i in range(n+1):
+                dp[0][i] = ins_cost * i
+
+        for i in range(1, m+1):
+                for j in range(1, n+1):
+                        if word1[i-1] == word2[j-1]:
+                                dp[i][j] = dp[i-1][j-1]
+                        else:
+                                dp[i][j] = min(
+                                    dp[i-1][j-1] + rep_cost,
+                                    dp[i][j-1] + ins_cost,
+                                    dp[i-1][j] + del_cost)
+
+        return dp[m][n]
+
+def suggest_known_words(text, known_words):
+        """Given a text, a list of known_words, suggest some correct
+        candidates from known_words."""
+
+        candidates = []
+        if not text:
+                return candidates
+
+        # We are confident to suggest if the text is part of the known words.
+        for known in known_words:
+                if len(text) < 4:
+                        # If the text's length is short, treat it as a prefix.
+                        if known.startswith(text):
+                                candidates.append(known)
+                elif text in known or known in text:
+                        # Otherwise check if the text is part of the known
+                        # words or vice verse.
+                        candidates.append(known)
+
+        if candidates:
+                if len(candidates) < 4:
+                        return candidates
+                else:
+                        # Give up suggestions if there are too many candidates.
+                        return
+
+        # If there are no candidates from the "contains" check, use the edit
+        # distance algorithm to seek further.
+        for known in known_words:
+                distance = _min_edit_distance(text, known)
+                if distance <= len(known) / 2.0:
+                        candidates.append((known, distance))
+
+        # Sort the candidates by their distance, and return the words only.
+        return [c[0] for c in sorted(candidates, key=itemgetter(1))]
+
--- a/src/tests/cli/t_pkg_help.py	Thu Apr 23 15:55:54 2015 -0700
+++ b/src/tests/cli/t_pkg_help.py	Fri Apr 24 14:42:07 2015 -0700
@@ -54,8 +54,15 @@
                                         self.assert_(False, "{0} in {1}".format(
                                             str, msg))
 
+                # Full list of subcommands, ensuring we exit 0
+                for option in ["-\?", "--help", "help"]:
+                        ret, out, err = self.pkg(option, out=True, stderr=True)
+                        verify_help(err,
+                            ["pkg [options] command [cmd_options] [operands]",
+                            "For more info, run: pkg help <command>"])
+
                 # Full usage text, ensuring we exit 0
-                for option in ["-\?", "--help", "help"]:
+                for option in ["help -v"]:
                         ret, out, err = self.pkg(option, out=True, stderr=True)
                         verify_help(err,
                             ["pkg [options] command [cmd_options] [operands]",
@@ -68,16 +75,16 @@
                         ret, out, err = self.pkg("-\? bobcat", exit=2, out=True,
                             stderr=True)
                         verify_help(err,
-                            ["pkg [options] command [cmd_options] [operands]",
-                            "pkg: unknown subcommand",
-                            "PKG_IMAGE", "Usage:"])
+                            ["pkg: unknown subcommand",
+                            "For a full list of subcommands, run: pkg help"])
 
                 # Unrequested usage
                 ret, out, err = self.pkg("", exit=2, out=True, stderr=True)
                 verify_help(err,
                     ["pkg: no subcommand specified",
-                    "Try `pkg --help or -?' for more information."],
-                    unexpected = ["PKG_IMAGE", "Usage:"])
+                    "pkg [options] command [cmd_options] [operands]",
+                    "For more info, run: pkg help <command>"],
+                    unexpected = ["PKG_IMAGE"])
 
                 # help for a subcommand should only print that subcommand usage
                 for option in ["property --help", "--help property",
@@ -113,7 +120,7 @@
                 f = codecs.open(eucJP_encode_file, encoding="eucJP")
 
                 locale_env = { "LC_ALL": "ja_JP.eucJP" }
-                ret, out, err = self.pkg("--help", env_arg=locale_env,
+                ret, out, err = self.pkg("help -v", env_arg=locale_env,
                     out=True, stderr=True)
                 cmd_out = unicode(err, encoding="eucJP")
                 # Take only 4 lines from "pkg --help" command output.