1#include <nix/config.h> // for nix/globals.hh's reference to SYSTEM
2
3#include <exception> // for exception_ptr, current_exception
4#include <functional> // for function
5#include <iostream> // for operator<<, basic_ostream, ostrin...
6#include <iterator> // for next
7#include <list> // for _List_iterator
8#include <memory> // for allocator, unique_ptr, make_unique
9#include <new> // for operator new
10#include <nix/args.hh> // for argvToStrings, UsageError
11#include <nix/attr-path.hh> // for findAlongAttrPath
12#include <nix/attr-set.hh> // for Attr, Bindings, Bindings::iterator
13#include <nix/common-eval-args.hh> // for MixEvalArgs
14#include <nix/eval-inline.hh> // for EvalState::forceValue
15#include <nix/eval.hh> // for EvalState, initGC, operator<<
16#include <nix/globals.hh> // for initPlugins, Settings, settings
17#include <nix/nixexpr.hh> // for Pos
18#include <nix/shared.hh> // for getArg, LegacyArgs, printVersion
19#include <nix/store-api.hh> // for openStore
20#include <nix/symbol-table.hh> // for Symbol, SymbolTable
21#include <nix/types.hh> // for Error, Path, Strings, PathSet
22#include <nix/util.hh> // for absPath, baseNameOf
23#include <nix/value.hh> // for Value, Value::(anonymous), Value:...
24#include <string> // for string, operator+, operator==
25#include <utility> // for move
26#include <variant> // for get, holds_alternative, variant
27#include <vector> // for vector<>::iterator, vector
28
29#include "libnix-copy-paste.hh"
30
31using nix::absPath;
32using nix::Bindings;
33using nix::Error;
34using nix::EvalError;
35using nix::EvalState;
36using nix::Path;
37using nix::PathSet;
38using nix::Strings;
39using nix::Symbol;
40using nix::tAttrs;
41using nix::ThrownError;
42using nix::tLambda;
43using nix::tString;
44using nix::UsageError;
45using nix::Value;
46
47// An ostream wrapper to handle nested indentation
48class Out
49{
50 public:
51 class Separator
52 {};
53 const static Separator sep;
54 enum LinePolicy
55 {
56 ONE_LINE,
57 MULTI_LINE
58 };
59 explicit Out(std::ostream & ostream) : ostream(ostream), policy(ONE_LINE), writeSinceSep(true) {}
60 Out(Out & o, const std::string & start, const std::string & end, LinePolicy policy);
61 Out(Out & o, const std::string & start, const std::string & end, int count)
62 : Out(o, start, end, count < 2 ? ONE_LINE : MULTI_LINE)
63 {}
64 Out(const Out &) = delete;
65 Out(Out &&) = default;
66 Out & operator=(const Out &) = delete;
67 Out & operator=(Out &&) = delete;
68 ~Out() { ostream << end; }
69
70 private:
71 std::ostream & ostream;
72 std::string indentation;
73 std::string end;
74 LinePolicy policy;
75 bool writeSinceSep;
76 template <typename T> friend Out & operator<<(Out & o, T thing);
77};
78
79template <typename T> Out & operator<<(Out & o, T thing)
80{
81 if (!o.writeSinceSep && o.policy == Out::MULTI_LINE) {
82 o.ostream << o.indentation;
83 }
84 o.writeSinceSep = true;
85 o.ostream << thing;
86 return o;
87}
88
89template <> Out & operator<<<Out::Separator>(Out & o, Out::Separator /* thing */)
90{
91 o.ostream << (o.policy == Out::ONE_LINE ? " " : "\n");
92 o.writeSinceSep = false;
93 return o;
94}
95
96Out::Out(Out & o, const std::string & start, const std::string & end, LinePolicy policy)
97 : ostream(o.ostream), indentation(policy == ONE_LINE ? o.indentation : o.indentation + " "),
98 end(policy == ONE_LINE ? end : o.indentation + end), policy(policy), writeSinceSep(true)
99{
100 o << start;
101 *this << Out::sep;
102}
103
104// Stuff needed for evaluation
105struct Context
106{
107 Context(EvalState & state, Bindings & autoArgs, Value optionsRoot, Value configRoot)
108 : state(state), autoArgs(autoArgs), optionsRoot(optionsRoot), configRoot(configRoot),
109 underscoreType(state.symbols.create("_type"))
110 {}
111 EvalState & state;
112 Bindings & autoArgs;
113 Value optionsRoot;
114 Value configRoot;
115 Symbol underscoreType;
116};
117
118Value evaluateValue(Context & ctx, Value & v)
119{
120 ctx.state.forceValue(v);
121 if (ctx.autoArgs.empty()) {
122 return v;
123 }
124 Value called{};
125 ctx.state.autoCallFunction(ctx.autoArgs, v, called);
126 return called;
127}
128
129bool isOption(Context & ctx, const Value & v)
130{
131 if (v.type != tAttrs) {
132 return false;
133 }
134 const auto & actualType = v.attrs->find(ctx.underscoreType);
135 if (actualType == v.attrs->end()) {
136 return false;
137 }
138 try {
139 Value evaluatedType = evaluateValue(ctx, *actualType->value);
140 if (evaluatedType.type != tString) {
141 return false;
142 }
143 return static_cast<std::string>(evaluatedType.string.s) == "option";
144 } catch (Error &) {
145 return false;
146 }
147}
148
149// Add quotes to a component of a path.
150// These are needed for paths like:
151// fileSystems."/".fsType
152// systemd.units."dbus.service".text
153std::string quoteAttribute(const std::string & attribute)
154{
155 if (isVarName(attribute)) {
156 return attribute;
157 }
158 std::ostringstream buf;
159 printStringValue(buf, attribute.c_str());
160 return buf.str();
161}
162
163const std::string appendPath(const std::string & prefix, const std::string & suffix)
164{
165 if (prefix.empty()) {
166 return quoteAttribute(suffix);
167 }
168 return prefix + "." + quoteAttribute(suffix);
169}
170
171bool forbiddenRecursionName(std::string name) { return (!name.empty() && name[0] == '_') || name == "haskellPackages"; }
172
173void recurse(const std::function<bool(const std::string & path, std::variant<Value, std::exception_ptr>)> & f,
174 Context & ctx, Value v, const std::string & path)
175{
176 std::variant<Value, std::exception_ptr> evaluated;
177 try {
178 evaluated = evaluateValue(ctx, v);
179 } catch (Error &) {
180 evaluated = std::current_exception();
181 }
182 if (!f(path, evaluated)) {
183 return;
184 }
185 if (std::holds_alternative<std::exception_ptr>(evaluated)) {
186 return;
187 }
188 const Value & evaluated_value = std::get<Value>(evaluated);
189 if (evaluated_value.type != tAttrs) {
190 return;
191 }
192 for (const auto & child : evaluated_value.attrs->lexicographicOrder()) {
193 if (forbiddenRecursionName(child->name)) {
194 continue;
195 }
196 recurse(f, ctx, *child->value, appendPath(path, child->name));
197 }
198}
199
200bool optionTypeIs(Context & ctx, Value & v, const std::string & soughtType)
201{
202 try {
203 const auto & typeLookup = v.attrs->find(ctx.state.sType);
204 if (typeLookup == v.attrs->end()) {
205 return false;
206 }
207 Value type = evaluateValue(ctx, *typeLookup->value);
208 if (type.type != tAttrs) {
209 return false;
210 }
211 const auto & nameLookup = type.attrs->find(ctx.state.sName);
212 if (nameLookup == type.attrs->end()) {
213 return false;
214 }
215 Value name = evaluateValue(ctx, *nameLookup->value);
216 if (name.type != tString) {
217 return false;
218 }
219 return name.string.s == soughtType;
220 } catch (Error &) {
221 return false;
222 }
223}
224
225bool isAggregateOptionType(Context & ctx, Value & v)
226{
227 return optionTypeIs(ctx, v, "attrsOf") || optionTypeIs(ctx, v, "listOf");
228}
229
230MakeError(OptionPathError, EvalError);
231
232Value getSubOptions(Context & ctx, Value & option)
233{
234 Value getSubOptions = evaluateValue(ctx, *findAlongAttrPath(ctx.state, "type.getSubOptions", ctx.autoArgs, option));
235 if (getSubOptions.type != tLambda) {
236 throw OptionPathError("Option's type.getSubOptions isn't a function");
237 }
238 Value emptyString{};
239 nix::mkString(emptyString, "");
240 Value v;
241 ctx.state.callFunction(getSubOptions, emptyString, v, nix::Pos{});
242 return v;
243}
244
245// Carefully walk an option path, looking for sub-options when a path walks past
246// an option value.
247struct FindAlongOptionPathRet
248{
249 Value option;
250 std::string path;
251};
252FindAlongOptionPathRet findAlongOptionPath(Context & ctx, const std::string & path)
253{
254 Strings tokens = parseAttrPath(path);
255 Value v = ctx.optionsRoot;
256 std::string processedPath;
257 for (auto i = tokens.begin(); i != tokens.end(); i++) {
258 const auto & attr = *i;
259 try {
260 bool lastAttribute = std::next(i) == tokens.end();
261 v = evaluateValue(ctx, v);
262 if (attr.empty()) {
263 throw OptionPathError("empty attribute name");
264 }
265 if (isOption(ctx, v) && optionTypeIs(ctx, v, "submodule")) {
266 v = getSubOptions(ctx, v);
267 }
268 if (isOption(ctx, v) && isAggregateOptionType(ctx, v)) {
269 auto subOptions = getSubOptions(ctx, v);
270 if (lastAttribute && subOptions.attrs->empty()) {
271 break;
272 }
273 v = subOptions;
274 // Note that we've consumed attr, but didn't actually use it. This is the path component that's looked
275 // up in the list or attribute set that doesn't name an option -- the "root" in "users.users.root.name".
276 } else if (v.type != tAttrs) {
277 throw OptionPathError("Value is %s while a set was expected", showType(v));
278 } else {
279 const auto & next = v.attrs->find(ctx.state.symbols.create(attr));
280 if (next == v.attrs->end()) {
281 throw OptionPathError("Attribute not found", attr, path);
282 }
283 v = *next->value;
284 }
285 processedPath = appendPath(processedPath, attr);
286 } catch (OptionPathError & e) {
287 throw OptionPathError("At '%s' in path '%s': %s", attr, path, e.msg());
288 }
289 }
290 return {v, processedPath};
291}
292
293// Calls f on all the option names at or below the option described by `path`.
294// Note that "the option described by `path`" is not trivial -- if path describes a value inside an aggregate
295// option (such as users.users.root), the *option* described by that path is one path component shorter
296// (eg: users.users), which results in f being called on sibling-paths (eg: users.users.nixbld1). If f
297// doesn't want these, it must do its own filtering.
298void mapOptions(const std::function<void(const std::string & path)> & f, Context & ctx, const std::string & path)
299{
300 auto root = findAlongOptionPath(ctx, path);
301 recurse(
302 [f, &ctx](const std::string & path, std::variant<Value, std::exception_ptr> v) {
303 bool isOpt = std::holds_alternative<std::exception_ptr>(v) || isOption(ctx, std::get<Value>(v));
304 if (isOpt) {
305 f(path);
306 }
307 return !isOpt;
308 },
309 ctx, root.option, root.path);
310}
311
312// Calls f on all the config values inside one option.
313// Simple options have one config value inside, like sound.enable = true.
314// Compound options have multiple config values. For example, the option
315// "users.users" has about 1000 config values inside it:
316// users.users.avahi.createHome = false;
317// users.users.avahi.cryptHomeLuks = null;
318// users.users.avahi.description = "`avahi-daemon' privilege separation user";
319// ...
320// users.users.avahi.openssh.authorizedKeys.keyFiles = [ ];
321// users.users.avahi.openssh.authorizedKeys.keys = [ ];
322// ...
323// users.users.avahi.uid = 10;
324// users.users.avahi.useDefaultShell = false;
325// users.users.cups.createHome = false;
326// ...
327// users.users.cups.useDefaultShell = false;
328// users.users.gdm = ... ... ...
329// users.users.messagebus = ... .. ...
330// users.users.nixbld1 = ... .. ...
331// ...
332// users.users.systemd-timesync = ... .. ...
333void mapConfigValuesInOption(
334 const std::function<void(const std::string & path, std::variant<Value, std::exception_ptr> v)> & f,
335 const std::string & path, Context & ctx)
336{
337 Value * option;
338 try {
339 option = findAlongAttrPath(ctx.state, path, ctx.autoArgs, ctx.configRoot);
340 } catch (Error &) {
341 f(path, std::current_exception());
342 return;
343 }
344 recurse(
345 [f, ctx](const std::string & path, std::variant<Value, std::exception_ptr> v) {
346 bool leaf = std::holds_alternative<std::exception_ptr>(v) || std::get<Value>(v).type != tAttrs ||
347 ctx.state.isDerivation(std::get<Value>(v));
348 if (!leaf) {
349 return true; // Keep digging
350 }
351 f(path, v);
352 return false;
353 },
354 ctx, *option, path);
355}
356
357std::string describeError(const Error & e) { return "«error: " + e.msg() + "»"; }
358
359void describeDerivation(Context & ctx, Out & out, Value v)
360{
361 // Copy-pasted from nix/src/nix/repl.cc :(
362 Bindings::iterator i = v.attrs->find(ctx.state.sDrvPath);
363 PathSet pathset;
364 try {
365 Path drvPath = i != v.attrs->end() ? ctx.state.coerceToPath(*i->pos, *i->value, pathset) : "???";
366 out << "«derivation " << drvPath << "»";
367 } catch (Error & e) {
368 out << describeError(e);
369 }
370}
371
372Value parseAndEval(EvalState & state, const std::string & expression, const std::string & path)
373{
374 Value v{};
375 state.eval(state.parseExprFromString(expression, absPath(path)), v);
376 return v;
377}
378
379void printValue(Context & ctx, Out & out, std::variant<Value, std::exception_ptr> maybeValue, const std::string & path);
380
381void printList(Context & ctx, Out & out, Value & v)
382{
383 Out listOut(out, "[", "]", v.listSize());
384 for (unsigned int n = 0; n < v.listSize(); ++n) {
385 printValue(ctx, listOut, *v.listElems()[n], "");
386 listOut << Out::sep;
387 }
388}
389
390void printAttrs(Context & ctx, Out & out, Value & v, const std::string & path)
391{
392 Out attrsOut(out, "{", "}", v.attrs->size());
393 for (const auto & a : v.attrs->lexicographicOrder()) {
394 std::string name = a->name;
395 if (!forbiddenRecursionName(name)) {
396 attrsOut << name << " = ";
397 printValue(ctx, attrsOut, *a->value, appendPath(path, name));
398 attrsOut << ";" << Out::sep;
399 }
400 }
401}
402
403void multiLineStringEscape(Out & out, const std::string & s)
404{
405 int i;
406 for (i = 1; i < s.size(); i++) {
407 if (s[i - 1] == '$' && s[i] == '{') {
408 out << "''${";
409 i++;
410 } else if (s[i - 1] == '\'' && s[i] == '\'') {
411 out << "'''";
412 i++;
413 } else {
414 out << s[i - 1];
415 }
416 }
417 if (i == s.size()) {
418 out << s[i - 1];
419 }
420}
421
422void printMultiLineString(Out & out, const Value & v)
423{
424 std::string s = v.string.s;
425 Out strOut(out, "''", "''", Out::MULTI_LINE);
426 std::string::size_type begin = 0;
427 while (begin < s.size()) {
428 std::string::size_type end = s.find('\n', begin);
429 if (end == std::string::npos) {
430 multiLineStringEscape(strOut, s.substr(begin, s.size() - begin));
431 break;
432 }
433 multiLineStringEscape(strOut, s.substr(begin, end - begin));
434 strOut << Out::sep;
435 begin = end + 1;
436 }
437}
438
439void printValue(Context & ctx, Out & out, std::variant<Value, std::exception_ptr> maybeValue, const std::string & path)
440{
441 try {
442 if (auto ex = std::get_if<std::exception_ptr>(&maybeValue)) {
443 std::rethrow_exception(*ex);
444 }
445 Value v = evaluateValue(ctx, std::get<Value>(maybeValue));
446 if (ctx.state.isDerivation(v)) {
447 describeDerivation(ctx, out, v);
448 } else if (v.isList()) {
449 printList(ctx, out, v);
450 } else if (v.type == tAttrs) {
451 printAttrs(ctx, out, v, path);
452 } else if (v.type == tString && std::string(v.string.s).find('\n') != std::string::npos) {
453 printMultiLineString(out, v);
454 } else {
455 ctx.state.forceValueDeep(v);
456 out << v;
457 }
458 } catch (ThrownError & e) {
459 if (e.msg() == "The option `" + path + "' is used but not defined.") {
460 // 93% of errors are this, and just letting this message through would be
461 // misleading. These values may or may not actually be "used" in the
462 // config. The thing throwing the error message assumes that if anything
463 // ever looks at this value, it is a "use" of this value. But here in
464 // nixos-option, we are looking at this value only to print it.
465 // In order to avoid implying that this undefined value is actually
466 // referenced, eat the underlying error message and emit "«not defined»".
467 out << "«not defined»";
468 } else {
469 out << describeError(e);
470 }
471 } catch (Error & e) {
472 out << describeError(e);
473 }
474}
475
476void printConfigValue(Context & ctx, Out & out, const std::string & path, std::variant<Value, std::exception_ptr> v)
477{
478 out << path << " = ";
479 printValue(ctx, out, std::move(v), path);
480 out << ";\n";
481}
482
483// Replace with std::starts_with when C++20 is available
484bool starts_with(const std::string & s, const std::string & prefix)
485{
486 return s.size() >= prefix.size() &&
487 std::equal(s.begin(), std::next(s.begin(), prefix.size()), prefix.begin(), prefix.end());
488}
489
490void printRecursive(Context & ctx, Out & out, const std::string & path)
491{
492 mapOptions(
493 [&ctx, &out, &path](const std::string & optionPath) {
494 mapConfigValuesInOption(
495 [&ctx, &out, &path](const std::string & configPath, std::variant<Value, std::exception_ptr> v) {
496 if (starts_with(configPath, path)) {
497 printConfigValue(ctx, out, configPath, v);
498 }
499 },
500 optionPath, ctx);
501 },
502 ctx, path);
503}
504
505void printAttr(Context & ctx, Out & out, const std::string & path, Value & root)
506{
507 try {
508 printValue(ctx, out, *findAlongAttrPath(ctx.state, path, ctx.autoArgs, root), path);
509 } catch (Error & e) {
510 out << describeError(e);
511 }
512}
513
514bool hasExample(Context & ctx, Value & option)
515{
516 try {
517 findAlongAttrPath(ctx.state, "example", ctx.autoArgs, option);
518 return true;
519 } catch (Error &) {
520 return false;
521 }
522}
523
524void printOption(Context & ctx, Out & out, const std::string & path, Value & option)
525{
526 out << "Value:\n";
527 printAttr(ctx, out, path, ctx.configRoot);
528
529 out << "\n\nDefault:\n";
530 printAttr(ctx, out, "default", option);
531
532 out << "\n\nType:\n";
533 printAttr(ctx, out, "type.description", option);
534
535 if (hasExample(ctx, option)) {
536 out << "\n\nExample:\n";
537 printAttr(ctx, out, "example", option);
538 }
539
540 out << "\n\nDescription:\n";
541 printAttr(ctx, out, "description", option);
542
543 out << "\n\nDeclared by:\n";
544 printAttr(ctx, out, "declarations", option);
545
546 out << "\n\nDefined by:\n";
547 printAttr(ctx, out, "files", option);
548 out << "\n";
549}
550
551void printListing(Out & out, Value & v)
552{
553 out << "This attribute set contains:\n";
554 for (const auto & a : v.attrs->lexicographicOrder()) {
555 std::string name = a->name;
556 if (!name.empty() && name[0] != '_') {
557 out << name << "\n";
558 }
559 }
560}
561
562void printOne(Context & ctx, Out & out, const std::string & path)
563{
564 try {
565 auto result = findAlongOptionPath(ctx, path);
566 Value & option = result.option;
567 option = evaluateValue(ctx, option);
568 if (path != result.path) {
569 out << "Note: showing " << result.path << " instead of " << path << "\n";
570 }
571 if (isOption(ctx, option)) {
572 printOption(ctx, out, result.path, option);
573 } else {
574 printListing(out, option);
575 }
576 } catch (Error & e) {
577 std::cerr << "error: " << e.msg()
578 << "\nAn error occurred while looking for attribute names. Are "
579 "you sure that '"
580 << path << "' exists?\n";
581 }
582}
583
584int main(int argc, char ** argv)
585{
586 bool recursive = false;
587 std::string path = ".";
588 std::string optionsExpr = "(import <nixpkgs/nixos> {}).options";
589 std::string configExpr = "(import <nixpkgs/nixos> {}).config";
590 std::vector<std::string> args;
591
592 struct MyArgs : nix::LegacyArgs, nix::MixEvalArgs
593 {
594 using nix::LegacyArgs::LegacyArgs;
595 };
596
597 MyArgs myArgs(nix::baseNameOf(argv[0]), [&](Strings::iterator & arg, const Strings::iterator & end) {
598 if (*arg == "--help") {
599 nix::showManPage("nixos-option");
600 } else if (*arg == "--version") {
601 nix::printVersion("nixos-option");
602 } else if (*arg == "-r" || *arg == "--recursive") {
603 recursive = true;
604 } else if (*arg == "--path") {
605 path = nix::getArg(*arg, arg, end);
606 } else if (*arg == "--options_expr") {
607 optionsExpr = nix::getArg(*arg, arg, end);
608 } else if (*arg == "--config_expr") {
609 configExpr = nix::getArg(*arg, arg, end);
610 } else if (!arg->empty() && arg->at(0) == '-') {
611 return false;
612 } else {
613 args.push_back(*arg);
614 }
615 return true;
616 });
617
618 myArgs.parseCmdline(nix::argvToStrings(argc, argv));
619
620 nix::initPlugins();
621 nix::initGC();
622 nix::settings.readOnlyMode = true;
623 auto store = nix::openStore();
624 auto state = std::make_unique<EvalState>(myArgs.searchPath, store);
625
626 Value optionsRoot = parseAndEval(*state, optionsExpr, path);
627 Value configRoot = parseAndEval(*state, configExpr, path);
628
629 Context ctx{*state, *myArgs.getAutoArgs(*state), optionsRoot, configRoot};
630 Out out(std::cout);
631
632 auto print = recursive ? printRecursive : printOne;
633 if (args.empty()) {
634 print(ctx, out, "");
635 }
636 for (const auto & arg : args) {
637 print(ctx, out, arg);
638 }
639
640 ctx.state.printStats();
641
642 return 0;
643}