lshal: add HelpCommand

Add *Command::usage() function for each Command and let
Lshal class to call them.

Suppress output from getopt_long and write our own
error message to customized error stream (for testing).

Test: lshal_test
Test: lshal --help

Change-Id: I8f5847c84a3e01af29fa85871479cab3baeb5312
diff --git a/cmds/lshal/Android.bp b/cmds/lshal/Android.bp
index 4781468..5a87505 100644
--- a/cmds/lshal/Android.bp
+++ b/cmds/lshal/Android.bp
@@ -25,6 +25,7 @@
     ],
     srcs: [
         "DebugCommand.cpp",
+        "HelpCommand.cpp",
         "Lshal.cpp",
         "ListCommand.cpp",
         "PipeRelay.cpp",
diff --git a/cmds/lshal/Command.h b/cmds/lshal/Command.h
index b1efb97..aff4975 100644
--- a/cmds/lshal/Command.h
+++ b/cmds/lshal/Command.h
@@ -31,7 +31,9 @@
     virtual ~Command() = default;
     // Expect optind to be set by Lshal::main and points to the next argument
     // to process.
-    virtual Status main(const std::string &command, const Arg &arg) = 0;
+    virtual Status main(const Arg &arg) = 0;
+
+    virtual void usage() const = 0;
 
 protected:
     Lshal& mLshal;
diff --git a/cmds/lshal/DebugCommand.cpp b/cmds/lshal/DebugCommand.cpp
index e657bcf..d21764c 100644
--- a/cmds/lshal/DebugCommand.cpp
+++ b/cmds/lshal/DebugCommand.cpp
@@ -21,9 +21,8 @@
 namespace android {
 namespace lshal {
 
-Status DebugCommand::parseArgs(const std::string &command, const Arg &arg) {
+Status DebugCommand::parseArgs(const Arg &arg) {
     if (optind >= arg.argc) {
-        mLshal.usage(command);
         return USAGE;
     }
     mInterfaceName = arg.argv[optind];
@@ -34,8 +33,8 @@
     return OK;
 }
 
-Status DebugCommand::main(const std::string &command, const Arg &arg) {
-    Status status = parseArgs(command, arg);
+Status DebugCommand::main(const Arg &arg) {
+    Status status = parseArgs(arg);
     if (status != OK) {
         return status;
     }
@@ -46,6 +45,19 @@
             mLshal.err());
 }
 
+void DebugCommand::usage() const {
+
+    static const std::string debug =
+            "debug:\n"
+            "    lshal debug <interface> [options [options [...]]] \n"
+            "        Print debug information of a specified interface.\n"
+            "        <inteface>: Format is `android.hardware.foo@1.0::IFoo/default`.\n"
+            "            If instance name is missing `default` is used.\n"
+            "        options: space separated options to IBase::debug.\n";
+
+    mLshal.err() << debug;
+}
+
 }  // namespace lshal
 }  // namespace android
 
diff --git a/cmds/lshal/DebugCommand.h b/cmds/lshal/DebugCommand.h
index afa5e97..6b70713 100644
--- a/cmds/lshal/DebugCommand.h
+++ b/cmds/lshal/DebugCommand.h
@@ -33,9 +33,10 @@
 public:
     DebugCommand(Lshal &lshal) : Command(lshal) {}
     ~DebugCommand() = default;
-    Status main(const std::string &command, const Arg &arg) override;
+    Status main(const Arg &arg) override;
+    void usage() const override;
 private:
-    Status parseArgs(const std::string &command, const Arg &arg);
+    Status parseArgs(const Arg &arg);
 
     std::string mInterfaceName;
     std::vector<std::string> mOptions;
diff --git a/cmds/lshal/HelpCommand.cpp b/cmds/lshal/HelpCommand.cpp
new file mode 100644
index 0000000..b393f05
--- /dev/null
+++ b/cmds/lshal/HelpCommand.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "HelpCommand.h"
+
+#include "Lshal.h"
+
+namespace android {
+namespace lshal {
+
+Status HelpCommand::main(const Arg &arg) {
+    if (optind >= arg.argc) {
+        // `lshal help` prints global usage.
+        mLshal.usage();
+        return OK;
+    }
+    (void)usageOfCommand(arg.argv[optind]);
+    return OK;
+}
+
+Status HelpCommand::usageOfCommand(const std::string& c) const {
+    if (c.empty()) {
+        mLshal.usage();
+        return USAGE;
+    }
+    auto command = mLshal.selectCommand(c);
+    if (command == nullptr) {
+        // from HelpCommand::main, `lshal help unknown`
+        mLshal.usage();
+        return USAGE;
+    }
+
+    command->usage();
+    return USAGE;
+
+}
+
+void HelpCommand::usage() const {
+    static const std::string help =
+            "help:\n"
+            "    lshal -h\n"
+            "    lshal --help\n"
+            "    lshal help\n"
+            "        Print this help message\n"
+            "    lshal help list\n"
+            "        Print help message for list\n"
+            "    lshal help debug\n"
+            "        Print help message for debug\n";
+
+    mLshal.err() << help;
+}
+
+}  // namespace lshal
+}  // namespace android
+
diff --git a/cmds/lshal/HelpCommand.h b/cmds/lshal/HelpCommand.h
new file mode 100644
index 0000000..3cc0d80
--- /dev/null
+++ b/cmds/lshal/HelpCommand.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FRAMEWORK_NATIVE_CMDS_LSHAL_HELP_COMMAND_H_
+#define FRAMEWORK_NATIVE_CMDS_LSHAL_HELP_COMMAND_H_
+
+#include <string>
+
+#include <android-base/macros.h>
+
+#include "Command.h"
+#include "utils.h"
+
+namespace android {
+namespace lshal {
+
+class Lshal;
+
+class HelpCommand : public Command {
+public:
+    HelpCommand(Lshal &lshal) : Command(lshal) {}
+    ~HelpCommand() = default;
+    Status main(const Arg &arg) override;
+    void usage() const override;
+    Status usageOfCommand(const std::string& c) const;
+};
+
+
+}  // namespace lshal
+}  // namespace android
+
+#endif  // FRAMEWORK_NATIVE_CMDS_LSHAL_HELP_COMMAND_H_
diff --git a/cmds/lshal/ListCommand.cpp b/cmds/lshal/ListCommand.cpp
index ffb4424..71ac25b 100644
--- a/cmds/lshal/ListCommand.cpp
+++ b/cmds/lshal/ListCommand.cpp
@@ -604,7 +604,7 @@
     return status;
 }
 
-Status ListCommand::parseArgs(const std::string &command, const Arg &arg) {
+Status ListCommand::parseArgs(const Arg &arg) {
     static struct option longOptions[] = {
         // long options with short alternatives
         {"help",      no_argument,       0, 'h' },
@@ -628,6 +628,9 @@
     std::vector<TableColumnType> selectedColumns;
     bool enableCmdlines = false;
 
+    // suppress output to std::err for unknown options
+    opterr = 0;
+
     int optionIndex;
     int c;
     // Lshal::parseArgs has set optind to the next option to parse
@@ -646,7 +649,6 @@
                 mSortColumn = TableEntry::sortByServerPid;
             } else {
                 err() << "Unrecognized sorting column: " << optarg << std::endl;
-                mLshal.usage(command);
                 return USAGE;
             }
             break;
@@ -697,22 +699,22 @@
             mNeat = true;
             break;
         }
-        case 'h': // falls through
+        case 'h': {
+            return USAGE;
+        }
         default: // see unrecognized options
-            mLshal.usage(command);
+            err() << "unrecognized option `" << arg.argv[optind - 1] << "'" << std::endl;
             return USAGE;
         }
     }
     if (optind < arg.argc) {
         // see non option
-        err() << "Unrecognized option `" << arg.argv[optind] << "`" << std::endl;
-        mLshal.usage(command);
+        err() << "unrecognized option `" << arg.argv[optind] << "'" << std::endl;
         return USAGE;
     }
 
     if (mNeat && mEmitDebugInfo) {
         err() << "Error: --neat should not be used with --debug." << std::endl;
-        mLshal.usage(command);
         return USAGE;
     }
 
