#include // for nix/globals.hh's reference to SYSTEM #include // for exception_ptr, current_exception #include // for function #include // for operator<<, basic_ostream, ostrin... #include // for next #include // for _List_iterator #include // for allocator, unique_ptr, make_unique #include // for operator new #include // for argvToStrings, UsageError #include // for findAlongAttrPath #include // for Attr, Bindings, Bindings::iterator #include // for MixEvalArgs #include // for EvalState::forceValue #include // for EvalState, initGC, operator<< #include // for initPlugins, Settings, settings #include // for Pos #include // for getArg, LegacyArgs, printVersion #include // for openStore #include // for Symbol, SymbolTable #include // for Error, Path, Strings, PathSet #include // for absPath, baseNameOf #include // for Value, Value::(anonymous), Value:... #include // for string, operator+, operator== #include // for move #include // for get, holds_alternative, variant #include // for vector<>::iterator, vector #include "libnix-copy-paste.hh" using nix::absPath; using nix::Bindings; using nix::Error; using nix::EvalError; using nix::EvalState; using nix::Path; using nix::PathSet; using nix::Strings; using nix::Symbol; using nix::tAttrs; using nix::ThrownError; using nix::tLambda; using nix::tString; using nix::UsageError; using nix::Value; // An ostream wrapper to handle nested indentation class Out { public: class Separator {}; const static Separator sep; enum LinePolicy { ONE_LINE, MULTI_LINE }; explicit Out(std::ostream & ostream) : ostream(ostream), policy(ONE_LINE), writeSinceSep(true) {} Out(Out & o, const std::string & start, const std::string & end, LinePolicy policy); Out(Out & o, const std::string & start, const std::string & end, int count) : Out(o, start, end, count < 2 ? ONE_LINE : MULTI_LINE) {} Out(const Out &) = delete; Out(Out &&) = default; Out & operator=(const Out &) = delete; Out & operator=(Out &&) = delete; ~Out() { ostream << end; } private: std::ostream & ostream; std::string indentation; std::string end; LinePolicy policy; bool writeSinceSep; template friend Out & operator<<(Out & o, T thing); }; template Out & operator<<(Out & o, T thing) { if (!o.writeSinceSep && o.policy == Out::MULTI_LINE) { o.ostream << o.indentation; } o.writeSinceSep = true; o.ostream << thing; return o; } template <> Out & operator<<(Out & o, Out::Separator /* thing */) { o.ostream << (o.policy == Out::ONE_LINE ? " " : "\n"); o.writeSinceSep = false; return o; } Out::Out(Out & o, const std::string & start, const std::string & end, LinePolicy policy) : ostream(o.ostream), indentation(policy == ONE_LINE ? o.indentation : o.indentation + " "), end(policy == ONE_LINE ? end : o.indentation + end), policy(policy), writeSinceSep(true) { o << start; *this << Out::sep; } // Stuff needed for evaluation struct Context { Context(EvalState & state, Bindings & autoArgs, Value optionsRoot, Value configRoot) : state(state), autoArgs(autoArgs), optionsRoot(optionsRoot), configRoot(configRoot), underscoreType(state.symbols.create("_type")) {} EvalState & state; Bindings & autoArgs; Value optionsRoot; Value configRoot; Symbol underscoreType; }; Value evaluateValue(Context & ctx, Value & v) { ctx.state.forceValue(v); if (ctx.autoArgs.empty()) { return v; } Value called{}; ctx.state.autoCallFunction(ctx.autoArgs, v, called); return called; } bool isOption(Context & ctx, const Value & v) { if (v.type != tAttrs) { return false; } const auto & actualType = v.attrs->find(ctx.underscoreType); if (actualType == v.attrs->end()) { return false; } try { Value evaluatedType = evaluateValue(ctx, *actualType->value); if (evaluatedType.type != tString) { return false; } return static_cast(evaluatedType.string.s) == "option"; } catch (Error &) { return false; } } // Add quotes to a component of a path. // These are needed for paths like: // fileSystems."/".fsType // systemd.units."dbus.service".text std::string quoteAttribute(const std::string & attribute) { if (isVarName(attribute)) { return attribute; } std::ostringstream buf; printStringValue(buf, attribute.c_str()); return buf.str(); } const std::string appendPath(const std::string & prefix, const std::string & suffix) { if (prefix.empty()) { return quoteAttribute(suffix); } return prefix + "." + quoteAttribute(suffix); } bool forbiddenRecursionName(std::string name) { return (!name.empty() && name[0] == '_') || name == "haskellPackages"; } void recurse(const std::function)> & f, Context & ctx, Value v, const std::string & path) { std::variant evaluated; try { evaluated = evaluateValue(ctx, v); } catch (Error &) { evaluated = std::current_exception(); } if (!f(path, evaluated)) { return; } if (std::holds_alternative(evaluated)) { return; } const Value & evaluated_value = std::get(evaluated); if (evaluated_value.type != tAttrs) { return; } for (const auto & child : evaluated_value.attrs->lexicographicOrder()) { if (forbiddenRecursionName(child->name)) { continue; } recurse(f, ctx, *child->value, appendPath(path, child->name)); } } bool optionTypeIs(Context & ctx, Value & v, const std::string & soughtType) { try { const auto & typeLookup = v.attrs->find(ctx.state.sType); if (typeLookup == v.attrs->end()) { return false; } Value type = evaluateValue(ctx, *typeLookup->value); if (type.type != tAttrs) { return false; } const auto & nameLookup = type.attrs->find(ctx.state.sName); if (nameLookup == type.attrs->end()) { return false; } Value name = evaluateValue(ctx, *nameLookup->value); if (name.type != tString) { return false; } return name.string.s == soughtType; } catch (Error &) { return false; } } bool isAggregateOptionType(Context & ctx, Value & v) { return optionTypeIs(ctx, v, "attrsOf") || optionTypeIs(ctx, v, "listOf"); } MakeError(OptionPathError, EvalError); Value getSubOptions(Context & ctx, Value & option) { Value getSubOptions = evaluateValue(ctx, *findAlongAttrPath(ctx.state, "type.getSubOptions", ctx.autoArgs, option)); if (getSubOptions.type != tLambda) { throw OptionPathError("Option's type.getSubOptions isn't a function"); } Value emptyString{}; nix::mkString(emptyString, ""); Value v; ctx.state.callFunction(getSubOptions, emptyString, v, nix::Pos{}); return v; } // Carefully walk an option path, looking for sub-options when a path walks past // an option value. struct FindAlongOptionPathRet { Value option; std::string path; }; FindAlongOptionPathRet findAlongOptionPath(Context & ctx, const std::string & path) { Strings tokens = parseAttrPath(path); Value v = ctx.optionsRoot; std::string processedPath; for (auto i = tokens.begin(); i != tokens.end(); i++) { const auto & attr = *i; try { bool lastAttribute = std::next(i) == tokens.end(); v = evaluateValue(ctx, v); if (attr.empty()) { throw OptionPathError("empty attribute name"); } if (isOption(ctx, v) && optionTypeIs(ctx, v, "submodule")) { v = getSubOptions(ctx, v); } if (isOption(ctx, v) && isAggregateOptionType(ctx, v)) { auto subOptions = getSubOptions(ctx, v); if (lastAttribute && subOptions.attrs->empty()) { break; } v = subOptions; // Note that we've consumed attr, but didn't actually use it. This is the path component that's looked // up in the list or attribute set that doesn't name an option -- the "root" in "users.users.root.name". } else if (v.type != tAttrs) { throw OptionPathError("Value is %s while a set was expected", showType(v)); } else { const auto & next = v.attrs->find(ctx.state.symbols.create(attr)); if (next == v.attrs->end()) { throw OptionPathError("Attribute not found", attr, path); } v = *next->value; } processedPath = appendPath(processedPath, attr); } catch (OptionPathError & e) { throw OptionPathError("At '%s' in path '%s': %s", attr, path, e.msg()); } } return {v, processedPath}; } // Calls f on all the option names at or below the option described by `path`. // Note that "the option described by `path`" is not trivial -- if path describes a value inside an aggregate // option (such as users.users.root), the *option* described by that path is one path component shorter // (eg: users.users), which results in f being called on sibling-paths (eg: users.users.nixbld1). If f // doesn't want these, it must do its own filtering. void mapOptions(const std::function & f, Context & ctx, const std::string & path) { auto root = findAlongOptionPath(ctx, path); recurse( [f, &ctx](const std::string & path, std::variant v) { bool isOpt = std::holds_alternative(v) || isOption(ctx, std::get(v)); if (isOpt) { f(path); } return !isOpt; }, ctx, root.option, root.path); } // Calls f on all the config values inside one option. // Simple options have one config value inside, like sound.enable = true. // Compound options have multiple config values. For example, the option // "users.users" has about 1000 config values inside it: // users.users.avahi.createHome = false; // users.users.avahi.cryptHomeLuks = null; // users.users.avahi.description = "`avahi-daemon' privilege separation user"; // ... // users.users.avahi.openssh.authorizedKeys.keyFiles = [ ]; // users.users.avahi.openssh.authorizedKeys.keys = [ ]; // ... // users.users.avahi.uid = 10; // users.users.avahi.useDefaultShell = false; // users.users.cups.createHome = false; // ... // users.users.cups.useDefaultShell = false; // users.users.gdm = ... ... ... // users.users.messagebus = ... .. ... // users.users.nixbld1 = ... .. ... // ... // users.users.systemd-timesync = ... .. ... void mapConfigValuesInOption( const std::function v)> & f, const std::string & path, Context & ctx) { Value * option; try { option = findAlongAttrPath(ctx.state, path, ctx.autoArgs, ctx.configRoot); } catch (Error &) { f(path, std::current_exception()); return; } recurse( [f, ctx](const std::string & path, std::variant v) { bool leaf = std::holds_alternative(v) || std::get(v).type != tAttrs || ctx.state.isDerivation(std::get(v)); if (!leaf) { return true; // Keep digging } f(path, v); return false; }, ctx, *option, path); } std::string describeError(const Error & e) { return "«error: " + e.msg() + "»"; } void describeDerivation(Context & ctx, Out & out, Value v) { // Copy-pasted from nix/src/nix/repl.cc :( Bindings::iterator i = v.attrs->find(ctx.state.sDrvPath); PathSet pathset; try { Path drvPath = i != v.attrs->end() ? ctx.state.coerceToPath(*i->pos, *i->value, pathset) : "???"; out << "«derivation " << drvPath << "»"; } catch (Error & e) { out << describeError(e); } } Value parseAndEval(EvalState & state, const std::string & expression, const std::string & path) { Value v{}; state.eval(state.parseExprFromString(expression, absPath(path)), v); return v; } void printValue(Context & ctx, Out & out, std::variant maybeValue, const std::string & path); void printList(Context & ctx, Out & out, Value & v) { Out listOut(out, "[", "]", v.listSize()); for (unsigned int n = 0; n < v.listSize(); ++n) { printValue(ctx, listOut, *v.listElems()[n], ""); listOut << Out::sep; } } void printAttrs(Context & ctx, Out & out, Value & v, const std::string & path) { Out attrsOut(out, "{", "}", v.attrs->size()); for (const auto & a : v.attrs->lexicographicOrder()) { std::string name = a->name; if (!forbiddenRecursionName(name)) { attrsOut << name << " = "; printValue(ctx, attrsOut, *a->value, appendPath(path, name)); attrsOut << ";" << Out::sep; } } } void multiLineStringEscape(Out & out, const std::string & s) { int i; for (i = 1; i < s.size(); i++) { if (s[i - 1] == '$' && s[i] == '{') { out << "''${"; i++; } else if (s[i - 1] == '\'' && s[i] == '\'') { out << "'''"; i++; } else { out << s[i - 1]; } } if (i == s.size()) { out << s[i - 1]; } } void printMultiLineString(Out & out, const Value & v) { std::string s = v.string.s; Out strOut(out, "''", "''", Out::MULTI_LINE); std::string::size_type begin = 0; while (begin < s.size()) { std::string::size_type end = s.find('\n', begin); if (end == std::string::npos) { multiLineStringEscape(strOut, s.substr(begin, s.size() - begin)); break; } multiLineStringEscape(strOut, s.substr(begin, end - begin)); strOut << Out::sep; begin = end + 1; } } void printValue(Context & ctx, Out & out, std::variant maybeValue, const std::string & path) { try { if (auto ex = std::get_if(&maybeValue)) { std::rethrow_exception(*ex); } Value v = evaluateValue(ctx, std::get(maybeValue)); if (ctx.state.isDerivation(v)) { describeDerivation(ctx, out, v); } else if (v.isList()) { printList(ctx, out, v); } else if (v.type == tAttrs) { printAttrs(ctx, out, v, path); } else if (v.type == tString && std::string(v.string.s).find('\n') != std::string::npos) { printMultiLineString(out, v); } else { ctx.state.forceValueDeep(v); out << v; } } catch (ThrownError & e) { if (e.msg() == "The option `" + path + "' is used but not defined.") { // 93% of errors are this, and just letting this message through would be // misleading. These values may or may not actually be "used" in the // config. The thing throwing the error message assumes that if anything // ever looks at this value, it is a "use" of this value. But here in // nixos-option, we are looking at this value only to print it. // In order to avoid implying that this undefined value is actually // referenced, eat the underlying error message and emit "«not defined»". out << "«not defined»"; } else { out << describeError(e); } } catch (Error & e) { out << describeError(e); } } void printConfigValue(Context & ctx, Out & out, const std::string & path, std::variant v) { out << path << " = "; printValue(ctx, out, std::move(v), path); out << ";\n"; } // Replace with std::starts_with when C++20 is available bool starts_with(const std::string & s, const std::string & prefix) { return s.size() >= prefix.size() && std::equal(s.begin(), std::next(s.begin(), prefix.size()), prefix.begin(), prefix.end()); } void printRecursive(Context & ctx, Out & out, const std::string & path) { mapOptions( [&ctx, &out, &path](const std::string & optionPath) { mapConfigValuesInOption( [&ctx, &out, &path](const std::string & configPath, std::variant v) { if (starts_with(configPath, path)) { printConfigValue(ctx, out, configPath, v); } }, optionPath, ctx); }, ctx, path); } void printAttr(Context & ctx, Out & out, const std::string & path, Value & root) { try { printValue(ctx, out, *findAlongAttrPath(ctx.state, path, ctx.autoArgs, root), path); } catch (Error & e) { out << describeError(e); } } bool hasExample(Context & ctx, Value & option) { try { findAlongAttrPath(ctx.state, "example", ctx.autoArgs, option); return true; } catch (Error &) { return false; } } void printOption(Context & ctx, Out & out, const std::string & path, Value & option) { out << "Value:\n"; printAttr(ctx, out, path, ctx.configRoot); out << "\n\nDefault:\n"; printAttr(ctx, out, "default", option); out << "\n\nType:\n"; printAttr(ctx, out, "type.description", option); if (hasExample(ctx, option)) { out << "\n\nExample:\n"; printAttr(ctx, out, "example", option); } out << "\n\nDescription:\n"; printAttr(ctx, out, "description", option); out << "\n\nDeclared by:\n"; printAttr(ctx, out, "declarations", option); out << "\n\nDefined by:\n"; printAttr(ctx, out, "files", option); out << "\n"; } void printListing(Out & out, Value & v) { out << "This attribute set contains:\n"; for (const auto & a : v.attrs->lexicographicOrder()) { std::string name = a->name; if (!name.empty() && name[0] != '_') { out << name << "\n"; } } } void printOne(Context & ctx, Out & out, const std::string & path) { try { auto result = findAlongOptionPath(ctx, path); Value & option = result.option; option = evaluateValue(ctx, option); if (path != result.path) { out << "Note: showing " << result.path << " instead of " << path << "\n"; } if (isOption(ctx, option)) { printOption(ctx, out, result.path, option); } else { printListing(out, option); } } catch (Error & e) { std::cerr << "error: " << e.msg() << "\nAn error occurred while looking for attribute names. Are " "you sure that '" << path << "' exists?\n"; } } int main(int argc, char ** argv) { bool recursive = false; std::string path = "."; std::string optionsExpr = "(import {}).options"; std::string configExpr = "(import {}).config"; std::vector args; struct MyArgs : nix::LegacyArgs, nix::MixEvalArgs { using nix::LegacyArgs::LegacyArgs; }; MyArgs myArgs(nix::baseNameOf(argv[0]), [&](Strings::iterator & arg, const Strings::iterator & end) { if (*arg == "--help") { nix::showManPage("nixos-option"); } else if (*arg == "--version") { nix::printVersion("nixos-option"); } else if (*arg == "-r" || *arg == "--recursive") { recursive = true; } else if (*arg == "--path") { path = nix::getArg(*arg, arg, end); } else if (*arg == "--options_expr") { optionsExpr = nix::getArg(*arg, arg, end); } else if (*arg == "--config_expr") { configExpr = nix::getArg(*arg, arg, end); } else if (!arg->empty() && arg->at(0) == '-') { return false; } else { args.push_back(*arg); } return true; }); myArgs.parseCmdline(nix::argvToStrings(argc, argv)); nix::initPlugins(); nix::initGC(); nix::settings.readOnlyMode = true; auto store = nix::openStore(); auto state = std::make_unique(myArgs.searchPath, store); Value optionsRoot = parseAndEval(*state, optionsExpr, path); Value configRoot = parseAndEval(*state, configExpr, path); Context ctx{*state, *myArgs.getAutoArgs(*state), optionsRoot, configRoot}; Out out(std::cout); auto print = recursive ? printRecursive : printOne; if (args.empty()) { print(ctx, out, ""); } for (const auto & arg : args) { print(ctx, out, arg); } ctx.state.printStats(); return 0; }