nixos-option: rewrite as a nix script

This ports the functionality of the C++ nixos-option to a nix script
(with a tiny shellscript for argument processing and invoking the nix
script). Benefits compared to the native binary include no longer being
tied to a specific nix version, generally improved maintainability and
improved stability.

The main tradeoff is that the C++ version would have better access to
introspecting and reporting errors nicely, but that doesn't seem to have
been the case in practice anyway.  The other tradeoff is that we
generate all the output at the end instead of streaming it as we
traverse the option tree.

Co-authored-by: Zhong Jianxin <azuwis@gmail.com>
Co-authored-by: aleksana <me@aleksana.moe>
Co-authored-by: eclairevoyant <contactmeongithubinstead@proton.me>
This commit is contained in:
FireFly
2025-01-01 13:18:02 +00:00
committed by Thiago Kenji Okada
co-authored by Zhong Jianxin aleksana eclairevoyant
parent 1c5eed9e58
commit 7c8e422689
8 changed files with 325 additions and 737 deletions
+56 -26
View File
@@ -1,43 +1,73 @@
{
lib,
stdenv,
boost,
meson,
ninja,
pkg-config,
makeWrapper,
stdenvNoCC,
installShellFiles,
shellcheck,
nix,
nixosTests,
jq,
man-db,
coreutils,
}:
stdenv.mkDerivation {
stdenvNoCC.mkDerivation {
name = "nixos-option";
src = ./.;
postInstall = ''
installManPage ../nixos-option.8
'';
strictDeps = true;
src = ./nixos-option.sh;
nativeBuildInputs = [
meson
ninja
pkg-config
installShellFiles
];
buildInputs = [
boost
nix
makeWrapper
shellcheck
];
passthru.tests.installer-simpleUefiSystemdBoot = nixosTests.installer.simpleUefiSystemdBoot;
env = {
nixosOptionNix = "${./nixos-option.nix}";
nixosOptionManpage = "${placeholder "out"}/share/man";
};
meta = with lib; {
license = licenses.lgpl2Plus;
dontUnpack = true;
dontConfigure = true;
dontPatch = true;
dontBuild = true;
installPhase = ''
runHook preInstall
install -Dm555 $src $out/bin/nixos-option
substituteAllInPlace $out/bin/nixos-option
installManPage ${./nixos-option.8}
runHook postInstall
'';
doInstallCheck = true;
installCheckPhase = ''
runHook preInstallCheck
shellcheck $out/bin/nixos-option
runHook postInstallCheck
'';
postFixup = ''
wrapProgram $out/bin/nixos-option \
--prefix PATH : ${
lib.makeBinPath [
nix
jq
man-db
coreutils
]
}
'';
meta = {
description = "Evaluate NixOS configuration and return the properties of given option";
license = lib.licenses.mit;
mainProgram = "nixos-option";
maintainers = [ ];
inherit (nix.meta) platforms;
maintainers = with lib.maintainers; [
FireyFly
azuwis
aleksana
];
};
}
-15
View File
@@ -1,15 +0,0 @@
project('nixos-option', 'cpp',
version : '0.1.6',
license : 'GPL-3.0',
)
nix_main_dep = dependency('nix-main', required: true)
nix_store_dep = dependency('nix-store', required: true)
nix_expr_dep = dependency('nix-expr', required: true)
nix_cmd_dep = dependency('nix-cmd', required: true)
nix_flake_dep = dependency('nix-flake', required: true)
threads_dep = dependency('threads', required: true)
nlohmann_json_dep = dependency('nlohmann_json', required: true)
boost_dep = dependency('boost', required: true)
subdir('src')
@@ -0,0 +1,167 @@
{
nixos ? import <nixpkgs/nixos> { },
# list representing a nixos option path (e.g. ['console' 'enable']), or a
# prefix of such a path (e.g. ['console']), or a string representing the same
# (e.g. 'console.enable')
path,
# whether to recurse down the config attrset and show each set value instead
recursive ? false,
}:
let
inherit (nixos.pkgs) lib;
path' = if lib.isString path then (if path == "" then [ ] else readOption path) else path;
# helper that maps `f` on subslices starting when `predStart x` and
# ending when `predEnd x` (no support for nested occurrences)
flatMapSlices =
predStart: predEnd: f: list:
let
empty = {
result = [ ];
active = [ ];
};
op =
{ result, active }:
x:
if predStart x && predEnd x then
{
result = result ++ active ++ f [ x ];
active = [ ];
}
else if predStart x then
{
result = result ++ active;
active = [ x ];
}
else if predEnd x then
{
result = result ++ f (active ++ [ x ]);
active = [ ];
}
else
{
inherit result;
active = active ++ [ x ];
};
in
(x: x.result ++ x.active) (lib.foldl op empty list);
# tries to invert showOption, taking a written-out option name and splitting
# it into its parts
readOption =
str:
let
unescape = list: lib.replaceStrings (map (c: "\\${c}") list) list;
unescapeNixString = lib.flip lib.pipe [
(lib.concatStringsSep ".")
(unescape [ "$" ])
builtins.fromJSON
];
in
flatMapSlices (lib.hasPrefix "\"") (lib.hasSuffix "\"") (x: [ (unescapeNixString x) ]) (
lib.splitString "." str
);
# like 'mapAttrsRecursiveCond' but handling errors in the attrset tree as leaf
# nodes (which means `f` is expected to handle shallow errors)
safeMapAttrsRecursiveCond =
cond: f: set:
let
recurse =
path:
lib.mapAttrs (
name: value:
let
e = builtins.tryEval value;
path' = path ++ [ name ];
in
if e.success && lib.isAttrs value && cond value then recurse path' value else f path' value
);
in
recurse [ ] set;
# traverse the option tree along `path` from `root`, returning the option or
# attrset at the given location
optionByPath =
path: root:
let
into =
opt: part:
if lib.isOption opt && opt.type.descriptionClass == "composite" then
opt.type.getSubOptions [ ]
else if lib.isOption opt then
throw "Trying to access '${part}' inside ${opt.type.name} option while traversing option path '${lib.showOption path}'"
else if lib.isAttrs opt && lib.hasAttr part opt then
opt.${part}
else
throw "Found neither an attrset nor supported option type near '${part}' while traversing option path '${lib.showOption path}'";
in
lib.foldl into root path;
toPretty = lib.generators.toPretty { multiline = true; };
safeToPretty =
x:
let
e = builtins.tryEval (toPretty x);
in
if e.success then e.value else "«error»";
indent = str: lib.concatStringsSep "\n" (map (x: " " + x) (lib.splitString "\n" str));
optionAttrNames = attrs: lib.filter (x: x != "_module") (lib.attrNames attrs);
## full, non-recursive mode: print an option from `options`
renderAttrs =
attrs: "This attribute set contains:\n${lib.concatStringsSep "\n" (optionAttrNames attrs)}";
renderOption =
option: value:
let
entry =
cond: heading: value:
lib.optional cond "${heading}:\n${indent value}";
in
lib.concatStringsSep "\n\n" (
lib.concatLists [
(entry true "Value" (toPretty value))
(entry (option ? default) "Default" (toPretty option.default))
(entry (option ? type) "Type" (option.type.description))
(entry (option ? description) "Description" (lib.removeSuffix "\n" option.description))
(entry (option ? example) "Example" (toPretty option.example))
(entry (option ? declarations) "Declared by" (lib.concatStringsSep "\n" option.declarations))
(entry (option ? files) "Defined by" (lib.concatStringsSep "\n" option.files))
]
);
renderFull =
entry: configEntry:
if lib.isOption entry then
renderOption entry configEntry
else if lib.isAttrs entry then
renderAttrs entry
else
throw "Found neither an attrset nor option at option path '${lib.showOption path'}'";
## recursive mode: print paths and values from `config`
renderRecursive =
config:
let
renderShort = n: v: "${lib.showOption (path' ++ n)} = ${safeToPretty v};";
mapAttrsRecursive' = safeMapAttrsRecursiveCond (x: !lib.isDerivation x);
in
if lib.isAttrs config then
lib.concatStringsSep "\n" (lib.collect lib.isString (mapAttrsRecursive' renderShort config))
else
renderShort [ ] config;
in
if !lib.hasAttrByPath path' nixos.config then
throw "Couldn't resolve config path '${lib.showOption path'}'"
else
let
optionEntry = optionByPath path' nixos.options;
configEntry = lib.attrByPath path' null nixos.config;
in
if recursive then renderRecursive configEntry else renderFull optionEntry configEntry
+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
# shellcheck shell=bash
set -eou pipefail
recursive=false
no_flake=false
positional_args=()
nix_args=()
flake=""
while [[ $# -gt 0 ]]; do
case "$1" in
--help)
exec man -M @nixosOptionManpage@ 8 nixos-option
;;
-r|--recursive)
recursive=true
shift
;;
-I)
nix_args+=("$1" "$2")
# Not breaking existing usages of it
no_flake=true
shift 2
;;
-F|--flake)
flake=$2
shift 2
;;
--no-flake)
no_flake=true
shift
;;
--show-trace)
nix_args+=("$1")
shift
;;
-*)
echo >&2 "Unsupported option $1"
exit 1
;;
*)
positional_args+=("$1")
shift
;;
esac
done
# Detection order, from high to low:
# `--flake`
# $NIXOS_CONFIG
# nixos-config=<path> in NIX_PATH (normally /etc/nixos/configuration.nix)
# `-I` (implies `--no-flake`)
# `--no-flake`
# /etc/nixos/flake.nix (if exists)
if [[ -z "$flake" ]] && [[ -e /etc/nixos/flake.nix ]] && [[ "$no_flake" == "false" ]]; then
flake="$(dirname "$(realpath /etc/nixos/flake.nix)")"
fi
if [[ -n "$flake" ]]; then
if [[ $flake =~ ^(.*)\#([^\#\"]*)$ ]]; then
flake="${BASH_REMATCH[1]}"
flakeAttr="${BASH_REMATCH[2]}"
fi
# Unlike nix cli, builtins.getFlake infer path:// when a path is given
# See https://github.com/NixOS/nix/issues/5836
if [[ -d "$flake" ]] && [[ -e "$flake/.git" ]] ; then
flake="git+file://$(realpath "$flake")"
fi
if [[ -z "${flakeAttr:-}" ]]; then
hostname=$(< /proc/sys/kernel/hostname)
if [[ -z "${hostname:-}" ]]; then
hostname=default
fi
flakeAttr="nixosConfigurations.\"$hostname\""
else
flakeAttr="nixosConfigurations.\"$flakeAttr\""
fi
nix_args+=(--arg nixos "(builtins.getFlake \"$flake\").$flakeAttr")
fi
case ${#positional_args[@]} in
0) path= ;;
# Remove trailing dot if exists, match the behavior of
# old nixos-option and make shell completions happy
1) path="${positional_args[0]%.}" ;;
*) echo >&2 "Only one option path can be provided"; exit 1 ;;
esac
nix-instantiate "${nix_args[@]}" --eval --json \
--argstr path "$path" \
--arg recursive "$recursive" \
@nixosOptionNix@ \
| jq -r
@@ -1,22 +0,0 @@
// These are useful methods inside the nix library that ought to be exported.
// Since they are not, copy/paste them here.
// TODO: Delete these and use the ones in the library as they become available.
#include "libnix-copy-paste.hh"
#include <nix/print.hh> // for Strings
// From nix/src/nix/repl.cc
bool isVarName(const std::string_view & s)
{
if (s.size() == 0) return false;
if (nix::isReservedKeyword(s)) 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;
}
@@ -1,5 +0,0 @@
#pragma once
#include <nix/types.hh>
bool isVarName(const std::string_view & s);
@@ -1,20 +0,0 @@
src = [
'nixos-option.cc',
'libnix-copy-paste.cc',
]
cc = meson.get_compiler('cpp')
executable('nixos-option', src,
dependencies : [
nix_main_dep,
nix_store_dep,
nix_expr_dep,
nix_cmd_dep,
nix_flake_dep,
boost_dep,
nlohmann_json_dep,
threads_dep
],
install: true,
cpp_args: ['-std=c++2a', '--include', 'nix/config.h'])
@@ -1,649 +0,0 @@
#include <nix/args.hh> // for argvToStrings, UsageError
#include <nix/attr-path.hh> // for findAlongAttrPath, parseAttrPath
#include <nix/attr-set.hh> // for Attr, Bindings, Bindings::iterator
#include <nix/common-eval-args.hh> // for MixEvalArgs
#include <nix/eval-gc.hh> // for initGC, initNix
#include <nix/eval-inline.hh> // for EvalState::forceValue
#include <nix/eval.hh> // for EvalState, initGC, operator<<
#include <nix/globals.hh> // for initPlugins, Settings, settings
#include <nix/nixexpr.hh> // for Pos
#include <nix/shared.hh> // for getArg, LegacyArgs, printVersion
#include <nix/store-api.hh> // for openStore
#include <nix/symbol-table.hh> // for Symbol, SymbolTable
#include <nix/types.hh> // for Error, Path, Strings, PathSet
#include <nix/util.hh> // for absPath, baseNameOf
#include <nix/value.hh> // for Value, Value::(anonymous), Value:...
#include <string> // for string, operator+, operator==
#include <utility> // for move
#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::nAttrs;
using nix::ThrownError;
using nix::tLambda;
using nix::nString;
using nix::UsageError;
using nix::Value;
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;
};
// 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 <typename T> friend Out & operator<<(Out & o, T thing);
friend void printValue(Context & ctx, Out & out, std::variant<Value, std::exception_ptr> maybeValue, const std::string & path);
};
template <typename T> 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::Separator>(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;
}
Value evaluateValue(Context & ctx, Value & v)
{
ctx.state.forceValue(v, v.determinePos(nix::noPos));
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() != nAttrs) {
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() != nString) {
return false;
}
return evaluatedType.string_view() == "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_view & attribute)
{
if (isVarName(attribute)) {
return std::string(attribute);
}
std::ostringstream buf;
nix::printLiteralString(buf, attribute);
return buf.str();
}
const std::string appendPath(const std::string & prefix, const std::string_view & suffix)
{
if (prefix.empty()) {
return quoteAttribute(suffix);
}
return prefix + "." + quoteAttribute(suffix);
}
bool forbiddenRecursionName(const nix::Symbol symbol, const nix::SymbolTable & symbolTable) {
// note: this is created from a pointer
// According to standard, it may never point to null, and hence attempts to check against nullptr are not allowed.
// However, at the time of writing, I am not certain about the full implications of the omission of a nullptr check here.
const std::string_view & name = symbolTable[symbol];
// TODO: figure out why haskellPackages is not recursed here
return (!name.empty() && name[0] == '_') || name == "haskellPackages";
}
void recurse(const std::function<bool(const std::string & path, std::variant<Value, std::exception_ptr>)> & f,
Context & ctx, Value v, const std::string & path)
{
std::variant<Value, std::exception_ptr> evaluated;
try {
evaluated = evaluateValue(ctx, v);
} catch (Error &) {
evaluated = std::current_exception();
}
if (!f(path, evaluated)) {
return;
}
if (std::holds_alternative<std::exception_ptr>(evaluated)) {
return;
}
const Value & evaluated_value = std::get<Value>(evaluated);
if (evaluated_value.type() != nAttrs) {
return;
}
for (const auto & child : evaluated_value.attrs()->lexicographicOrder(ctx.state.symbols)) {
if (forbiddenRecursionName(child->name, ctx.state.symbols)) {
continue;
}
std::string_view name = ctx.state.symbols[child->name];
recurse(f, ctx, *child->value, appendPath(path, 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() != nAttrs) {
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() != nString) {
return false;
}
return name.string_view() == 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).first);
if (getSubOptions.isLambda()) {
throw OptionPathError(ctx.state, "Option's type.getSubOptions isn't a function");
}
Value emptyString{};
emptyString.mkString("");
Value v;
ctx.state.callFunction(getSubOptions, emptyString, v, nix::PosIdx{});
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)
{
std::vector<Symbol> tokens = nix::parseAttrPath(ctx.state, path);
Value v = ctx.optionsRoot;
std::string processedPath;
for (auto i = tokens.begin(); i != tokens.end(); i++) {
const std::string_view attr = ctx.state.symbols[*i];
try {
bool lastAttribute = std::next(i) == tokens.end();
v = evaluateValue(ctx, v);
if (attr.empty()) {
throw OptionPathError(ctx.state, "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() != nAttrs) {
throw OptionPathError(ctx.state, "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(ctx.state, "Attribute not found", attr, path);
}
v = *next->value;
}
processedPath = appendPath(processedPath, attr);
} catch (OptionPathError & e) {
throw OptionPathError(ctx.state, "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<void(const std::string & path)> & f, Context & ctx, const std::string & path)
{
auto root = findAlongOptionPath(ctx, path);
recurse(
[f, &ctx](const std::string & path, std::variant<Value, std::exception_ptr> v) {
bool isOpt = std::holds_alternative<std::exception_ptr>(v) || isOption(ctx, std::get<Value>(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 services.foo.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<void(const std::string & path, std::variant<Value, std::exception_ptr> v)> & f,
const std::string & path, Context & ctx)
{
Value * option;
try {
option = findAlongAttrPath(ctx.state, path, ctx.autoArgs, ctx.configRoot).first;
} catch (Error &) {
f(path, std::current_exception());
return;
}
recurse(
[f, ctx](const std::string & path, std::variant<Value, std::exception_ptr> v) {
bool leaf = std::holds_alternative<std::exception_ptr>(v) || std::get<Value>(v).type() != nAttrs ||
ctx.state.isDerivation(std::get<Value>(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 printDerivation() :(
std::optional<nix::StorePath> storePath = std::nullopt;
if (auto i = v.attrs()->get(ctx.state.sDrvPath)) {
nix::NixStringContext context;
storePath = ctx.state.coerceToStorePath(i->pos, *i->value, context, "while evaluating the drvPath of a derivation");
}
out << "«derivation ";
if (storePath) {
out << " " << ctx.state.store->printStorePath(*storePath);
}
out << "»";
}
Value parseAndEval(EvalState & state, const std::string & expression, const std::string & path)
{
Value v{};
state.eval(state.parseExprFromString(expression, state.rootPath(".")), v);
return v;
}
void printValue(Context & ctx, Out & out, std::variant<Value, std::exception_ptr> 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(ctx.state.symbols)) {
if (!forbiddenRecursionName(a->name, ctx.state.symbols)) {
std::string_view name = ctx.state.symbols[a->name];
attrsOut << name << " = ";
printValue(ctx, attrsOut, *a->value, appendPath(path, name));
attrsOut << ";" << Out::sep;
}
}
}
void multiLineStringEscape(Out & out, const std::string_view & s)
{
size_t 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_view s = v.string_view();
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<Value, std::exception_ptr> maybeValue, const std::string & path)
{
try {
if (auto ex = std::get_if<std::exception_ptr>(&maybeValue)) {
std::rethrow_exception(*ex);
}
Value v = evaluateValue(ctx, std::get<Value>(maybeValue));
if (ctx.state.isDerivation(v)) {
describeDerivation(ctx, out, v);
} else if (v.isList()) {
printList(ctx, out, v);
} else if (v.type() == nAttrs) {
printAttrs(ctx, out, v, path);
} else if (v.type() == nString && std::string(v.string_view()).find('\n') != std::string::npos) {
printMultiLineString(out, v);
} else {
ctx.state.forceValueDeep(v);
v.print(ctx.state, out.ostream);
}
} catch (ThrownError & e) {
if (e.msg() == "The option `" + path + "' was accessed but has no value defined. Try setting the option.") {
// 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<Value, std::exception_ptr> 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<Value, std::exception_ptr> 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).first, 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(Context & ctx, Out & out, Value & v)
{
out << "This attribute set contains:\n";
for (const auto & a : v.attrs()->lexicographicOrder(ctx.state.symbols)) {
const std::string_view & name = ctx.state.symbols[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(ctx, 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 <nixpkgs/nixos> {}).options";
std::string configExpr = "(import <nixpkgs/nixos> {}).config";
std::vector<std::string> args;
struct MyArgs : nix::LegacyArgs, nix::MixEvalArgs
{
using nix::LegacyArgs::LegacyArgs;
};
MyArgs myArgs(std::string(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::initNix();
nix::initGC();
nix::settings.readOnlyMode = true;
auto store = nix::openStore();
auto evalStore = myArgs.evalStoreUrl ? nix::openStore(*myArgs.evalStoreUrl)
: nix::openStore();
auto state = nix::make_ref<nix::EvalState>(
myArgs.lookupPath, evalStore, nix::fetchSettings, nix::evalSettings);
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.maybePrintStats();
return 0;
}