@@ -739,8 +741,8 @@
     return OK;
 }
 
-Status ListCommand::main(const std::string &command, const Arg &arg) {
-    Status status = parseArgs(command, arg);
+Status ListCommand::main(const Arg &arg) {
+    Status status = parseArgs(arg);
     if (status != OK) {
         return status;
     }
@@ -750,6 +752,41 @@
     return status;
 }
 
+void ListCommand::usage() const {
+
+    static const std::string list =
+            "list:\n"
+            "    lshal\n"
+            "    lshal list\n"
+            "        List all hals with default ordering and columns (`lshal list -iepc`)\n"
+            "    lshal list [-h|--help]\n"
+            "        -h, --help: Print help message for list (`lshal help list`)\n"
+            "    lshal [list] [--interface|-i] [--transport|-t] [-r|--arch] [-e|--threads]\n"
+            "            [--pid|-p] [--address|-a] [--clients|-c] [--cmdline|-m]\n"
+            "            [--sort={interface|i|pid|p}] [--init-vintf[=<output file>]]\n"
+            "            [--debug|-d[=<output file>]] [--neat]\n"
+            "        -i, --interface: print the interface name column\n"
+            "        -n, --instance: print the instance name column\n"
+            "        -t, --transport: print the transport mode column\n"
+            "        -r, --arch: print if the HAL is in 64-bit or 32-bit\n"
+            "        -e, --threads: print currently used/available threads\n"
+            "                       (note, available threads created lazily)\n"
+            "        -p, --pid: print the server PID, or server cmdline if -m is set\n"
+            "        -a, --address: print the server object address column\n"
+            "        -c, --clients: print the client PIDs, or client cmdlines if -m is set\n"
+            "        -m, --cmdline: print cmdline instead of PIDs\n"
+            "        -d[=<output file>], --debug[=<output file>]: emit debug info from \n"
+            "                IBase::debug with empty options. Cannot be used with --neat.\n"
+            "        --sort=i, --sort=interface: sort by interface name\n"
+            "        --sort=p, --sort=pid: sort by server pid\n"
+            "        --neat: output is machine parsable (no explanatory text)\n"
+            "                Cannot be used with --debug.\n"
+            "        --init-vintf[=<output file>]: form a skeleton HAL manifest to specified\n"
+            "                      file, or stdout if no file specified.\n";
+
+    err() << list;
+}
+
 }  // namespace lshal
 }  // namespace android
 
