From f132d82a796c91fcb741c127f37c963622b4cae4 Mon Sep 17 00:00:00 2001
From: Eelco Dolstra <edolstra@gmail.com>
Date: Tue, 5 May 2020 15:18:23 +0200
Subject: [PATCH] nix --help: Group commands

---
 src/libutil/ansicolor.hh            | 24 ++++++------
 src/libutil/args.cc                 | 57 ++++++++++++++++-------------
 src/libutil/args.hh                 | 18 +++++----
 src/nix/add-to-store.cc             |  2 +
 src/nix/cat.cc                      |  8 +++-
 src/nix/command.hh                  |  4 ++
 src/nix/copy.cc                     |  2 +
 src/nix/dev-shell.cc                |  2 +
 src/nix/doctor.cc                   |  2 +
 src/nix/dump-path.cc                |  2 +
 src/nix/edit.cc                     |  2 +
 src/nix/eval.cc                     |  2 +
 src/nix/hash.cc                     |  4 ++
 src/nix/log.cc                      |  2 +
 src/nix/ls.cc                       |  8 +++-
 src/nix/main.cc                     | 18 ++++++---
 src/nix/make-content-addressable.cc |  3 ++
 src/nix/optimise-store.cc           |  2 +
 src/nix/path-info.cc                |  2 +
 src/nix/ping-store.cc               |  2 +
 src/nix/show-config.cc              |  2 +
 src/nix/show-derivation.cc          |  2 +
 src/nix/sigs.cc                     |  4 ++
 src/nix/upgrade-nix.cc              |  2 +
 src/nix/verify.cc                   |  2 +
 src/nix/why-depends.cc              |  2 +
 26 files changed, 125 insertions(+), 55 deletions(-)

diff --git a/src/libutil/ansicolor.hh b/src/libutil/ansicolor.hh
index 390bd4d17..8ae07b092 100644
--- a/src/libutil/ansicolor.hh
+++ b/src/libutil/ansicolor.hh
@@ -1,13 +1,15 @@
-#pragma once 
+#pragma once
+
+namespace nix {
+
+/* Some ANSI escape sequences. */
+#define ANSI_NORMAL "\e[0m"
+#define ANSI_BOLD "\e[1m"
+#define ANSI_FAINT "\e[2m"
+#define ANSI_ITALIC "\e[3m"
+#define ANSI_RED "\e[31;1m"
+#define ANSI_GREEN "\e[32;1m"
+#define ANSI_YELLOW "\e[33;1m"
+#define ANSI_BLUE "\e[34;1m"
 
-namespace nix 
-{
-  /* Some ANSI escape sequences. */
-  #define ANSI_NORMAL "\e[0m"
-  #define ANSI_BOLD "\e[1m"
-  #define ANSI_FAINT "\e[2m"
-  #define ANSI_RED "\e[31;1m"
-  #define ANSI_GREEN "\e[32;1m"
-  #define ANSI_YELLOW "\e[33;1m"
-  #define ANSI_BLUE "\e[34;1m"
 }
diff --git a/src/libutil/args.cc b/src/libutil/args.cc
index 91fc2f581..f829415d1 100644
--- a/src/libutil/args.cc
+++ b/src/libutil/args.cc
@@ -59,7 +59,7 @@ void Args::parseCmdline(const Strings & _cmdline)
 
 void Args::printHelp(const string & programName, std::ostream & out)
 {
-    std::cout << "Usage: " << programName << " <FLAGS>...";
+    std::cout << fmt(ANSI_BOLD "Usage:" ANSI_NORMAL " %s " ANSI_ITALIC "FLAGS..." ANSI_NORMAL, programName);
     for (auto & exp : expectedArgs) {
         std::cout << renderLabels({exp.label});
         // FIXME: handle arity > 1
@@ -70,11 +70,11 @@ void Args::printHelp(const string & programName, std::ostream & out)
 
     auto s = description();
     if (s != "")
-        std::cout << "\nSummary: " << s << ".\n";
+        std::cout << "\n" ANSI_BOLD "Summary:" ANSI_NORMAL " " << s << ".\n";
 
     if (longFlags.size()) {
         std::cout << "\n";
-        std::cout << "Flags:\n";
+        std::cout << ANSI_BOLD "Flags:" ANSI_NORMAL "\n";
         printFlags(out);
     }
 }
@@ -181,7 +181,7 @@ std::string renderLabels(const Strings & labels)
     std::string res;
     for (auto label : labels) {
         for (auto & c : label) c = std::toupper(c);
-        res += " <" + label + ">";
+        res += " " ANSI_ITALIC + label + ANSI_NORMAL;
     }
     return res;
 }
@@ -190,10 +190,10 @@ void printTable(std::ostream & out, const Table2 & table)
 {
     size_t max = 0;
     for (auto & row : table)
-        max = std::max(max, row.first.size());
+        max = std::max(max, filterANSIEscapes(row.first, true).size());
     for (auto & row : table) {
         out << "  " << row.first
-            << std::string(max - row.first.size() + 2, ' ')
+            << std::string(max - filterANSIEscapes(row.first, true).size() + 2, ' ')
             << row.second << "\n";
     }
 }
@@ -204,8 +204,7 @@ void Command::printHelp(const string & programName, std::ostream & out)
 
     auto exs = examples();
     if (!exs.empty()) {
-        out << "\n";
-        out << "Examples:\n";
+        out << "\n" ANSI_BOLD "Examples:" ANSI_NORMAL "\n";
         for (auto & ex : exs)
             out << "\n"
                 << "  " << ex.description << "\n" // FIXME: wrap
@@ -221,49 +220,55 @@ MultiCommand::MultiCommand(const Commands & commands)
         auto i = commands.find(ss[0]);
         if (i == commands.end())
             throw UsageError("'%s' is not a recognised command", ss[0]);
-        command = i->second();
-        command->_name = ss[0];
+        command = {ss[0], i->second()};
     }});
