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 "markdown.hh"
#include "store-api.hh"
#include "local-fs-store.hh"
#include "derivations.hh"
@ -34,6 +35,19 @@ nlohmann::json NixMultiCommand::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()
{
}

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)";
struct NixMultiCommand : virtual MultiCommand, virtual Command
struct NixMultiCommand : MultiCommand, virtual Command
{
nlohmann::json toJSON() override;
using MultiCommand::MultiCommand;
virtual void run() override;
};
// For the overloaded run methods

View file

@ -483,7 +483,7 @@ bool Args::processArgs(const Strings & args, bool finish)
if (!anyCompleted)
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()`,
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
@ -622,8 +622,9 @@ std::optional<ExperimentalFeature> Command::experimentalFeature ()
return { Xp::NixCommand };
}
MultiCommand::MultiCommand(const Commands & commands_)
MultiCommand::MultiCommand(std::string_view commandName, const Commands & commands_)
: commands(commands_)
, commandName(commandName)
{
expectArgs({
.label = "subcommand",

View file

@ -223,11 +223,11 @@ protected:
std::list<ExpectedArg> expectedArgs;
/**
* List of processed positional argument forms.
*
*
* All items removed from `expectedArgs` are added here. After all
* arguments were processed, this list should be exactly the same as
* `expectedArgs` was before.
*
*
* This list is used to extend the lifetime of the argument forms.
* If this is not done, some closures that reference the command
* itself will segfault.
@ -356,13 +356,16 @@ public:
*/
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 processArgs(const Strings & args, bool finish) override;
nlohmann::json toJSON() override;
protected:
std::string commandName = "";
};
Strings argvToStrings(int argc, char * * argv);

View file

@ -7,9 +7,9 @@
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
@ -18,13 +18,6 @@ struct CmdConfig : virtual NixMultiCommand
}
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

View file

@ -2,9 +2,9 @@
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
@ -13,13 +13,6 @@ struct CmdDerivation : virtual NixMultiCommand
}
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");

View file

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

View file

@ -130,7 +130,9 @@ struct CmdToBase : Command
struct CmdHash : NixMultiCommand
{
CmdHash()
: MultiCommand({
: NixMultiCommand(
"hash",
{
{"file", []() { return make_ref<CmdHashBase>(FileIngestionMethod::Flat);; }},
{"path", []() { return make_ref<CmdHashBase>(FileIngestionMethod::Recursive); }},
{"to-base16", []() { return make_ref<CmdToBase>(HashFormat::Base16); }},
@ -146,13 +148,6 @@ struct CmdHash : NixMultiCommand
}
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");

View file

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

View file

@ -4,7 +4,7 @@ using namespace nix;
struct CmdNar : NixMultiCommand
{
CmdNar() : MultiCommand(RegisterCommand::getCommandsFor({"nar"}))
CmdNar() : NixMultiCommand("nar", RegisterCommand::getCommandsFor({"nar"}))
{ }
std::string description() override
@ -20,13 +20,6 @@ struct CmdNar : NixMultiCommand
}
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");

View file

@ -825,7 +825,9 @@ struct CmdProfileWipeHistory : virtual StoreCommand, MixDefaultProfile, MixDryRu
struct CmdProfile : NixMultiCommand
{
CmdProfile()
: MultiCommand({
: NixMultiCommand(
"profile",
{
{"install", []() { return make_ref<CmdProfileInstall>(); }},
{"remove", []() { return make_ref<CmdProfileRemove>(); }},
{"upgrade", []() { return make_ref<CmdProfileUpgrade>(); }},
@ -848,13 +850,6 @@ struct CmdProfile : NixMultiCommand
#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");

View file

@ -5,9 +5,9 @@
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
@ -16,13 +16,6 @@ struct CmdRealisation : virtual NixMultiCommand
}
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");

View file

@ -196,10 +196,12 @@ struct CmdRegistryPin : RegistryCommand, EvalCommand
}
};
struct CmdRegistry : virtual NixMultiCommand
struct CmdRegistry : NixMultiCommand
{
CmdRegistry()
: MultiCommand({
: NixMultiCommand(
"registry",
{
{"list", []() { return make_ref<CmdRegistryList>(); }},
{"add", []() { return make_ref<CmdRegistryAdd>(); }},
{"remove", []() { return make_ref<CmdRegistryRemove>(); }},
@ -221,14 +223,6 @@ struct CmdRegistry : virtual NixMultiCommand
}
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");

View file

@ -205,7 +205,9 @@ struct CmdKeyConvertSecretToPublic : Command
struct CmdKey : NixMultiCommand
{
CmdKey()
: MultiCommand({
: NixMultiCommand(
"key",
{
{"generate-secret", []() { return make_ref<CmdKeyGenerateSecret>(); }},
{"convert-secret-to-public", []() { return make_ref<CmdKeyConvertSecretToPublic>(); }},
})
@ -218,13 +220,6 @@ struct CmdKey : NixMultiCommand
}
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");

View file

@ -2,9 +2,9 @@
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
@ -13,13 +13,6 @@ struct CmdStore : virtual NixMultiCommand
}
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");