diff --git a/cmds/lshal/ListCommand.h b/cmds/lshal/ListCommand.h
index 8d25d94..6defb0a 100644
--- a/cmds/lshal/ListCommand.h
+++ b/cmds/lshal/ListCommand.h
@@ -47,9 +47,10 @@
 public:
     ListCommand(Lshal &lshal) : Command(lshal) {}
     virtual ~ListCommand() = default;
-    Status main(const std::string &command, const Arg &arg) override;
+    Status main(const Arg &arg) override;
+    void usage() const override;
 protected:
-    Status parseArgs(const std::string &command, const Arg &arg);
+    Status parseArgs(const Arg &arg);
     Status fetch();
     void postprocess();
     Status dump();
diff --git a/cmds/lshal/Lshal.cpp b/cmds/lshal/Lshal.cpp
index 9c5c234..a08a02c 100644
--- a/cmds/lshal/Lshal.cpp
+++ b/cmds/lshal/Lshal.cpp
@@ -48,7 +48,7 @@
 
 }
 
-void Lshal::usage(const std::string &command) const {
+void Lshal::usage() {
     static const std::string helpSummary =
             "lshal: List and debug HALs.\n"
             "\n"
@@ -59,65 +59,12 @@
             "\n"
             "If no command is specified, `list` is the default.\n";
 
-    static const std::string list =
-            "list:\n"
-            "    lshal\n"
-            "    lshal list\n"
-            "        List all hals with default ordering and columns (`lshal list -iepc`)\n"
-            "    lshal list [-h|--help]\n"
-            "        -h, --help: Print help message for list (`lshal help list`)\n"
-            "    lshal [list] [--interface|-i] [--transport|-t] [-r|--arch] [-e|--threads]\n"
-            "            [--pid|-p] [--address|-a] [--clients|-c] [--cmdline|-m]\n"
-            "            [--sort={interface|i|pid|p}] [--init-vintf[=<output file>]]\n"
-            "            [--debug|-d[=<output file>]] [--neat]\n"
-            "        -i, --interface: print the interface name column\n"
-            "        -n, --instance: print the instance name column\n"
-            "        -t, --transport: print the transport mode column\n"
-            "        -r, --arch: print if the HAL is in 64-bit or 32-bit\n"
-            "        -e, --threads: print currently used/available threads\n"
-            "                       (note, available threads created lazily)\n"
-            "        -p, --pid: print the server PID, or server cmdline if -m is set\n"
-            "        -a, --address: print the server object address column\n"
-            "        -c, --clients: print the client PIDs, or client cmdlines if -m is set\n"
-            "        -m, --cmdline: print cmdline instead of PIDs\n"
-            "        -d[=<output file>], --debug[=<output file>]: emit debug info from \n"
-            "                IBase::debug with empty options. Cannot be used with --neat.\n"
-            "        --sort=i, --sort=interface: sort by interface name\n"
-            "        --sort=p, --sort=pid: sort by server pid\n"
-            "        --neat: output is machine parsable (no explanatory text)\n"
-            "                Cannot be used with --debug.\n"
-            "        --init-vintf[=<output file>]: form a skeleton HAL manifest to specified\n"
-            "                      file, or stdout if no file specified.\n";
-
-    static const std::string debug =
-            "debug:\n"
-            "    lshal debug <interface> [options [options [...]]] \n"
-            "        Print debug information of a specified interface.\n"
-            "        <inteface>: Format is `android.hardware.foo@1.0::IFoo/default`.\n"
-            "            If instance name is missing `default` is used.\n"
-            "        options: space separated options to IBase::debug.\n";
-
-    static const std::string help =
-            "help:\n"
-            "    lshal -h\n"
-            "    lshal --help\n"
-            "    lshal help\n"
-            "        Print this help message\n"
-            "    lshal help list\n"
-            "        Print help message for list\n"
-            "    lshal help debug\n"
-            "        Print help message for debug\n";
-
-    if (command == "list") {
-        err() << list;
-        return;
-    }
-    if (command == "debug") {
-        err() << debug;
-        return;
-    }
-
-    err() << helpSummary << "\n" << list << "\n" << debug << "\n" << help;
+    err() << helpSummary << "\n";
+    selectCommand("list")->usage();
+    err() << "\n";
+    selectCommand("debug")->usage();
+    err() << "\n";
+    selectCommand("help")->usage();
 }
 
 // A unique_ptr type using a custom deleter function.
@@ -206,8 +153,7 @@
         return OK;
     }
 