+
+    categories[Command::catDefault] = "Available commands";
 }
 
 void MultiCommand::printHelp(const string & programName, std::ostream & out)
 {
     if (command) {
-        command->printHelp(programName + " " + command->name(), out);
+        command->second->printHelp(programName + " " + command->first, out);
         return;
     }
 
-    out << "Usage: " << programName << " <COMMAND> <FLAGS>... <ARGS>...\n";
+    out << fmt(ANSI_BOLD "Usage:" ANSI_NORMAL " %s " ANSI_ITALIC "COMMAND FLAGS... ARGS..." ANSI_NORMAL "\n", programName);
 
-    out << "\n";
-    out << "Common flags:\n";
+    out << "\n" ANSI_BOLD "Common flags:" ANSI_NORMAL "\n";
     printFlags(out);
 
-    out << "\n";
-    out << "Available commands:\n";
+    std::map<Command::Category, std::map<std::string, ref<Command>>> commandsByCategory;
 
-    Table2 table;
-    for (auto & i : commands) {
-        auto command = i.second();
-        command->_name = i.first;
-        auto descr = command->description();
-        if (!descr.empty())
-            table.push_back(std::make_pair(command->name(), descr));
+    for (auto & [name, commandFun] : commands) {
+        auto command = commandFun();
+        commandsByCategory[command->category()].insert_or_assign(name, command);
+    }
+
+    for (auto & [category, commands] : commandsByCategory) {
+        out << fmt("\n" ANSI_BOLD "%s:" ANSI_NORMAL "\n", categories[category]);
+
+        Table2 table;
+        for (auto & [name, command] : commands) {
+            auto descr = command->description();
+            if (!descr.empty())
+                table.push_back(std::make_pair(name, descr));
+        }
+        printTable(out, table);
     }
-    printTable(out, table);
 }
 
 bool MultiCommand::processFlag(Strings::iterator & pos, Strings::iterator end)
 {
     if (Args::processFlag(pos, end)) return true;
-    if (command && command->processFlag(pos, end)) return true;
+    if (command && command->second->processFlag(pos, end)) return true;
     return false;
 }
 
 bool MultiCommand::processArgs(const Strings & args, bool finish)
 {
     if (command)
-        return command->processArgs(args, finish);
+        return command->second->processArgs(args, finish);
     else
         return Args::processArgs(args, finish);
 }
diff --git a/src/libutil/args.hh b/src/libutil/args.hh
index 9b5e316a5..1932e6a8a 100644
--- a/src/libutil/args.hh
+++ b/src/libutil/args.hh
@@ -197,17 +197,10 @@ public:
    run() method. */
 struct Command : virtual Args
 {
-private:
-    std::string _name;
-
     friend class MultiCommand;
 
-public:
-
     virtual ~Command() { }
 
-    std::string name() { return _name; }
-
     virtual void prepare() { };
     virtual void run() = 0;
 
@@ -221,6 +214,12 @@ public:
 
     virtual Examples examples() { return Examples(); }
 
+    typedef int Category;
+
+    static constexpr Category catDefault = 0;
+
+    virtual Category category() { return catDefault; }
+
     void printHelp(const string & programName, std::ostream & out) override;
 };
 
@@ -233,7 +232,10 @@ class MultiCommand : virtual Args
 public:
     Commands commands;
 
-    std::shared_ptr<Command> command;
+    std::map<Command::Category, std::string> categories;
+
+    // Selected command, if any.
+    std::optional<std::pair<std::string, ref<Command>>> command;
 
     MultiCommand(const Commands & commands);
 
diff --git a/src/nix/add-to-store.cc b/src/nix/add-to-store.cc
index 00da01f7e..1d298903b 100644
--- a/src/nix/add-to-store.cc
+++ b/src/nix/add-to-store.cc
@@ -34,6 +34,8 @@ struct CmdAddToStore : MixDryRun, StoreCommand
         };
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store) override
     {
         if (!namePart) namePart = baseNameOf(path);
diff --git a/src/nix/cat.cc b/src/nix/cat.cc
index 851f90abd..fd91f2036 100644
--- a/src/nix/cat.cc
+++ b/src/nix/cat.cc
@@ -30,9 +30,11 @@ struct CmdCatStore : StoreCommand, MixCat
 
     std::string description() override
     {
-        return "print the contents of a store file on stdout";
+        return "print the contents of a file in the Nix store on stdout";
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store) override
     {
         cat(store->getFSAccessor());
@@ -51,9 +53,11 @@ struct CmdCatNar : StoreCommand, MixCat
 
     std::string description() override
     {
-        return "print the contents of a file inside a NAR file";
+        return "print the contents of a file inside a NAR file on stdout";
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store) override
     {
         cat(makeNarAccessor(make_ref<std::string>(readFile(narPath))));
diff --git a/src/nix/command.hh b/src/nix/command.hh
index bf43d950f..959d5f19d 100644
--- a/src/nix/command.hh
+++ b/src/nix/command.hh
@@ -10,6 +10,10 @@ namespace nix {
 
 extern std::string programPath;
 
+static constexpr Command::Category catSecondary = 100;
+static constexpr Command::Category catUtility = 101;
+static constexpr Command::Category catNixInstallation = 102;
+
 /* A command that requires a Nix store. */
 struct StoreCommand : virtual Command
 {
diff --git a/src/nix/copy.cc b/src/nix/copy.cc
index 77673a1c2..c7c38709d 100644
--- a/src/nix/copy.cc
+++ b/src/nix/copy.cc
@@ -80,6 +80,8 @@ struct CmdCopy : StorePathsCommand
         };
     }
 
+    Category category() override { return catSecondary; }
+
     ref<Store> createStore() override
     {
         return srcUri.empty() ? StoreCommand::createStore() : openStore(srcUri);
diff --git a/src/nix/dev-shell.cc b/src/nix/dev-shell.cc
index 2bdf59839..d300f6a23 100644
--- a/src/nix/dev-shell.cc
+++ b/src/nix/dev-shell.cc
@@ -328,6 +328,8 @@ struct CmdPrintDevEnv : Common
         };
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store) override
     {
         auto buildEnvironment = getBuildEnvironment(store).first;
diff --git a/src/nix/doctor.cc b/src/nix/doctor.cc
index 0aa634d6e..cc36354f1 100644
--- a/src/nix/doctor.cc
+++ b/src/nix/doctor.cc
@@ -43,6 +43,8 @@ struct CmdDoctor : StoreCommand
         return "check your system for potential problems and print a PASS or FAIL for each check.";
     }
 
+    Category category() override { return catNixInstallation; }
+
     void run(ref<Store> store) override
     {
         logger->log("Running checks against store uri: " + store->getUri());
diff --git a/src/nix/dump-path.cc b/src/nix/dump-path.cc
index bb741b572..e1de71bf8 100644
--- a/src/nix/dump-path.cc
+++ b/src/nix/dump-path.cc
@@ -20,6 +20,8 @@ struct CmdDumpPath : StorePathCommand
         };
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store, const StorePath & storePath) override
     {
         FdSink sink(STDOUT_FILENO);
diff --git a/src/nix/edit.cc b/src/nix/edit.cc
index 1683eada0..067d3a973 100644
--- a/src/nix/edit.cc
+++ b/src/nix/edit.cc
@@ -25,6 +25,8 @@ struct CmdEdit : InstallableCommand
         };
     }
 
