aboutsummaryrefslogtreecommitdiff
path: root/src/nix/repl.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/nix/repl.cc')
-rw-r--r--src/nix/repl.cc719
1 files changed, 719 insertions, 0 deletions
diff --git a/src/nix/repl.cc b/src/nix/repl.cc
new file mode 100644
index 000000000..71790eb48
--- /dev/null
+++ b/src/nix/repl.cc
@@ -0,0 +1,719 @@
+#include <nix/config.h>
+
+#include <iostream>
+#include <cstdlib>
+
+#include <setjmp.h>
+
+#include <readline/readline.h>
+#include <readline/history.h>
+
+#include "shared.hh"
+#include "eval.hh"
+#include "eval-inline.hh"
+#include "store-api.hh"
+#include "common-opts.hh"
+#include "get-drvs.hh"
+#include "derivations.hh"
+#include "affinity.hh"
+#include "globals.hh"
+
+using namespace std;
+using namespace nix;
+
+#define ESC_RED "\033[31m"
+#define ESC_GRE "\033[32m"
+#define ESC_YEL "\033[33m"
+#define ESC_BLU "\033[34;1m"
+#define ESC_MAG "\033[35m"
+#define ESC_CYA "\033[36m"
+#define ESC_END "\033[0m"
+
+string programId = "nix-repl";
+const string historyFile = string(getenv("HOME")) + "/.nix-repl-history";
+
+struct NixRepl
+{
+ string curDir;
+ EvalState state;
+
+ Strings loadedFiles;
+
+ const static int envSize = 32768;
+ StaticEnv staticEnv;
+ Env * env;
+ int displ;
+ StringSet varNames;
+
+ StringSet completions;
+ StringSet::iterator curCompletion;
+
+ NixRepl(const Strings & searchPath, nix::ref<Store> store);
+ void mainLoop(const Strings & files);
+ void completePrefix(string prefix);
+ bool getLine(string & input, const char * prompt);
+ Path getDerivationPath(Value & v);
+ bool processLine(string line);
+ void loadFile(const Path & path);
+ void initEnv();
+ void reloadFiles();
+ void addAttrsToScope(Value & attrs);
+ void addVarToScope(const Symbol & name, Value & v);
+ Expr * parseString(string s);
+ void evalString(string s, Value & v);
+
+ typedef set<Value *> ValuesSeen;
+ std::ostream & printValue(std::ostream & str, Value & v, unsigned int maxDepth);
+ std::ostream & printValue(std::ostream & str, Value & v, unsigned int maxDepth, ValuesSeen & seen);
+};
+
+
+void printHelp()
+{
+ cout << "Usage: nix-repl [--help] [--version] [-I path] paths...\n"
+ << "\n"
+ << "nix-repl is a simple read-eval-print loop (REPL) for the Nix package manager.\n"
+ << "\n"
+ << "Options:\n"
+ << " --help\n"
+ << " Prints out a summary of the command syntax and exits.\n"
+ << "\n"
+ << " --version\n"
+ << " Prints out the Nix version number on standard output and exits.\n"
+ << "\n"
+ << " -I path\n"
+ << " Add a path to the Nix expression search path. This option may be given\n"
+ << " multiple times. See the NIX_PATH environment variable for information on\n"
+ << " the semantics of the Nix search path. Paths added through -I take\n"
+ << " precedence over NIX_PATH.\n"
+ << "\n"
+ << " paths...\n"
+ << " A list of paths to files containing Nix expressions which nix-repl will\n"
+ << " load and add to its scope.\n"
+ << "\n"
+ << " A path surrounded in < and > will be looked up in the Nix expression search\n"
+ << " path, as in the Nix language itself.\n"
+ << "\n"
+ << " If an element of paths starts with http:// or https://, it is interpreted\n"
+ << " as the URL of a tarball that will be downloaded and unpacked to a temporary\n"
+ << " location. The tarball must include a single top-level directory containing\n"
+ << " at least a file named default.nix.\n"
+ << flush;
+}
+
+
+string removeWhitespace(string s)
+{
+ s = chomp(s);
+ size_t n = s.find_first_not_of(" \n\r\t");
+ if (n != string::npos) s = string(s, n);
+ return s;
+}
+
+
+NixRepl::NixRepl(const Strings & searchPath, nix::ref<Store> store)
+ : state(searchPath, store)
+ , staticEnv(false, &state.staticBaseEnv)
+{
+ curDir = absPath(".");
+}
+
+
+void NixRepl::mainLoop(const Strings & files)
+{
+ string error = ANSI_RED "error:" ANSI_NORMAL " ";
+ std::cout << "Welcome to Nix version " << NIX_VERSION << ". Type :? for help." << std::endl << std::endl;
+
+ for (auto & i : files)
+ loadedFiles.push_back(i);
+
+ reloadFiles();
+ if (!loadedFiles.empty()) std::cout << std::endl;
+
+ // Allow nix-repl specific settings in .inputrc
+ rl_readline_name = "nix-repl";
+ using_history();
+ read_history(historyFile.c_str());
+
+ string input;
+
+ while (true) {
+ // When continuing input from previous lines, don't print a prompt, just align to the same
+ // number of chars as the prompt.
+ const char * prompt = input.empty() ? "nix-repl> " : " ";
+ if (!getLine(input, prompt)) {
+ std::cout << std::endl;
+ break;
+ }
+
+ try {
+ if (!removeWhitespace(input).empty() && !processLine(input)) return;
+ } catch (ParseError & e) {
+ if (e.msg().find("unexpected $end") != std::string::npos) {
+ // For parse errors on incomplete input, we continue waiting for the next line of
+ // input without clearing the input so far.
+ continue;
+ } else {
+ printMsg(lvlError, format(error + "%1%%2%") % (settings.showTrace ? e.prefix() : "") % e.msg());
+ }
+ } catch (Error & e) {
+ printMsg(lvlError, format(error + "%1%%2%") % (settings.showTrace ? e.prefix() : "") % e.msg());
+ } catch (Interrupted & e) {
+ printMsg(lvlError, format(error + "%1%%2%") % (settings.showTrace ? e.prefix() : "") % e.msg());
+ }
+
+ // We handled the current input fully, so we should clear it and read brand new input.
+ input.clear();
+ std::cout << std::endl;
+ }
+}
+
+
+/* Apparently, the only way to get readline() to return on Ctrl-C
+ (SIGINT) is to use siglongjmp(). That's fucked up... */
+static sigjmp_buf sigintJmpBuf;
+
+
+static void sigintHandler(int signo)
+{
+ siglongjmp(sigintJmpBuf, 1);
+}
+
+
+/* Oh, if only g++ had nested functions... */
+NixRepl * curRepl;
+
+char * completerThunk(const char * s, int state)
+{
+ string prefix(s);
+
+ /* If the prefix has a slash in it, use readline's builtin filename
+ completer. */
+ if (prefix.find('/') != string::npos)
+ return rl_filename_completion_function(s, state);
+
+ /* Otherwise, return all symbols that start with the prefix. */
+ if (state == 0) {
+ curRepl->completePrefix(s);
+ curRepl->curCompletion = curRepl->completions.begin();
+ }
+ if (curRepl->curCompletion == curRepl->completions.end()) return 0;
+ return strdup((curRepl->curCompletion++)->c_str());
+}
+
+
+bool NixRepl::getLine(string & input, const char * prompt)
+{
+ struct sigaction act, old;
+ act.sa_handler = sigintHandler;
+ sigfillset(&act.sa_mask);
+ act.sa_flags = 0;
+ if (sigaction(SIGINT, &act, &old))
+ throw SysError("installing handler for SIGINT");
+
+ if (sigsetjmp(sigintJmpBuf, 1)) {
+ input.clear();
+ } else {
+ curRepl = this;
+ rl_completion_entry_function = completerThunk;
+
+ char * s = readline(prompt);
+ if (!s) return false;
+ input.append(s);
+ input.push_back('\n');
+ if (!removeWhitespace(s).empty()) {
+ add_history(s);
+ append_history(1, 0);
+ }
+ free(s);
+ }
+
+ _isInterrupted = 0;
+
+ if (sigaction(SIGINT, &old, 0))
+ throw SysError("restoring handler for SIGINT");
+
+ return true;
+}
+
+
+void NixRepl::completePrefix(string prefix)
+{
+ completions.clear();
+
+ size_t dot = prefix.rfind('.');
+
+ if (dot == string::npos) {
+ /* This is a variable name; look it up in the current scope. */
+ StringSet::iterator i = varNames.lower_bound(prefix);
+ while (i != varNames.end()) {
+ if (string(*i, 0, prefix.size()) != prefix) break;
+ completions.insert(*i);
+ i++;
+ }
+ } else {
+ try {
+ /* This is an expression that should evaluate to an
+ attribute set. Evaluate it to get the names of the
+ attributes. */
+ string expr(prefix, 0, dot);
+ string prefix2 = string(prefix, dot + 1);
+
+ Expr * e = parseString(expr);
+ Value v;
+ e->eval(state, *env, v);
+ state.forceAttrs(v);
+
+ for (auto & i : *v.attrs) {
+ string name = i.name;
+ if (string(name, 0, prefix2.size()) != prefix2) continue;
+ completions.insert(expr + "." + name);
+ }
+
+ } catch (ParseError & e) {
+ // Quietly ignore parse errors.
+ } catch (EvalError & e) {
+ // Quietly ignore evaluation errors.
+ } catch (UndefinedVarError & e) {
+ // Quietly ignore undefined variable errors.
+ }
+ }
+}
+
+
+static int runProgram(const string & program, const Strings & args)
+{
+ std::vector<const char *> cargs; /* careful with c_str()! */
+ cargs.push_back(program.c_str());
+ for (Strings::const_iterator i = args.begin(); i != args.end(); ++i)
+ cargs.push_back(i->c_str());
+ cargs.push_back(0);
+
+ Pid pid;
+ pid = fork();
+ if (pid == -1) throw SysError("forking");
+ if (pid == 0) {
+ restoreAffinity();
+ execvp(program.c_str(), (char * *) &cargs[0]);
+ _exit(1);
+ }
+
+ return pid.wait();
+}
+
+
+bool isVarName(const string & s)
+{
+ if (s.size() == 0) return false;
+ char c = s[0];
+ if ((c >= '0' && c <= '9') || c == '-' || c == '\'') return false;
+ for (auto & i : s)
+ if (!((i >= 'a' && i <= 'z') ||
+ (i >= 'A' && i <= 'Z') ||
+ (i >= '0' && i <= '9') ||
+ i == '_' || i == '-' || i == '\''))
+ return false;
+ return true;
+}
+
+
+Path NixRepl::getDerivationPath(Value & v) {
+ DrvInfo drvInfo(state);
+ if (!getDerivation(state, v, drvInfo, false))
+ throw Error("expression does not evaluate to a derivation, so I can't build it");
+ Path drvPath = drvInfo.queryDrvPath();
+ if (drvPath == "" || !state.store->isValidPath(drvPath))
+ throw Error("expression did not evaluate to a valid derivation");
+ return drvPath;
+}
+
+
+bool NixRepl::processLine(string line)
+{
+ if (line == "") return true;
+
+ string command, arg;
+
+ if (line[0] == ':') {
+ size_t p = line.find_first_of(" \n\r\t");
+ command = string(line, 0, p);
+ if (p != string::npos) arg = removeWhitespace(string(line, p));
+ } else {
+ arg = line;
+ }
+
+ if (command == ":?" || command == ":help") {
+ cout << "The following commands are available:\n"
+ << "\n"
+ << " <expr> Evaluate and print expression\n"
+ << " <x> = <expr> Bind expression to variable\n"
+ << " :a <expr> Add attributes from resulting set to scope\n"
+ << " :b <expr> Build derivation\n"
+ << " :i <expr> Build derivation, then install result into current profile\n"
+ << " :l <path> Load Nix expression and add it to scope\n"
+ << " :p <expr> Evaluate and print expression recursively\n"
+ << " :q Exit nix-repl\n"
+ << " :r Reload all files\n"
+ << " :s <expr> Build dependencies of derivation, then start nix-shell\n"
+ << " :t <expr> Describe result of evaluation\n"
+ << " :u <expr> Build derivation, then start nix-shell\n";
+ }
+
+ 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 == ":r" || command == ":reload") {
+ state.resetFileCache();
+ reloadFiles();
+ }
+
+ else if (command == ":t") {
+ Value v;
+ evalString(arg, v);
+ std::cout << showType(v) << std::endl;
+
+ } else if (command == ":u") {
+ Value v, f, result;
+ evalString(arg, v);
+ evalString("drv: (import <nixpkgs> {}).runCommand \"shell\" { buildInputs = [ drv ]; } \"\"", f);
+ state.callFunction(f, v, result, Pos());
+
+ Path drvPath = getDerivationPath(result);
+ runProgram("nix-shell", Strings{drvPath});
+ }
+
+ else if (command == ":b" || command == ":i" || command == ":s") {
+ Value v;
+ evalString(arg, v);
+ Path drvPath = getDerivationPath(v);
+
+ if (command == ":b") {
+ /* We could do the build in this process using buildPaths(),
+ but doing it in a child makes it easier to recover from
+ problems / SIGINT. */
+ if (runProgram("nix-store", Strings{"-r", drvPath}) == 0) {
+ Derivation drv = readDerivation(drvPath);
+ std::cout << std::endl << "this derivation produced the following outputs:" << std::endl;
+ for (auto & i : drv.outputs)
+ std::cout << format(" %1% -> %2%") % i.first % i.second.path << std::endl;
+ }
+ } else if (command == ":i") {
+ runProgram("nix-env", Strings{"-i", drvPath});
+ } else {
+ runProgram("nix-shell", Strings{drvPath});
+ }
+ }
+
+ else if (command == ":p" || command == ":print") {
+ Value v;
+ evalString(arg, v);
+ printValue(std::cout, v, 1000000000) << std::endl;
+ }
+
+ else if (command == ":q" || command == ":quit")
+ return false;
+
+ else if (command != "")
+ throw Error(format("unknown command ‘%1%’") % command);
+
+ else {
+ size_t p = line.find('=');
+ string name;
+ if (p != string::npos &&
+ p < line.size() &&
+ line[p + 1] != '=' &&
+ isVarName(name = removeWhitespace(string(line, 0, p))))
+ {
+ Expr * e = parseString(string(line, p + 1));
+ Value & v(*state.allocValue());
+ v.type = tThunk;
+ v.thunk.env = env;
+ v.thunk.expr = e;
+ addVarToScope(state.symbols.create(name), v);
+ } else {
+ Value v;
+ evalString(line, v);
+ printValue(std::cout, v, 1) << std::endl;
+ }
+ }
+
+ return true;
+}
+
+
+void NixRepl::loadFile(const Path & path)
+{
+ loadedFiles.remove(path);
+ loadedFiles.push_back(path);
+ Value v, v2;
+ state.evalFile(lookupFileArg(state, path), v);
+ Bindings & bindings(*state.allocBindings(0));
+ state.autoCallFunction(bindings, v, v2);
+ addAttrsToScope(v2);
+}
+
+
+void NixRepl::initEnv()
+{
+ env = &state.allocEnv(envSize);
+ env->up = &state.baseEnv;
+ displ = 0;
+ staticEnv.vars.clear();
+
+ varNames.clear();
+ for (auto & i : state.staticBaseEnv.vars)
+ varNames.insert(i.first);
+}
+
+
+void NixRepl::reloadFiles()
+{
+ initEnv();
+
+ Strings old = loadedFiles;
+ loadedFiles.clear();
+
+ bool first = true;
+ for (auto & i : old) {
+ if (!first) std::cout << std::endl;
+ first = false;
+ std::cout << format("Loading ‘%1%’...") % i << std::endl;
+ loadFile(i);
+ }
+}
+
+
+void NixRepl::addAttrsToScope(Value & attrs)
+{
+ state.forceAttrs(attrs);
+ for (auto & i : *attrs.attrs)
+ addVarToScope(i.name, *i.value);
+ std::cout << format("Added %1% variables.") % attrs.attrs->size() << std::endl;
+}
+
+
+void NixRepl::addVarToScope(const Symbol & name, Value & v)
+{
+ if (displ >= envSize)
+ throw Error("environment full; cannot add more variables");
+ staticEnv.vars[name] = displ;
+ env->values[displ++] = &v;
+ varNames.insert((string) name);
+}
+
+
+Expr * NixRepl::parseString(string s)
+{
+ Expr * e = state.parseExprFromString(s, curDir, staticEnv);
+ return e;
+}
+
+
+void NixRepl::evalString(string s, Value & v)
+{
+ Expr * e = parseString(s);
+ e->eval(state, *env, v);
+ state.forceValue(v);
+}
+
+
+std::ostream & NixRepl::printValue(std::ostream & str, Value & v, unsigned int maxDepth)
+{
+ ValuesSeen seen;
+ return printValue(str, v, maxDepth, seen);
+}
+
+
+std::ostream & printStringValue(std::ostream & str, const char * string) {
+ str << "\"";
+ for (const char * i = string; *i; i++)
+ if (*i == '\"' || *i == '\\') str << "\\" << *i;
+ else if (*i == '\n') str << "\\n";
+ else if (*i == '\r') str << "\\r";
+ else if (*i == '\t') str << "\\t";
+ else str << *i;
+ str << "\"";
+ return str;
+}
+
+
+// FIXME: lot of cut&paste from Nix's eval.cc.
+std::ostream & NixRepl::printValue(std::ostream & str, Value & v, unsigned int maxDepth, ValuesSeen & seen)
+{
+ str.flush();
+ checkInterrupt();
+
+ state.forceValue(v);
+
+ switch (v.type) {
+
+ case tInt:
+ str << ESC_CYA << v.integer << ESC_END;
+ break;
+
+ case tBool:
+ str << ESC_CYA << (v.boolean ? "true" : "false") << ESC_END;
+ break;
+
+ case tString:
+ str << ESC_YEL;
+ printStringValue(str, v.string.s);
+ str << ESC_END;
+ break;
+
+ case tPath:
+ str << ESC_GRE << v.path << ESC_END; // !!! escaping?
+ break;
+
+ case tNull:
+ str << ESC_CYA "null" ESC_END;
+ break;
+
+ case tAttrs: {
+ seen.insert(&v);
+
+ bool isDrv = state.isDerivation(v);
+
+ if (isDrv) {
+ str << "«derivation ";
+ Bindings::iterator i = v.attrs->find(state.sDrvPath);
+ PathSet context;
+ Path drvPath = i != v.attrs->end() ? state.coerceToPath(*i->pos, *i->value, context) : "???";
+ str << drvPath << "»";
+ }
+
+ else if (maxDepth > 0) {
+ str << "{ ";
+
+ typedef std::map<string, Value *> Sorted;
+ Sorted sorted;
+ for (auto & i : *v.attrs)
+ sorted[i.name] = i.value;
+
+ /* If this is a derivation, then don't show the
+ self-references ("all", "out", etc.). */
+ StringSet hidden;
+ if (isDrv) {
+ hidden.insert("all");
+ Bindings::iterator i = v.attrs->find(state.sOutputs);
+ if (i == v.attrs->end())
+ hidden.insert("out");
+ else {
+ state.forceList(*i->value);
+ for (unsigned int j = 0; j < i->value->listSize(); ++j)
+ hidden.insert(state.forceStringNoCtx(*i->value->listElems()[j]));
+ }
+ }
+
+ for (auto & i : sorted) {
+ if (isVarName(i.first))
+ str << i.first;
+ else
+ printStringValue(str, i.first.c_str());
+ str << " = ";
+ if (hidden.find(i.first) != hidden.end())
+ str << "«...»";
+ else if (seen.find(i.second) != seen.end())
+ str << "«repeated»";
+ else
+ try {
+ printValue(str, *i.second, maxDepth - 1, seen);
+ } catch (AssertionError & e) {
+ str << ESC_RED "«error: " << e.msg() << "»" ESC_END;
+ }
+ str << "; ";
+ }
+
+ str << "}";
+ } else
+ str << "{ ... }";
+
+ break;
+ }
+
+ case tList1:
+ case tList2:
+ case tListN:
+ seen.insert(&v);
+
+ str << "[ ";
+ if (maxDepth > 0)
+ for (unsigned int n = 0; n < v.listSize(); ++n) {
+ if (seen.find(v.listElems()[n]) != seen.end())
+ str << "«repeated»";
+ else
+ try {
+ printValue(str, *v.listElems()[n], maxDepth - 1, seen);
+ } catch (AssertionError & e) {
+ str << ESC_RED "«error: " << e.msg() << "»" ESC_END;
+ }
+ str << " ";
+ }
+ else
+ str << "... ";
+ str << "]";
+ break;
+
+ case tLambda: {
+ std::ostringstream s;
+ s << v.lambda.fun->pos;
+ str << ESC_BLU "«lambda @ " << filterANSIEscapes(s.str()) << "»" ESC_END;
+ break;
+ }
+
+ case tPrimOp:
+ str << ESC_MAG "«primop»" ESC_END;
+ break;
+
+ case tPrimOpApp:
+ str << ESC_BLU "«primop-app»" ESC_END;
+ break;
+
+ default:
+ str << ESC_RED "«unknown»" ESC_END;
+ break;
+ }
+
+ return str;
+}
+
+
+int main(int argc, char * * argv)
+{
+ return handleExceptions(argv[0], [&]() {
+ initNix();
+ initGC();
+
+ Strings files, searchPath;
+
+ parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
+ if (*arg == "--version")
+ printVersion("nix-repl");
+ else if (*arg == "--help") {
+ printHelp();
+ // exit with 0 since user asked for help
+ _exit(0);
+ }
+ else if (parseSearchPathArg(arg, end, searchPath))
+ ;
+ else if (*arg != "" && arg->at(0) == '-')
+ return false;
+ else
+ files.push_back(*arg);
+ return true;
+ });
+
+ NixRepl repl(searchPath, openStore());
+ repl.mainLoop(files);
+
+ write_history(historyFile.c_str());
+ });
+}