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

@ -483,7 +483,7 @@ bool Args::processArgs(const Strings & args, bool finish)
if (!anyCompleted) if (!anyCompleted)
exp.handler.fun(ss); exp.handler.fun(ss);
/* Move the list element to the processedArgs. This is almost the same as /* Move the list element to the processedArgs. This is almost the same as
`processedArgs.push_back(expectedArgs.front()); expectedArgs.pop_front()`, `processedArgs.push_back(expectedArgs.front()); expectedArgs.pop_front()`,
except that it will only adjust the next and prev pointers of the list except that it will only adjust the next and prev pointers of the list
elements, meaning the actual contents don't move in memory. This is elements, meaning the actual contents don't move in memory. This is
@ -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

@ -223,11 +223,11 @@ protected:
std::list<ExpectedArg> expectedArgs; std::list<ExpectedArg> expectedArgs;
/** /**
* List of processed positional argument forms. * List of processed positional argument forms.
* *
* All items removed from `expectedArgs` are added here. After all * All items removed from `expectedArgs` are added here. After all
* arguments were processed, this list should be exactly the same as * arguments were processed, this list should be exactly the same as
* `expectedArgs` was before. * `expectedArgs` was before.
* *
* This list is used to extend the lifetime of the argument forms. * This list is used to extend the lifetime of the argument forms.
* If this is not done, some closures that reference the command * If this is not done, some closures that reference the command
* itself will segfault. * itself will segfault.
@ -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");