+    Category category() override { return catSecondary; }
+
     void run(ref<Store> store) override
     {
         auto state = getEvalState();
diff --git a/src/nix/eval.cc b/src/nix/eval.cc
index 86a1e8b68..26e98ac2a 100644
--- a/src/nix/eval.cc
+++ b/src/nix/eval.cc
@@ -45,6 +45,8 @@ struct CmdEval : MixJSON, InstallableCommand
         };
     }
 
+    Category category() override { return catSecondary; }
+
     void run(ref<Store> store) override
     {
         if (raw && json)
diff --git a/src/nix/hash.cc b/src/nix/hash.cc
index 9b9509d3a..366314227 100644
--- a/src/nix/hash.cc
+++ b/src/nix/hash.cc
@@ -41,6 +41,8 @@ struct CmdHash : Command
             : "print cryptographic hash of the NAR serialisation of a path";
     }
 
+    Category category() override { return catUtility; }
+
     void run() override
     {
         for (auto path : paths) {
@@ -87,6 +89,8 @@ struct CmdToBase : Command
             "SRI");
     }
 
+    Category category() override { return catUtility; }
+
     void run() override
     {
         for (auto s : args)
diff --git a/src/nix/log.cc b/src/nix/log.cc
index 795991cb7..3fe22f6c2 100644
--- a/src/nix/log.cc
+++ b/src/nix/log.cc
@@ -31,6 +31,8 @@ struct CmdLog : InstallableCommand
         };
     }
 