-    err() << arg.argv[0] << ": unrecognized option `" << arg.argv[optind] << "`" << std::endl;
-    usage();
+    err() << arg.argv[0] << ": unrecognized option `" << arg.argv[optind] << "'" << std::endl;
     return USAGE;
 }
 
@@ -218,6 +164,10 @@
     }
 }
 
+std::unique_ptr<HelpCommand> Lshal::selectHelpCommand() {
+    return std::make_unique<HelpCommand>(*this);
+}
+
 std::unique_ptr<Command> Lshal::selectCommand(const std::string& command) {
     // Default command is list
     if (command == "list" || command == "") {
@@ -226,6 +176,9 @@
     if (command == "debug") {
         return std::make_unique<DebugCommand>(*this);
     }
+    if (command == "help") {
+        return selectHelpCommand();
+    }
     return nullptr;
 }
 
@@ -235,18 +188,24 @@
 
     Status status = parseArgs(arg);
     if (status != OK) {
+        usage();
         return status;
     }
-    if (mCommand == "help") {
-        usage(optind < arg.argc ? arg.argv[optind] : "");
+    auto c = selectCommand(mCommand);
+    if (c == nullptr) {
+        // unknown command, print global usage
+        usage();
         return USAGE;
     }
-    auto c = selectCommand(mCommand);
-    if (c != nullptr) {
-        return c->main(mCommand, arg);
+    status = c->main(arg);
+    if (status == USAGE) {
+        // bad options. Run `lshal help ${mCommand}` instead.
+        // For example, `lshal --unknown-option` becomes `lshal help` (prints global help)
+        // and `lshal list --unknown-option` becomes `lshal help list`
+        return selectHelpCommand()->usageOfCommand(mCommand);
     }
-    usage();
-    return USAGE;
+
+    return status;
 }
 
 NullableOStream<std::ostream> Lshal::err() const {
diff --git a/cmds/lshal/Lshal.h b/cmds/lshal/Lshal.h
index bfd6a40..89b38db 100644
--- a/cmds/lshal/Lshal.h
+++ b/cmds/lshal/Lshal.h
@@ -25,6 +25,7 @@
 #include <utils/StrongPointer.h>
 
 #include "Command.h"
+#include "HelpCommand.h"
 #include "NullableOStream.h"
 #include "utils.h"
 
@@ -39,7 +40,8 @@
             sp<hidl::manager::V1_0::IServiceManager> serviceManager,
             sp<hidl::manager::V1_0::IServiceManager> passthroughManager);
     Status main(const Arg &arg);
-    void usage(const std::string &command = "") const;
+    // global usage
+    void usage();
     virtual NullableOStream<std::ostream> err() const;
     virtual NullableOStream<std::ostream> out() const;
     const sp<hidl::manager::V1_0::IServiceManager> &serviceManager() const;
@@ -56,6 +58,7 @@
 
 private:
     Status parseArgs(const Arg &arg);
+    std::unique_ptr<HelpCommand> selectHelpCommand();
 
     std::string mCommand;
     Arg mCmdArgs;
diff --git a/cmds/lshal/test.cpp b/cmds/lshal/test.cpp
index 44b196e..06b6819 100644
--- a/cmds/lshal/test.cpp
+++ b/cmds/lshal/test.cpp
@@ -183,8 +183,8 @@
 public:
     MockListCommand(Lshal* lshal) : ListCommand(*lshal) {}
 
-    Status parseArgs(const Arg& arg) { return ListCommand::parseArgs("", arg); }
-    Status main(const Arg& arg) { return ListCommand::main("", arg); }
+    Status parseArgs(const Arg& arg) { return ListCommand::parseArgs(arg); }
+    Status main(const Arg& arg) { return ListCommand::main(arg); }
     void forEachTable(const std::function<void(const Table &)> &f) const {
         return ListCommand::forEachTable(f);
     }
@@ -528,6 +528,70 @@
     EXPECT_EQ(expected, out.str());
     EXPECT_EQ("", err.str());
 }
+
+class HelpTest : public ::testing::Test {
+public:
+    void SetUp() override {
+        lshal = std::make_unique<Lshal>(out, err, new MockServiceManager() /* serviceManager */,
+                                        new MockServiceManager() /* passthruManager */);
+    }
+
+    std::stringstream err;
+    std::stringstream out;
+    std::unique_ptr<Lshal> lshal;
+};
+
+TEST_F(HelpTest, GlobalUsage) {
+    (void)callMain(lshal, {"lshal", "--help"}); // ignore return
+    std::string errStr = err.str();
+    EXPECT_THAT(errStr, ContainsRegex("(^|\n)commands:($|\n)"))
+        << "`lshal --help` does not contain global usage";
+    EXPECT_THAT(errStr, ContainsRegex("(^|\n)list:($|\n)"))
+        << "`lshal --help` does not contain usage for 'list' command";
+    EXPECT_THAT(errStr, ContainsRegex("(^|\n)debug:($|\n)"))
+        << "`lshal --help` does not contain usage for 'debug' command";
+    EXPECT_THAT(errStr, ContainsRegex("(^|\n)help:($|\n)"))
+        << "`lshal --help` does not contain usage for 'help' command";
+
+    err.str("");
+    (void)callMain(lshal, {"lshal", "help"}); // ignore return
+    EXPECT_EQ(errStr, err.str()) << "`lshal help` should have the same output as `lshal --help`";
+
+    err.str("");
+    EXPECT_NE(0u, callMain(lshal, {"lshal", "--unknown-option"}));
+    EXPECT_THAT(err.str(), ContainsRegex("unrecognized option"));
+    EXPECT_THAT(err.str(), EndsWith(errStr))
+            << "`lshal --unknown-option` should have the same output as `lshal --help`";
+    EXPECT_EQ("", out.str());
+}
+
+TEST_F(HelpTest, UnknownOptionList1) {
+    (void)callMain(lshal, {"lshal", "help", "list"});
+    EXPECT_THAT(err.str(), ContainsRegex("(^|\n)list:($|\n)"))
+        << "`lshal help list` does not contain usage for 'list' command";
+}
+
+TEST_F(HelpTest, UnknownOptionList2) {
+    EXPECT_NE(0u, callMain(lshal, {"lshal", "list", "--unknown-option"}));
+    EXPECT_THAT(err.str(), ContainsRegex("unrecognized option"));
+    EXPECT_THAT(err.str(), ContainsRegex("(^|\n)list:($|\n)"))
+        << "`lshal list --unknown-option` does not contain usage for 'list' command";
+    EXPECT_EQ("", out.str());
+}
+
+TEST_F(HelpTest, UnknownOptionHelp1) {
+    (void)callMain(lshal, {"lshal", "help", "help"});
+    EXPECT_THAT(err.str(), ContainsRegex("(^|\n)help:($|\n)"))
+        << "`lshal help help` does not contain usage for 'help' command";
+}
+
+TEST_F(HelpTest, UnknownOptionHelp2) {
+    (void)callMain(lshal, {"lshal", "help", "--unknown-option"});
+    EXPECT_THAT(err.str(), ContainsRegex("(^|\n)help:($|\n)"))
+        << "`lshal help --unknown-option` does not contain usage for 'help' command";
+    EXPECT_EQ("", out.str());
+}
+
 } // namespace lshal
 } // namespace android
 
diff --git a/cmds/lshal/utils.h b/cmds/lshal/utils.h
index 45b922c..1f680d5 100644
--- a/cmds/lshal/utils.h
+++ b/cmds/lshal/utils.h
@@ -29,6 +29,7 @@
 
 enum : unsigned int {
     OK                                      = 0,
+    // Return to Lshal::main to print help info.
     USAGE                                   = 1 << 0,
     NO_BINDERIZED_MANAGER                   = 1 << 1,
     NO_PASSTHROUGH_MANAGER                  = 1 << 2,