From e1f5a981ef02a26482682447910a0ebeab0cfecf Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Sun, 7 Dec 2025 00:26:30 +0100 Subject: [PATCH] libcmd/repl: rework command handler It's 2026 and the command handler was entirely manual! We introduce a basic command handler framework for the REPL that handles all the previous (implicit) features reasonably well. This also builds up the infrastructure for nicer features. Most important feature is that now we can let external plugins adds commands to the REPL. There's slight REPL changes: - errors for debugger commands are more explicit. - short-hand have a long-hand version. - help shows command sorted by std::map natural key sorting (based on aliases) while keeping the special syntaxes for the REPL language at the top. - sections were introduced for Flakes commands. - :st was merged into one command with an optional argument marker. Change-Id: Ibd24f994491bf5d45d4b218fd84c0944ae5050fd Signed-off-by: Raito Bezarius --- lix/libcmd/repl.cc | 1123 +++++++++++------ .../data/debug_frames.test | 65 +- 2 files changed, 776 insertions(+), 412 deletions(-) diff --git a/lix/libcmd/repl.cc b/lix/libcmd/repl.cc index 840bd93ed..79e51aed6 100644 --- a/lix/libcmd/repl.cc +++ b/lix/libcmd/repl.cc @@ -83,6 +83,49 @@ enum class ProcessLineResult { using namespace std::literals::string_view_literals; +struct NixRepl; +using ReplFunction = std::function; +using PrintDerivationOutputFunction = + std::function; + +struct CommandArgumentSpecifier +{ + std::string placeholderText; + bool optional = false; +}; + +static const CommandArgumentSpecifier argExpr = {.placeholderText = "expr"}; +static const CommandArgumentSpecifier argPath = {.placeholderText = "path"}; + +struct CommandAttributes +{ + std::list aliases; + /** + * Whether this command can only be used inside of the debugger. + */ + bool debugModeOnly; + std::optional help; + std::optional section; + std::list positionalArgsSpecifiers; +}; + +struct REPLCommand +{ + ReplFunction handler; + CommandAttributes attributes; +}; + +struct UnexpectedArgument : Error +{ + std::string argValue; + + UnexpectedArgument(const std::string & argValue) + : Error("unexpected argument") + , argValue(argValue) + { + } +}; + struct NixRepl : AbstractNixRepl , detail::ReplCompleterMixin @@ -90,40 +133,12 @@ struct NixRepl , gc #endif { - /* clang-format: off */ - static constexpr std::array COMMANDS = { - "add"sv, "a"sv, - "load"sv, "l"sv, - "load-flake"sv, "lf"sv, - "reload"sv, "r"sv, - "edit"sv, "e"sv, - "t"sv, - "u"sv, - "b"sv, - "bl"sv, - "i"sv, - "sh"sv, - "log"sv, - "print"sv, "p"sv, - "quit"sv, "q"sv, - "doc"sv, - "te"sv, - }; - - static constexpr std::array DEBUG_COMMANDS = { - "env"sv, - "bt"sv, "backtrace"sv, - "st"sv, - "c"sv, "continue"sv, - "s"sv, "step"sv, - }; - /* clang-format: on */ - Evaluator & evaluator; size_t debugTraceIndex; Strings loadedFiles; std::function getValues; + std::map> registeredCommands; // Uses 8MiB of memory. It's fine. const static int envSize = 1 << 20; @@ -141,6 +156,9 @@ struct NixRepl ReplExitStatus mainLoop() override; void initEnv() override; + void initDebugBuiltinCommands(); + void initBuiltinCommands(); + virtual StringSet completePrefix(const std::string & prefix) override; /** @@ -149,9 +167,19 @@ struct NixRepl */ StorePath getDerivationPath(Value & v); /** - * Build a path and show a progress bar for it. + * Evaluate a string argument into a store path. + */ + StorePath evalIntoDerivationPath(const std::string & drvArg); + /** + * Build a derivation path and show a progress bar for it. */ Derivation buildWithProgressBar(const StorePath & drvPath); + /** + * Print the derivation outputs produced by a derivation. + * A function that should return the string for each output can be passed + * to customize the formatting and perform additional actions such as adding permanent roots. + */ + void printDerivationOutputs(const StorePath & drvPath, PrintDerivationOutputFunction printFn); ProcessLineResult processLine(std::string line); @@ -165,6 +193,13 @@ struct NixRepl void loadFiles(); void reloadFiles(); + void addCommand( + const std::string & name, + ReplFunction && function, + const CommandAttributes & attributes = {} + ); + void generateHelpCommand(); + template void addToScope(T && things, NameFn nameFn, ValueFn valueFn); @@ -257,6 +292,7 @@ NixRepl::NixRepl(const SearchPath & searchPath, nix::ref store, EvalState , staticEnv(new StaticEnv(nullptr, evaluator.builtins.staticEnv.get())) , interacter(makeInteracter()) { + initBuiltinCommands(); } void runNix(Path program, const Strings & args) @@ -379,20 +415,12 @@ StringSet NixRepl::completePrefix(const std::string &prefix) // Luckily, editline provides a global variable for its current buffer, so we can // check for the presence of a colon there. if (rl_line_buffer != nullptr && rl_line_buffer[0] == ':') { - for (auto const & colonCmd : this->COMMANDS) { - if (colonCmd.starts_with(prefix)) { + for (auto const & [colonCmd, cmd] : registeredCommands) { + if ((!cmd->attributes.debugModeOnly || inDebugger()) && colonCmd.starts_with(prefix)) { completions.insert(std::string(colonCmd)); } } - if (evaluator.debug && evaluator.debug->inDebugger) { - for (auto const & colonCmd : this->DEBUG_COMMANDS) { - if (colonCmd.starts_with(prefix)) { - completions.insert(std::string(colonCmd)); - } - } - } - // If there were : command completions, then we should only return those, // because otherwise this is not valid Nix syntax. // However if we didn't get any completions, then this could be something @@ -501,6 +529,14 @@ StorePath NixRepl::getDerivationPath(Value & v) { return *drvPath; } +StorePath NixRepl::evalIntoDerivationPath(const std::string & drvArg) +{ + Value v; + evalString(drvArg, v); + + return getDerivationPath(v); +} + Derivation NixRepl::buildWithProgressBar(const StorePath & drvPath) { // TODO: this only shows a progress bar for explicitly initiated builds, @@ -520,6 +556,18 @@ Derivation NixRepl::buildWithProgressBar(const StorePath & drvPath) return drv; } +void NixRepl::printDerivationOutputs( + const StorePath & drvPath, PrintDerivationOutputFunction printFn +) +{ + logger->cout("\nThis derivation produced the following outputs:"); + for (auto & [outputName, outputPath] : + state.aio.blockOn(evaluator.store->queryDerivationOutputMap(drvPath))) + { + logger->cout(printFn(outputName, outputPath)); + } +} + void NixRepl::loadDebugTraceEnv(const DebugTrace & dt) { initEnv(); @@ -533,6 +581,30 @@ void NixRepl::loadDebugTraceEnv(const DebugTrace & dt) } } +void NixRepl::addCommand( + const std::string & name, ReplFunction && handler, const CommandAttributes & attributes +) +{ + if (registeredCommands.contains(name)) { + throw Error("Command '%s' is already registered: commands cannot be shadowed", name); + } + + registeredCommands[name] = std::make_shared(std::move(handler), attributes); + + for (auto & alias : attributes.aliases) { + if (registeredCommands.contains(alias)) { + throw Error( + "Command '%s' is already registered: alias (original command '%s') cannot shadow other " + "commands", + name, + alias + ); + } + + registeredCommands[alias] = registeredCommands[name]; + } +} + ProcessLineResult NixRepl::processLine(std::string line) { line = trim(line); @@ -543,366 +615,659 @@ ProcessLineResult NixRepl::processLine(std::string line) if (line[0] == ':') { size_t p = line.find_first_of(" \n\r\t"); - command = line.substr(0, p); + command = line.substr(1, p - 1); if (p != std::string::npos) arg = removeWhitespace(line.substr(p)); } else { arg = line; } - bool inDebugger = evaluator.debug && evaluator.debug->inDebugger; - - if (command == ":?" || command == ":help") { - // FIXME: convert to Markdown, include in the 'nix repl' manpage. - std::cout - << "The following commands are available:\n" - << "\n" - << " Evaluate and print expression\n" - << " = Bind expression to variable\n" - << " :a, :add Add attributes from resulting set to scope\n" - << " :b Build a derivation\n" - << " :bl Build a derivation, creating GC roots in the\n" - << " working directory\n" - << " :e, :edit Open package or function in $EDITOR\n" - << " :env Show env stack\n" - << " :i Build derivation, then install result into\n" - << " current profile\n" - << " :l, :load Load Nix expression and add it to scope\n" - << " :lf, :load-flake Load Nix flake and add it to scope\n" - << " :p, :print Evaluate and print expression recursively\n" - << " Strings are printed directly, without escaping.\n" - << " :q, :quit Exit nix-repl\n" - << " :r, :reload Reload all files\n" - << " :sh Build dependencies of derivation, then start\n" - << " nix-shell\n" - << " :t Describe result of evaluation\n" - << " :u Build derivation, then start nix-shell\n" - << " :doc Show documentation for the provided function (experimental lambda support)\n" - << " :log Show logs for a derivation\n" - << " :te, :trace-enable [bool] Enable, disable or toggle showing traces for\n" - << " errors\n" - << " :?, :help Brings up this help menu\n" - ; - if (inDebugger) { - std::cout - << "\n" - << " Debug mode commands\n" - << " :bt, :backtrace Show trace stack\n" - << " :st Show current trace\n" - << " :st Change to another trace in the stack\n" - << " :c, :continue Go until end of program, exception, or builtins.break\n" - << " :s, :step Go one step\n" - ; - } - - } - - else if (command == ":bt" || command == ":backtrace") { - if (!inDebugger) - throw Error("backtrace command is only available in debug mode (see %s)", "--debugger"); - auto traces = evaluator.debug->traces(); - for (const auto & [idx, i] : enumerate(traces)) { - std::cout << "\n" << ANSI_BLUE << idx << ANSI_NORMAL << ": "; - showDebugTrace(std::cout, evaluator.positions, *i); - } - } - - else if (command == ":env") { - if (inDebugger) { - auto traces = evaluator.debug->traces(); - for (const auto & [idx, i] : enumerate(traces)) { - if (idx == debugTraceIndex) { - printEnvBindings(state, i->expr, i->env); - break; - } - } - } else { - printEnvBindings(state.ctx.symbols, *staticEnv, *env, 0); - } - } - - else if (command == ":st") { - if (!inDebugger) - throw Error("trace command is only available in debug mode (see %s)", "--debugger"); + if (registeredCommands.contains(command)) { + auto registeredCommand = registeredCommands[command]; try { - // change the DebugTrace index. - debugTraceIndex = stoi(arg); - } catch (...) { } - - auto traces = evaluator.debug->traces(); - for (const auto & [idx, i] : enumerate(traces)) { - if (idx == debugTraceIndex) { - std::cout << "\n" << ANSI_BLUE << idx << ANSI_NORMAL << ": "; - showDebugTrace(std::cout, evaluator.positions, *i); - std::cout << std::endl; - printEnvBindings(state, i->expr, i->env); - loadDebugTraceEnv(*i); - break; - } - } - } - - else if (command == ":s" || command == ":step") { - if (!inDebugger) - throw Error("step command is only available in debug mode (see %s)", "--debugger"); - // set flag to stop at next DebugTrace; exit repl. - evaluator.debug->stop = true; - return ProcessLineResult::Continue; - } - - else if (command == ":c" || command == ":continue") { - if (!inDebugger) - throw Error("continue command is only available in debug mode (see %s)", "--debugger"); - // set flag to run to next breakpoint or end of program; exit repl. - evaluator.debug->stop = false; - return ProcessLineResult::Continue; - } - - else if (command == ":a" || command == ":add") { - Value v; - evalString(arg, v); - addAttrsToScope(v); - } - - else if (command == ":l" || command == ":load") { - state.resetFileCache(); - loadFile(arg); - } - - else if (command == ":lf" || command == ":load-flake") { - loadFlake(arg); - } - - else if (command == ":r" || command == ":reload") { - state.resetFileCache(); - reloadFiles(); - } - - else if (command == ":e" || command == ":edit") { - Value v; - evalString(arg, v); - - const auto [path, line] = [&] () -> std::pair { - if (v.type() == nPath || v.type() == nString) { - NixStringContext context; - auto path = state.coerceToPath(noPos, v, context, "while evaluating the filename to edit"); - return {path, 0}; - } else if (v.isLambda()) { - auto pos = evaluator.positions[v.lambda().fun->pos]; - if (auto path = std::get_if(&pos.origin)) - return {*path, pos.line}; - else - throw Error("'%s' cannot be shown in an editor", pos); - } else { - // assume it's a derivation - return findPackageFilename(state, v, arg); - } - }(); - - // Open in EDITOR - auto args = editorFor(path, line); - auto editor = args.front(); - args.pop_front(); - - // runProgram redirects stdout to a StringSink, - // using runProgram2 to allow editors to display their UI - runProgram2(RunOptions { .program = editor, .searchPath = true, .args = args }).waitAndCheck(); - - // Reload right after exiting the editor if path is not in store - // Store is immutable, so there could be no changes, so there's no need to reload - if (!evaluator.store->isInStore(canonPath(path.canonical().abs(), true))) { - state.resetFileCache(); - reloadFiles(); - } - } - - else if (command == ":t") { - Value v; - evalString(arg, v); - logger->cout(showType(v)); - } - - else if (command == ":u") { - Value v, f, result; - evalString(arg, v); - evalString("drv: (import {}).runCommand \"shell\" { buildInputs = [ drv ]; } \"\"", f); - state.callFunction(f, v, result, PosIdx()); - - StorePath drvPath = getDerivationPath(result); - runNix("nix-shell", {evaluator.store->printStorePath(drvPath)}); - } - - else if (command == ":log") { - if (arg.empty()) - throw Error("cannot use ':log' without a specifying a derivation"); - StorePath drvPath = ([&] { - auto maybeDrvPath = evaluator.store->maybeParseStorePath(arg); - if (maybeDrvPath && maybeDrvPath->isDerivation()) { - return std::move(*maybeDrvPath); - } else { - Value v; - evalString(arg, v); - return getDerivationPath(v); - } - })(); - Path drvPathRaw = evaluator.store->printStorePath(drvPath); - - settings.readOnlyMode = true; - Finally roModeReset([&]() { - settings.readOnlyMode = false; - }); - auto subs = state.aio.blockOn(getDefaultSubstituters()); - - subs.push_front(evaluator.store); - - bool foundLog = false; - RunPager pager; - for (auto & sub : subs) { - auto * logSubP = dynamic_cast(&*sub); - if (!logSubP) { - printInfo("Skipped '%s' which does not support retrieving build logs", sub->getUri()); - continue; - } - auto & logSub = *logSubP; - - auto log = state.aio.blockOn(logSub.getBuildLog(drvPath)); - if (log) { - printInfo("got build log for '%s' from '%s'", drvPathRaw, logSub.getUri()); - logger->writeToStdout(*log); - foundLog = true; - break; - } - } - if (!foundLog) throw Error("build log of '%s' is not available", drvPathRaw); - } - - else if (command == ":b" || command == ":bl" || command == ":i" || command == ":sh") { - Value v; - evalString(arg, v); - StorePath drvPath = getDerivationPath(v); - Path drvPathRaw = evaluator.store->printStorePath(drvPath); - - if (command == ":b" || command == ":bl") { - auto drv = buildWithProgressBar(drvPath); - logger->cout("\nThis derivation produced the following outputs:"); - for (auto & [outputName, outputPath] : - state.aio.blockOn(evaluator.store->queryDerivationOutputMap(drvPath))) - { - auto localStore = evaluator.store.try_cast_shared(); - if (localStore && command == ":bl") { - std::string symlink = "repl-result-" + outputName; - state.aio.blockOn(localStore->addPermRoot(outputPath, absPath(symlink))); - logger->cout(" ./%s -> %s", symlink, evaluator.store->printStorePath(outputPath)); - } else { - logger->cout(" %s -> %s", outputName, evaluator.store->printStorePath(outputPath)); - } - } - } else if (command == ":i") { - runNix("nix-env", {"-i", drvPathRaw}); - } else { - runNix("nix-shell", {drvPathRaw}); - } - } - - else if (command == ":p" || command == ":print") { - Value v; - evalString(arg, v); - if (v.type() == nString) { - std::cout << v.str(); - } else if (v.type() == nAttrs && state.isDerivation(v)) { - printValue(std::cout, v, 2, 1); - } else { - printValue(std::cout, v, std::numeric_limits::max(), 0); - } - std::cout << std::endl; - } - - else if (command == ":q" || command == ":quit") { - if (evaluator.debug) { - evaluator.debug->stop = false; - } - return ProcessLineResult::Quit; - } - - else if (command == ":doc") { - Value v; - evalString(arg, v); - if (auto doc = evaluator.builtins.getDoc(v)) { - std::string markdown; - - if (!doc->args.empty() && doc->name) { - auto args = doc->args; - for (auto & arg : args) - arg = "*" + arg + "*"; - - markdown += - "**Synopsis:** `builtins." + (std::string) (*doc->name) + "` " - + concatStringsSep(" ", args) + "\n\n"; + if (registeredCommand->attributes.debugModeOnly && !inDebugger()) { + throw Error("command '%s' can only be used when the debugger is active", command); } - markdown += stripIndentation(doc->doc); - - logger->cout(trim(renderMarkdownToTerminal(markdown))); - } else if (v.isLambda()) { - auto pos = evaluator.positions[v.lambda().fun->pos]; - if (auto path = std::get_if(&pos.origin)) { - // Path and position have now been obtained, feed to nix-doc library to get data. - auto docComment = lambdaDocsForPos(*path, pos); - if (!docComment) { - throw Error("lambda '%s' has no documentation comment", pos); - } - - // Build and print Markdown representation of documentation comment. - std::string markdown = stripIndentation(docComment.get()); - logger->cout(trim(renderMarkdownToTerminal(markdown))); - } else { - throw Error("lambda '%s' doesn't have a determinable source file", pos); - } - } else { - throw Error("value '%s' does not have documentation", arg); + return registeredCommand->handler(*this, arg); + } catch (UnexpectedArgument & excArg) { + throw Error("unexpected argument '%1%' to command '%2%", excArg.argValue, command); } - } - - else if (command == ":te" || command == ":trace-enable") { - if (arg == "false" || (arg == "" && loggerSettings.showTrace)) { - std::cout << "not showing error traces\n"; - loggerSettings.showTrace.override(false); - } else if (arg == "true" || (arg == "" && !loggerSettings.showTrace)) { - std::cout << "showing error traces\n"; - loggerSettings.showTrace.override(true); - } else { - throw Error("unexpected argument '%s' to %s", arg, command); - }; - } - - else if (command != "") + } else if (command != "") { throw Error("unknown command '%1%'", command); - - else { + } else { /* A line is either a regular expression or a `var = expr` assignment */ std::variant, ExprReplBindings> result = parseReplString(line); - std::visit(overloaded { - [&](ExprReplBindings & b) { - for (auto & [name, e] : b.symbols) { + std::visit( + overloaded{ + [&](ExprReplBindings & b) { + for (auto & [name, e] : b.symbols) { + Value v; + e->eval(state, *env, v); + // NONEXTLINE(bugprone-unused-return-value): leak because of thunk + // references + (void) e.release(); + addVarToScope(name, v); + } + }, + [&](std::unique_ptr & e) { Value v; e->eval(state, *env, v); - (void) e.release(); // NOLINT(bugprone-unused-return-value): leak because of thunk references - addVarToScope(name, v); + // NONEXTLINE(bugprone-unused-return-value): leak because of thunk references + (void) e.release(); + state.forceValue(v, noPos); + printValue(std::cout, v, 1); + std::cout << std::endl; } }, - [&](std::unique_ptr & e) { - Value v; - e->eval(state, *env, v); - (void) e.release(); // NOLINT(bugprone-unused-return-value): leak because of thunk references - state.forceValue(v, noPos); - printValue(std::cout, v, 1); - std::cout << std::endl; - } - }, result); + result + ); } return ProcessLineResult::PromptAgain; } +void NixRepl::initDebugBuiltinCommands() +{ + addCommand( + "backtrace", + [](NixRepl & repl, const std::string & _arg) { + auto traces = repl.evaluator.debug->traces(); + for (const auto & [idx, i] : enumerate(traces)) { + std::cout << "\n" << ANSI_BLUE << idx << ANSI_NORMAL << ": "; + showDebugTrace(std::cout, repl.evaluator.positions, *i); + } + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"bt"}, + .debugModeOnly = true, + .help = "Show trace stack", + .section = "Debug mode"} + ); + + addCommand( + "show-trace", + [](NixRepl & repl, const std::string & arg) { + try { + repl.debugTraceIndex = stoi(arg); + } catch (...) { + } + + auto traces = repl.evaluator.debug->traces(); + for (const auto & [idx, i] : enumerate(traces)) { + if (idx == repl.debugTraceIndex) { + std::cout << "\n" << ANSI_BLUE << idx << ANSI_NORMAL << ": "; + showDebugTrace(std::cout, repl.evaluator.positions, *i); + std::cout << std::endl; + printEnvBindings(repl.state, i->expr, i->env); + repl.loadDebugTraceEnv(*i); + break; + } + } + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"st"}, + .debugModeOnly = true, + .help = "Show current trace. If an integer is provided, this switches to that stack " + "beforehand.", + .section = "Debug mode", + .positionalArgsSpecifiers = {{.placeholderText = "integer index", .optional = true}}} + ); + + addCommand( + "step", + [](NixRepl & repl, const std::string & _arg) { + repl.evaluator.debug->stop = true; + return ProcessLineResult::Continue; + }, + {.aliases = {"s"}, .debugModeOnly = true, .help = "Go one step", .section = "Debug mode"} + ); + + addCommand( + "continue", + [](NixRepl & repl, const std::string & _arg) { + repl.evaluator.debug->stop = false; + return ProcessLineResult::Continue; + }, + {.aliases = {"c"}, + .debugModeOnly = true, + .help = "Go until end of program, exception or builtins.break", + .section = "Debug mode"} + ); +} + +void NixRepl::initBuiltinCommands() +{ + initDebugBuiltinCommands(); + + addCommand( + "add", + [](NixRepl & repl, const std::string & arg) { + Value v; + repl.evalString(arg, v); + repl.addAttrsToScope(v); + + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"a"}, + .help = "Add attributes from resulting set to scope", + .positionalArgsSpecifiers = {argExpr}} + ); + + addCommand( + "load", + [](NixRepl & repl, const std::string & arg) { + repl.state.resetFileCache(); + repl.loadFile(arg); + + return ProcessLineResult::PromptAgain; + }, + { + .aliases = {"l"}, + .help = "Load Nix expression and add it to scope", + .positionalArgsSpecifiers = {argPath}, + } + ); + + addCommand( + "reload", + [](NixRepl & repl, const std::string & _arg) { + repl.state.resetFileCache(); + repl.reloadFiles(); + + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"r"}, .help = "Reload all files successfully loaded"} + ); + + addCommand( + "edit", + [](NixRepl & repl, const std::string & arg) { + Value v; + repl.evalString(arg, v); + + const auto [path, line] = [&]() -> std::pair { + if (v.type() == nPath || v.type() == nString) { + NixStringContext context; + auto path = repl.state.coerceToPath( + noPos, v, context, "while evaluating the filename to edit" + ); + return {path, 0}; + } else if (v.isLambda()) { + auto pos = repl.evaluator.positions[v.lambda().fun->pos]; + if (auto path = std::get_if(&pos.origin)) { + return {*path, pos.line}; + } + throw Error("'%s' cannot be shown in an editor", pos); + } else { + return findPackageFilename(repl.state, v, arg); + } + }(); + + auto args = editorFor(path, line); + auto editor = args.front(); + args.pop_front(); + + runProgram2(RunOptions{.program = editor, .searchPath = true, .args = args}) + .waitAndCheck(); + + if (!repl.evaluator.store->isInStore(canonPath(path.canonical().abs(), true))) { + repl.state.resetFileCache(); + repl.reloadFiles(); + } + + return ProcessLineResult::PromptAgain; + }, + { + .aliases = {"e"}, + .help = "Open package or function in $EDITOR", + .positionalArgsSpecifiers = {argExpr}, + } + ); + + addCommand( + "type", + [](NixRepl & repl, const std::string & arg) { + Value v; + repl.evalString(arg, v); + logger->cout(showType(v)); + + return ProcessLineResult::PromptAgain; + }, + { + .aliases = {"t"}, + .help = "Describe result of evaluation", + .positionalArgsSpecifiers = {argExpr}, + } + ); + + addCommand( + "use", + [](NixRepl & repl, const std::string & arg) { + Value v, f, result; + repl.evalString(arg, v); + repl.evalString( + R""("drv: (import {}).runCommand "shell")"" + R""({ buildInputs = [ drv ]; } "")"", + f + ); + repl.state.callFunction(f, v, result, PosIdx()); + + StorePath drvPath = repl.getDerivationPath(result); + runNix("nix-shell", {repl.evaluator.store->printStorePath(drvPath)}); + + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"u"}, + .help = "Build derivation, then start nix-shell", + .positionalArgsSpecifiers = {argExpr}} + ); + + addCommand( + "log", + [](NixRepl & repl, const std::string & arg) { + if (arg.empty()) { + throw Error("cannot use ':log' without specifying a derivation"); + } + + StorePath drvPath = ([&] { + auto maybeDrvPath = repl.evaluator.store->maybeParseStorePath(arg); + if (maybeDrvPath && maybeDrvPath->isDerivation()) { + return std::move(*maybeDrvPath); + } else { + Value v; + repl.evalString(arg, v); + return repl.getDerivationPath(v); + } + })(); + Path drvPathRaw = repl.evaluator.store->printStorePath(drvPath); + + settings.readOnlyMode = true; + Finally roReset([&]() { settings.readOnlyMode = false; }); + + auto subs = repl.state.aio.blockOn(getDefaultSubstituters()); + subs.push_front(repl.evaluator.store); + + bool foundLog = false; + RunPager pager; + + for (auto & sub : subs) { + auto * logSubP = dynamic_cast(&*sub); + if (!logSubP) { + printInfo( + "Skipped '%s' which does not support retrieving build logs", sub->getUri() + ); + continue; + } + auto & logSub = *logSubP; + + auto log = repl.state.aio.blockOn(logSub.getBuildLog(drvPath)); + if (log) { + printInfo("got build log for '%s' from '%s'", drvPathRaw, logSub.getUri()); + logger->writeToStdout(*log); + foundLog = true; + break; + } + } + + if (!foundLog) { + throw Error("build log of '%s' is not available", drvPathRaw); + } + + return ProcessLineResult::PromptAgain; + }, + {.help = "Show logs for a derivation", + .positionalArgsSpecifiers = {{.placeholderText = "expr | .drv path"}}} + ); + + addCommand( + "build", + [](NixRepl & repl, const std::string & arg) { + auto drvPath = repl.evalIntoDerivationPath(arg); + auto drv = repl.buildWithProgressBar(drvPath); + + repl.printDerivationOutputs( + drvPath, [&repl](const std::string & outputName, const StorePath & outputPath) { + return fmt( + " %s -> %s", outputName, repl.evaluator.store->printStorePath(outputPath) + ); + } + ); + + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"b"}, .help = "Build a derivation", .positionalArgsSpecifiers = {argExpr}} + ); + + addCommand( + "build-with-gc-roots", + [](NixRepl & repl, const std::string & arg) { + auto drvPath = repl.evalIntoDerivationPath(arg); + auto drv = repl.buildWithProgressBar(drvPath); + + repl.printDerivationOutputs( + drvPath, [&repl](const std::string & outputName, const StorePath & outputPath) { + auto localStore = repl.evaluator.store.try_cast_shared(); + + std::string symlink = fmt("repl-result-%s", outputName); + repl.state.aio.blockOn(localStore->addPermRoot(outputPath, absPath(symlink))); + return fmt( + " ./%s -> %s", symlink, repl.evaluator.store->printStorePath(outputPath) + ); + } + ); + + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"bl"}, + .help = "Build a derivation, creating GC roots in the working directory", + .positionalArgsSpecifiers = {argExpr}} + ); + + addCommand( + "build-and-install", + [](NixRepl & repl, const std::string & arg) { + Path drvPathRaw = + repl.evaluator.store->printStorePath(repl.evalIntoDerivationPath(arg)); + + runNix("nix-env", {"-i", drvPathRaw}); + + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"i"}, + .help = "Build derivation, then install result into current profile", + .positionalArgsSpecifiers = {argExpr}} + ); + + addCommand( + "shell", + [](NixRepl & repl, const std::string & arg) { + Path drvPathRaw = + repl.evaluator.store->printStorePath(repl.evalIntoDerivationPath(arg)); + + runNix("nix-shell", {drvPathRaw}); + + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"sh"}, + .help = "Build dependencies of derivation, then start nix-shell", + .positionalArgsSpecifiers = {argExpr}} + ); + + addCommand( + "print", + [](NixRepl & repl, const std::string & arg) { + Value v; + repl.evalString(arg, v); + if (v.type() == nString) { + std::cout << v.str(); + } else if (v.type() == nAttrs && repl.state.isDerivation(v)) { + repl.printValue(std::cout, v, 2, 1); + } else { + repl.printValue(std::cout, v, std::numeric_limits::max(), 0); + } + std::cout << std::endl; + + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"p"}, + .help = "Evaluate and print expression recursively\n" + "Strings are printed directly, without escaping.", + .positionalArgsSpecifiers = {argExpr}} + ); + + addCommand( + "quit", + [](NixRepl & repl, const std::string & _arg) { + if (repl.evaluator.debug) { + repl.evaluator.debug->stop = false; + } + return ProcessLineResult::Quit; + }, + {.aliases = {"q"}, .help = "Exit the REPL"} + ); + + addCommand( + "doc", + [](NixRepl & repl, const std::string & arg) { + Value v; + repl.evalString(arg, v); + + if (auto doc = repl.evaluator.builtins.getDoc(v)) { + std::string markdown; + + if (!doc->args.empty() && doc->name) { + auto args = doc->args; + for (auto & arg : args) { + arg = "*" + arg + "*"; + } + + markdown += "**Synopsis:** `builtins." + (std::string) *doc->name + "` " + + concatStringsSep(" ", args) + "\n\n"; + } + + markdown += stripIndentation(doc->doc); + logger->cout(trim(renderMarkdownToTerminal(markdown))); + + } else if (v.isLambda()) { + auto pos = repl.evaluator.positions[v.lambda().fun->pos]; + if (auto path = std::get_if(&pos.origin)) { + auto docComment = lambdaDocsForPos(*path, pos); + if (!docComment) { + throw Error("lambda '%s' has no documentation comment", pos); + } + + std::string markdown = stripIndentation(docComment.get()); + logger->cout(trim(renderMarkdownToTerminal(markdown))); + } else { + throw Error("lambda '%s' doesn't have a determinable source file", pos); + } + } else { + throw Error("value '%s' does not have documentation", arg); + } + + return ProcessLineResult::PromptAgain; + }, + {.help = "Show documentation for the provided function (experimental lambda support)", + .positionalArgsSpecifiers = {argExpr}} + ); + + addCommand( + "trace-enable", + [](NixRepl & repl, const std::string & arg) { + if (arg == "false" || (arg == "" && loggerSettings.showTrace)) { + std::cout << "not showing error traces\n"; + loggerSettings.showTrace.override(false); + } else if (arg == "true" || (arg == "" && !loggerSettings.showTrace)) { + std::cout << "showing error traces\n"; + loggerSettings.showTrace.override(true); + } else { + throw UnexpectedArgument(arg); + } + + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"te"}, + .help = "Enable, disable, or toggle showing traces for errors", + .positionalArgsSpecifiers = {{.placeholderText = "bool", .optional = true}}} + ); + + addCommand( + "env", + [](NixRepl & repl, const std::string & _arg) { + if (repl.inDebugger()) { + auto traces = repl.evaluator.debug->traces(); + for (const auto & [idx, i] : enumerate(traces)) { + if (idx == repl.debugTraceIndex) { + printEnvBindings(repl.state, i->expr, i->env); + break; + } + } + } else { + printEnvBindings(repl.state.ctx.symbols, *repl.staticEnv, *repl.env, 0); + } + + return ProcessLineResult::PromptAgain; + }, + {.help = "Show environment stack"} + ); + + addCommand( + "load-flake", + [](NixRepl & repl, const std::string & arg) { + repl.loadFlake(arg); + + return ProcessLineResult::PromptAgain; + }, + {.aliases = {"lf"}, + .help = "Load Nix flake and add it to the scope", + .section = "Flakes", + .positionalArgsSpecifiers = {{.placeholderText = "flakeref"}}} + ); + + generateHelpCommand(); +} + +static Strings wrapText(const std::string & text, size_t width) +{ + Strings lines, inputLines; + + inputLines = tokenizeString(text, "\n"); + + for (auto & line : inputLines) { + // Preserve empty lines + if (line.empty()) { + lines.emplace_back(""); + continue; + } + + std::string wrappedLine; + Strings words = tokenizeString(line, " "); + + for (auto & word : words) { + if (wrappedLine.size() + word.size() + 1 > width) { + lines.push_back(wrappedLine); + wrappedLine.clear(); + } + if (!wrappedLine.empty()) { + wrappedLine += " "; + } + wrappedLine += word; + } + + if (!wrappedLine.empty()) { + lines.push_back(wrappedLine); + } + } + + return lines; +} + +void NixRepl::generateHelpCommand() +{ + addCommand( + "help", + [](NixRepl & repl, const std::string & _arg) { + // Special entries appear at the top, general entries stems from command registration. + std::map generalEntries, specialEntries; + std::map> perSectionEntries; + std::cout << "The following commands are available:\n" + << "\n"; + + specialEntries[""] = "Evaluate and print expression"; + specialEntries[" = "] = "Bind expression to variable"; + + size_t maxLhsWidth = 20; + for (auto & [name, command] : repl.registeredCommands) { + auto aliases = command->attributes.aliases; + bool isAlias = std::find(aliases.begin(), aliases.end(), name) != aliases.end(); + + if (command->attributes.debugModeOnly && !repl.inDebugger()) { + continue; + } + + if (isAlias) { + continue; + } + + auto lhs = concatMapStringsSep(", ", aliases, [](const std::string & alias) { + return ":" + alias; + }); + if (!lhs.empty()) { + lhs += ", "; + } + + lhs += ":"; + lhs += name; + + if (!command->attributes.positionalArgsSpecifiers.empty()) { + lhs += " "; + } + + lhs += concatMapStringsSep( + " ", + command->attributes.positionalArgsSpecifiers, + [](const CommandArgumentSpecifier & specifier) { + if (specifier.optional) { + return "[" + specifier.placeholderText + "]"; + } else { + return "<" + specifier.placeholderText + ">"; + } + } + ); + + maxLhsWidth = std::max(maxLhsWidth, lhs.size() + 5); + auto helpText = + command->attributes.help.value_or("No help text is provided for this command."); + + if (!command->attributes.section) { + generalEntries[lhs] = helpText; + } else { + perSectionEntries[*command->attributes.section][lhs] = helpText; + } + } + + const size_t totalWidth = std::get<1>(getWindowSize()); + const size_t lhsWidth = maxLhsWidth; + // 2 + 1 spaces + const size_t rhsWidth = std::max(static_cast(0), totalWidth - lhsWidth - 3); + + auto printSection = [lhsWidth, + rhsWidth](const std::map & entries) { + for (auto & [lhs, rhs] : entries) { + auto wrapped = wrapText(rhs, rhsWidth); + + for (auto const [index, wrappedComponent] : enumerate(wrapped)) { + if (index == 0) { + // 2 + 1 spaces + auto compensatedLhsWidth = std::max(static_cast(0), lhsWidth - 3); + std::cout + << std::format(" {:<{}} {}\n", lhs, compensatedLhsWidth, wrappedComponent); + } else { + // only 1 space + auto compensatedLhsWidth = std::max(static_cast(0), lhsWidth - 1); + std::cout + << std::format("{:<{}} {}\n", " ", compensatedLhsWidth, wrappedComponent); + } + } + } + }; + + printSection(specialEntries); + printSection(generalEntries); + for (auto & [section, entries] : perSectionEntries) { + std::cout << "\n " << section << " commands\n" << std::endl; + printSection(entries); + } + + return ProcessLineResult::PromptAgain; + }, + { + .aliases = {"?"}, + .help = "Print help about all commands (this content)", + } + ); +} + void NixRepl::loadFile(const Path & path) { loadedFiles.remove(path); diff --git a/tests/functional/repl_characterization/data/debug_frames.test b/tests/functional/repl_characterization/data/debug_frames.test index a556b2420..d0ac05133 100644 --- a/tests/functional/repl_characterization/data/debug_frames.test +++ b/tests/functional/repl_characterization/data/debug_frames.test @@ -2,7 +2,7 @@ :c at the root repl is not allowed since no debugger is running yet nix-repl> :c - error: unknown command ':c' + error: command 'c' can only be used when the debugger is active :c and other commands become available once a debugger starts nix-repl> with {}; a @@ -14,38 +14,37 @@ nix-repl> :? The following commands are available: - Evaluate and print expression - = Bind expression to variable - :a, :add Add attributes from resulting set to scope - :b Build a derivation - :bl Build a derivation, creating GC roots in the - working directory - :e, :edit Open package or function in $EDITOR - :i Build derivation, then install result into - current profile - :l, :load Load Nix expression and add it to scope - :lf, :load-flake Load Nix flake and add it to scope - :p, :print Evaluate and print expression recursively - Strings are printed directly, without escaping. - :q, :quit Exit nix-repl - :r, :reload Reload all files - :sh Build dependencies of derivation, then start - nix-shell - :t Describe result of evaluation - :u Build derivation, then start nix-shell - :doc Show documentation for the provided function (experimental lambda support) - :log Show logs for a derivation - :te, :trace-enable [bool] Enable, disable or toggle showing traces for - errors - :?, :help Brings up this help menu + Evaluate and print expression + = Bind expression to variable + :?, :help Print help about all commands (this content) + :a, :add Add attributes from resulting set to scope + :b, :build Build a derivation + :bl, :build-with-gc-roots Build a derivation, creating GC roots in the working directory + :doc Show documentation for the provided function (experimental lambda support) + :e, :edit Open package or function in $EDITOR + :env Show environment stack + :i, :build-and-install Build derivation, then install result into current profile + :l, :load Load Nix expression and add it to scope + :log Show logs for a derivation + :p, :print Evaluate and print expression recursively + Strings are printed directly, without escaping. + :q, :quit Exit the REPL + :r, :reload Reload all files successfully loaded + :sh, :shell Build dependencies of derivation, then start nix-shell + :t, :type Describe result of evaluation + :te, :trace-enable [bool] Enable, disable, or toggle showing traces for errors + :u, :use Build derivation, then start nix-shell - Debug mode commands - :env Show env stack - :bt, :backtrace Show trace stack - :st Show current trace - :st Change to another trace in the stack - :c, :continue Go until end of program, exception, or builtins.break - :s, :step Go one step + Debug mode commands + + :bt, :backtrace Show trace stack + :c, :continue Go until end of program, exception or builtins.break + :s, :step Go one step + :st, :show-trace [integer index] Show current trace. If an integer is provided, this switches to that stack beforehand. + + Flakes commands + + :lf, :load-flake Load Nix flake and add it to the scope we can now inspect state nix-repl> :bt @@ -70,7 +69,7 @@ and resume execution the debugger is once again disabled nix-repl> :c - error: unknown command ':c' + error: command 'c' can only be used when the debugger is active leaving the debugger from a toplevel error and entering it again doesn't leave old frames visible nix-repl> with {}; a