+    Category category() override { return catSecondary; }
+
     void run(ref<Store> store) override
     {
         settings.readOnlyMode = true;
diff --git a/src/nix/ls.cc b/src/nix/ls.cc
index 8590199d7..6ae4df27e 100644
--- a/src/nix/ls.cc
+++ b/src/nix/ls.cc
@@ -100,9 +100,11 @@ struct CmdLsStore : StoreCommand, MixLs
 
     std::string description() override
     {
-        return "show information about a store path";
+        return "show information about a path in the Nix store";
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store) override
     {
         list(store->getFSAccessor());
@@ -131,9 +133,11 @@ struct CmdLsNar : Command, MixLs
 
     std::string description() override
     {
-        return "show information about the contents of a NAR file";
+        return "show information about a path inside a NAR file";
     }
 
+    Category category() override { return catUtility; }
+
     void run() override
     {
         list(makeNarAccessor(make_ref<std::string>(readFile(narPath, true))));
diff --git a/src/nix/main.cc b/src/nix/main.cc
index 57b8bed9f..5cf09c4f0 100644
--- a/src/nix/main.cc
+++ b/src/nix/main.cc
@@ -59,6 +59,12 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs
 
     NixArgs() : MultiCommand(*RegisterCommand::commands), MixCommonArgs("nix")
     {
+        categories.clear();
+        categories[Command::catDefault] = "Main commands";
+        categories[catSecondary] = "Infrequently used commands";
+        categories[catUtility] = "Utility/scripting commands";
+        categories[catNixInstallation] = "Commands for upgrading or troubleshooting your Nix installation";
+
         addFlag({
             .longName = "help",
             .description = "show usage information",
@@ -111,8 +117,8 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs
         Args::printFlags(out);
         std::cout <<
             "\n"
-            "In addition, most configuration settings can be overriden using '--<name> <value>'.\n"
-            "Boolean settings can be overriden using '--<name>' or '--no-<name>'. See 'nix\n"
+            "In addition, most configuration settings can be overriden using '--" ANSI_ITALIC "name value" ANSI_NORMAL "'.\n"
+            "Boolean settings can be overriden using '--" ANSI_ITALIC "name" ANSI_NORMAL "' or '--no-" ANSI_ITALIC "name" ANSI_NORMAL "'. See 'nix\n"
             "--help-config' for a list of configuration settings.\n";
     }
 
@@ -121,10 +127,10 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs
         MultiCommand::printHelp(programName, out);
 
 #if 0
-        out << "\nFor full documentation, run 'man " << programName << "' or 'man " << programName << "-<COMMAND>'.\n";
+        out << "\nFor full documentation, run 'man " << programName << "' or 'man " << programName << "-" ANSI_ITALIC "COMMAND" ANSI_NORMAL "'.\n";
 #endif
 
-        std::cout << "\nNote: this program is EXPERIMENTAL and subject to change.\n";
+        std::cout << "\nNote: this program is " ANSI_RED "EXPERIMENTAL" ANSI_NORMAL " and subject to change.\n";
     }
 
     void showHelpAndExit()
@@ -191,8 +197,8 @@ void mainWrapped(int argc, char * * argv)
     if (args.refresh)
         settings.tarballTtl = 0;
 
-    args.command->prepare();
-    args.command->run();
+    args.command->second->prepare();
+    args.command->second->run();
 }
 
 }
diff --git a/src/nix/make-content-addressable.cc b/src/nix/make-content-addressable.cc
index f9c7fef3f..8803461f4 100644
--- a/src/nix/make-content-addressable.cc
+++ b/src/nix/make-content-addressable.cc
@@ -31,6 +31,9 @@ struct CmdMakeContentAddressable : StorePathsCommand, MixJSON
             },
         };
     }
