Improve the error message for “multicommands” commands (#9510)

* Factor out the default `MultiCommand` behavior

All the `MultiCommand`s had (nearly) the same behavior when called
without a subcommand.
Factor out this behavior into the `NixMultiCommand` class.

* Display the list of available subcommands when none is specified

Whenever a user runs a command that excepts a subcommand, add the list
of available subcommands to the error message.

* Print the multi-command lists as Markdown lists

This takes more screen real estate, but is also much more readable than
a comma-separated list
This commit is contained in:
Théophane Hufschmitt 2023-12-06 14:13:45 +01:00 committed by GitHub
parent fbc855b3c3
commit 7fff625e39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 55 additions and 89 deletions

View file

@ -1,4 +1,5 @@
#include "command.hh" #include "command.hh"
#include "markdown.hh"
#include "store-api.hh" #include "store-api.hh"
#include "local-fs-store.hh" #include "local-fs-store.hh"
#include "derivations.hh" #include "derivations.hh"
@ -34,6 +35,19 @@ nlohmann::json NixMultiCommand::toJSON()
return MultiCommand::toJSON(); return MultiCommand::toJSON();
} }
void NixMultiCommand::run()
{
if (!command) {
std::set<std::string> subCommandTextLines;
for (auto & [name, _] : commands)
subCommandTextLines.insert(fmt("- `%s`", name));
std::string markdownError = fmt("`nix %s` requires a sub-command. Available sub-commands:\n\n%s\n",
commandName, concatStringsSep("\n", subCommandTextLines));
throw UsageError(renderMarkdownToTerminal(markdownError));
}
command->second->run();
}
StoreCommand::StoreCommand() StoreCommand::StoreCommand()
{ {
} }

View file

@ -26,9 +26,13 @@ static constexpr Command::Category catNixInstallation = 102;
static constexpr auto installablesCategory = "Options that change the interpretation of [installables](@docroot@/command-ref/new-cli/nix.md#installables)"; static constexpr auto installablesCategory = "Options that change the interpretation of [installables](@docroot@/command-ref/new-cli/nix.md#installables)";
struct NixMultiCommand : virtual MultiCommand, virtual Command struct NixMultiCommand : MultiCommand, virtual Command
{ {
nlohmann::json toJSON() override; nlohmann::json toJSON() override;
using MultiCommand::MultiCommand;
virtual void run() override;
}; };
// For the overloaded run methods // For the overloaded run methods

View file

@ -622,8 +622,9 @@ std::optional<ExperimentalFeature> Command::experimentalFeature ()
return { Xp::NixCommand }; return { Xp::NixCommand };
} }
MultiCommand::MultiCommand(const Commands & commands_) MultiCommand::MultiCommand(std::string_view commandName, const Commands & commands_)
: commands(commands_) : commands(commands_)
, commandName(commandName)
{ {
expectArgs({ expectArgs({
.label = "subcommand", .label = "subcommand",

View file

@ -356,13 +356,16 @@ public:
*/ */
std::optional<std::pair<std::string, ref<Command>>> command; std::optional<std::pair<std::string, ref<Command>>> command;
MultiCommand(const Commands & commands); MultiCommand(std::string_view commandName, const Commands & commands);
bool processFlag(Strings::iterator & pos, Strings::iterator end) override; bool processFlag(Strings::iterator & pos, Strings::iterator end) override;
bool processArgs(const Strings & args, bool finish) override; bool processArgs(const Strings & args, bool finish) override;
nlohmann::json toJSON() override; nlohmann::json toJSON() override;
protected:
std::string commandName = "";
}; };
Strings argvToStrings(int argc, char * * argv); Strings argvToStrings(int argc, char * * argv);

View file

@ -7,9 +7,9 @@
using namespace nix; using namespace nix;
struct CmdConfig : virtual NixMultiCommand struct CmdConfig : NixMultiCommand
{ {
CmdConfig() : MultiCommand(RegisterCommand::getCommandsFor({"config"})) CmdConfig() : NixMultiCommand("config", RegisterCommand::getCommandsFor({"config"}))
{ } { }
std::string description() override std::string description() override
@ -18,13 +18,6 @@ struct CmdConfig : virtual NixMultiCommand
} }
Category category() override { return catUtility; } Category category() override { return catUtility; }
void run() override
{
if (!command)
throw UsageError("'nix config' requires a sub-command.");
command->second->run();
}
}; };
struct CmdConfigShow : Command, MixJSON struct CmdConfigShow : Command, MixJSON

View file

@ -2,9 +2,9 @@
using namespace nix; using namespace nix;
struct CmdDerivation : virtual NixMultiCommand struct CmdDerivation : NixMultiCommand
{ {
CmdDerivation() : MultiCommand(RegisterCommand::getCommandsFor({"derivation"})) CmdDerivation() : NixMultiCommand("derivation", RegisterCommand::getCommandsFor({"derivation"}))
{ } { }
std::string description() override std::string description() override
@ -13,13 +13,6 @@ struct CmdDerivation : virtual NixMultiCommand
} }
Category category() override { return catUtility; } Category category() override { return catUtility; }
void run() override
{
if (!command)
throw UsageError("'nix derivation' requires a sub-command.");
command->second->run();
}
}; };
static auto rCmdDerivation = registerCommand<CmdDerivation>("derivation"); static auto rCmdDerivation = registerCommand<CmdDerivation>("derivation");