+
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store, StorePaths storePaths) override
     {
         auto paths = store->topoSortPaths(storePathsToSet(storePaths));
diff --git a/src/nix/optimise-store.cc b/src/nix/optimise-store.cc
index fed012b04..b45951879 100644
--- a/src/nix/optimise-store.cc
+++ b/src/nix/optimise-store.cc
@@ -23,6 +23,8 @@ struct CmdOptimiseStore : StoreCommand
         };
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store) override
     {
         store->optimiseStore();
diff --git a/src/nix/path-info.cc b/src/nix/path-info.cc
index 45ec297d2..88d7fffd4 100644
--- a/src/nix/path-info.cc
+++ b/src/nix/path-info.cc
@@ -29,6 +29,8 @@ struct CmdPathInfo : StorePathsCommand, MixJSON
         return "query information about store paths";
     }
 
+    Category category() override { return catSecondary; }
+
     Examples examples() override
     {
         return {
diff --git a/src/nix/ping-store.cc b/src/nix/ping-store.cc
index 3a2e542a3..127397a29 100644
--- a/src/nix/ping-store.cc
+++ b/src/nix/ping-store.cc
@@ -21,6 +21,8 @@ struct CmdPingStore : StoreCommand
         };
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store) override
     {
         store->connect();
diff --git a/src/nix/show-config.cc b/src/nix/show-config.cc
index 6104b10bc..4fd8886de 100644
--- a/src/nix/show-config.cc
+++ b/src/nix/show-config.cc
@@ -13,6 +13,8 @@ struct CmdShowConfig : Command, MixJSON
         return "show the Nix configuration";
     }
 
+    Category category() override { return catUtility; }
+
     void run() override
     {
         if (json) {
diff --git a/src/nix/show-derivation.cc b/src/nix/show-derivation.cc
index b6f24599f..22c569f3c 100644
--- a/src/nix/show-derivation.cc
+++ b/src/nix/show-derivation.cc
@@ -42,6 +42,8 @@ struct CmdShowDerivation : InstallablesCommand
         };
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store) override
     {
         auto drvPaths = toDerivations(store, installables, true);
diff --git a/src/nix/sigs.cc b/src/nix/sigs.cc
index a91465c2a..6c9b9a792 100644
--- a/src/nix/sigs.cc
+++ b/src/nix/sigs.cc
@@ -27,6 +27,8 @@ struct CmdCopySigs : StorePathsCommand
         return "copy path signatures from substituters (like binary caches)";
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store, StorePaths storePaths) override
     {
         if (substituterUris.empty())
@@ -112,6 +114,8 @@ struct CmdSignPaths : StorePathsCommand
         return "sign the specified paths";
     }
 
+    Category category() override { return catUtility; }
+
     void run(ref<Store> store, StorePaths storePaths) override
     {
         if (secretKeyFile.empty())
diff --git a/src/nix/upgrade-nix.cc b/src/nix/upgrade-nix.cc
index 32efcc3a7..678780f33 100644
--- a/src/nix/upgrade-nix.cc
+++ b/src/nix/upgrade-nix.cc
@@ -51,6 +51,8 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand
         };
     }
 
+    Category category() override { return catNixInstallation; }
+
     void run(ref<Store> store) override
     {
         evalSettings.pureEval = true;
diff --git a/src/nix/verify.cc b/src/nix/verify.cc
index 08a36ac50..cf1fa6a99 100644
--- a/src/nix/verify.cc
+++ b/src/nix/verify.cc
@@ -49,6 +49,8 @@ struct CmdVerify : StorePathsCommand
         };
     }
 
+    Category category() override { return catSecondary; }
+
     void run(ref<Store> store, StorePaths storePaths) override
     {
         std::vector<ref<Store>> substituters;
diff --git a/src/nix/why-depends.cc b/src/nix/why-depends.cc
index 36a3ee863..6057beedb 100644
--- a/src/nix/why-depends.cc
+++ b/src/nix/why-depends.cc
@@ -68,6 +68,8 @@ struct CmdWhyDepends : SourceExprCommand
         };
     }
 
+    Category category() override { return catSecondary; }
+
     void run(ref<Store> store) override
     {
         auto package = parseInstallable(*this, store, _package, false);
-- 
GitLab