View file

@ -1399,7 +1399,9 @@ struct CmdFlakePrefetch : FlakeCommand, MixJSON
struct CmdFlake : NixMultiCommand struct CmdFlake : NixMultiCommand
{ {
CmdFlake() CmdFlake()
: MultiCommand({ : NixMultiCommand(
"flake",
{
{"update", []() { return make_ref<CmdFlakeUpdate>(); }}, {"update", []() { return make_ref<CmdFlakeUpdate>(); }},
{"lock", []() { return make_ref<CmdFlakeLock>(); }}, {"lock", []() { return make_ref<CmdFlakeLock>(); }},
{"metadata", []() { return make_ref<CmdFlakeMetadata>(); }}, {"metadata", []() { return make_ref<CmdFlakeMetadata>(); }},
@ -1429,10 +1431,8 @@ struct CmdFlake : NixMultiCommand
void run() override void run() override
{ {
if (!command)
throw UsageError("'nix flake' requires a sub-command.");
experimentalFeatureSettings.require(Xp::Flakes); experimentalFeatureSettings.require(Xp::Flakes);
command->second->run(); NixMultiCommand::run();
} }
}; };

View file

@ -130,7 +130,9 @@ struct CmdToBase : Command
struct CmdHash : NixMultiCommand struct CmdHash : NixMultiCommand
{ {
CmdHash() CmdHash()
: MultiCommand({ : NixMultiCommand(
"hash",
{
{"file", []() { return make_ref<CmdHashBase>(FileIngestionMethod::Flat);; }}, {"file", []() { return make_ref<CmdHashBase>(FileIngestionMethod::Flat);; }},
{"path", []() { return make_ref<CmdHashBase>(FileIngestionMethod::Recursive); }}, {"path", []() { return make_ref<CmdHashBase>(FileIngestionMethod::Recursive); }},
{"to-base16", []() { return make_ref<CmdToBase>(HashFormat::Base16); }}, {"to-base16", []() { return make_ref<CmdToBase>(HashFormat::Base16); }},
@ -146,13 +148,6 @@ struct CmdHash : NixMultiCommand
} }
Category category() override { return catUtility; } Category category() override { return catUtility; }
void run() override
{
if (!command)
throw UsageError("'nix hash' requires a sub-command.");
command->second->run();
}
}; };
static auto rCmdHash = registerCommand<CmdHash>("hash"); static auto rCmdHash = registerCommand<CmdHash>("hash");

View file

@ -67,7 +67,7 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs, virtual RootArgs
bool helpRequested = false; bool helpRequested = false;
bool showVersion = false; bool showVersion = false;
NixArgs() : MultiCommand(RegisterCommand::getCommandsFor({})), MixCommonArgs("nix") NixArgs() : MultiCommand("", RegisterCommand::getCommandsFor({})), MixCommonArgs("nix")
{ {
categories.clear(); categories.clear();
categories[catHelp] = "Help commands"; categories[catHelp] = "Help commands";

View file

@ -4,7 +4,7 @@ using namespace nix;
struct CmdNar : NixMultiCommand struct CmdNar : NixMultiCommand
{ {
CmdNar() : MultiCommand(RegisterCommand::getCommandsFor({"nar"})) CmdNar() : NixMultiCommand("nar", RegisterCommand::getCommandsFor({"nar"}))
{ } { }
std::string description() override std::string description() override
@ -20,13 +20,6 @@ struct CmdNar : NixMultiCommand
} }
Category category() override { return catUtility; } Category category() override { return catUtility; }
void run() override
{
if (!command)
throw UsageError("'nix nar' requires a sub-command.");
command->second->run();
}
}; };
static auto rCmdNar = registerCommand<CmdNar>("nar"); static auto rCmdNar = registerCommand<CmdNar>("nar");

View file

@ -825,7 +825,9 @@ struct CmdProfileWipeHistory : virtual StoreCommand, MixDefaultProfile, MixDryRu
struct CmdProfile : NixMultiCommand struct CmdProfile : NixMultiCommand
{ {
CmdProfile() CmdProfile()
: MultiCommand({ : NixMultiCommand(
"profile",
{
{"install", []() { return make_ref<CmdProfileInstall>(); }}, {"install", []() { return make_ref<CmdProfileInstall>(); }},
{"remove", []() { return make_ref<CmdProfileRemove>(); }}, {"remove", []() { return make_ref<CmdProfileRemove>(); }},
{"upgrade", []() { return make_ref<CmdProfileUpgrade>(); }}, {"upgrade", []() { return make_ref<CmdProfileUpgrade>(); }},
@ -848,13 +850,6 @@ struct CmdProfile : NixMultiCommand
#include "profile.md" #include "profile.md"
; ;
} }
void run() override
{
if (!command)
throw UsageError("'nix profile' requires a sub-command.");
command->second->run();
}
}; };
static auto rCmdProfile = registerCommand<CmdProfile>("profile"); static auto rCmdProfile = registerCommand<CmdProfile>("profile");

View file

@ -5,9 +5,9 @@
using namespace nix; using namespace nix;
struct CmdRealisation : virtual NixMultiCommand struct CmdRealisation : NixMultiCommand
{ {
CmdRealisation() : MultiCommand(RegisterCommand::getCommandsFor({"realisation"})) CmdRealisation() : NixMultiCommand("realisation", RegisterCommand::getCommandsFor({"realisation"}))
{ } { }
std::string description() override std::string description() override
@ -16,13 +16,6 @@ struct CmdRealisation : virtual NixMultiCommand
} }
Category category() override { return catUtility; } Category category() override { return catUtility; }
void run() override
{
if (!command)
throw UsageError("'nix realisation' requires a sub-command.");
command->second->run();
}
}; };
static auto rCmdRealisation = registerCommand<CmdRealisation>("realisation"); static auto rCmdRealisation = registerCommand<CmdRealisation>("realisation");

View file

@ -196,10 +196,12 @@ struct CmdRegistryPin : RegistryCommand, EvalCommand
} }
}; };
struct CmdRegistry : virtual NixMultiCommand struct CmdRegistry : NixMultiCommand
{ {
CmdRegistry() CmdRegistry()
: MultiCommand({ : NixMultiCommand(
"registry",
{
{"list", []() { return make_ref<CmdRegistryList>(); }}, {"list", []() { return make_ref<CmdRegistryList>(); }},
{"add", []() { return make_ref<CmdRegistryAdd>(); }}, {"add", []() { return make_ref<CmdRegistryAdd>(); }},
{"remove", []() { return make_ref<CmdRegistryRemove>(); }}, {"remove", []() { return make_ref<CmdRegistryRemove>(); }},
@ -221,14 +223,6 @@ struct CmdRegistry : virtual NixMultiCommand
} }
Category category() override { return catSecondary; } Category category() override { return catSecondary; }
void run() override
{
experimentalFeatureSettings.require(Xp::Flakes);
if (!command)
throw UsageError("'nix registry' requires a sub-command.");
command->second->run();
}
}; };
static auto rCmdRegistry = registerCommand<CmdRegistry>("registry"); static auto rCmdRegistry = registerCommand<CmdRegistry>("registry");

View file

@ -205,7 +205,9 @@ struct CmdKeyConvertSecretToPublic : Command
struct CmdKey : NixMultiCommand struct CmdKey : NixMultiCommand
{ {
CmdKey() CmdKey()
: MultiCommand({ : NixMultiCommand(
"key",
{
{"generate-secret", []() { return make_ref<CmdKeyGenerateSecret>(); }}, {"generate-secret", []() { return make_ref<CmdKeyGenerateSecret>(); }},
{"convert-secret-to-public", []() { return make_ref<CmdKeyConvertSecretToPublic>(); }}, {"convert-secret-to-public", []() { return make_ref<CmdKeyConvertSecretToPublic>(); }},
}) })
@ -218,13 +220,6 @@ struct CmdKey : NixMultiCommand
} }
Category category() override { return catUtility; } Category category() override { return catUtility; }
void run() override
{
if (!command)
throw UsageError("'nix key' requires a sub-command.");
command->second->run();
}
}; };
static auto rCmdKey = registerCommand<CmdKey>("key"); static auto rCmdKey = registerCommand<CmdKey>("key");

View file

@ -2,9 +2,9 @@
using namespace nix; using namespace nix;
struct CmdStore : virtual NixMultiCommand struct CmdStore : NixMultiCommand
{ {
CmdStore() : MultiCommand(RegisterCommand::getCommandsFor({"store"})) CmdStore() : NixMultiCommand("store", RegisterCommand::getCommandsFor({"store"}))
{ } { }
std::string description() override std::string description() override
@ -13,13 +13,6 @@ struct CmdStore : virtual NixMultiCommand
} }
Category category() override { return catUtility; } Category category() override { return catUtility; }
void run() override
{
if (!command)
throw UsageError("'nix store' requires a sub-command.");
command->second->run();
}
}; };
static auto rCmdStore = registerCommand<CmdStore>("store"); static auto rCmdStore = registerCommand<CmdStore>("store");