From e0780c5cffa7516ad85b885cae7d0c85aed90267 Mon Sep 17 00:00:00 2001
From: Maximilian Bosch <maximilian@mbosch.me>
Date: Tue, 10 Dec 2019 14:31:52 +0100
Subject: [PATCH 1/4] nixos/nixos-option: fix evaluator to render a full
 submodule entry

When running e.g. `nixos-option users.users.ma27`, the evaluation breaks
since `ma27` is the attribute name in `attrsOf (submodule {})`, but not
a part of the option tree and therefore breaks with the following
errors:

```
error: At 'ma27' in path 'users.users.ma27': Attribute not found
An error occurred while looking for attribute names. Are you sure that 'users.users.ma27' exists?
```

This happens since the option evaluator expects that either the option
exists or the option is a submodule and the "next" token in the
attribute path points to an option (e.g. `users.users.ma27.createHome`).

This patch checks in the `Attribute not found` condition if the attribute-path
actually exists in the config tree. If that's true, a dummy-attrset is created
which contains `{_type = "__nixos-option-submodule-attr";}`, in that case, the
entire entry of the submodule will be displayed.
---
 .../tools/nixos-option/nixos-option.cc        | 39 ++++++++++++++-----
 1 file changed, 30 insertions(+), 9 deletions(-)

diff --git a/nixos/modules/installer/tools/nixos-option/nixos-option.cc b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
index 9b92dc829cd1..89f9c88be964 100644
--- a/nixos/modules/installer/tools/nixos-option/nixos-option.cc
+++ b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
@@ -106,13 +106,14 @@ struct Context
 {
     Context(EvalState & state, Bindings & autoArgs, Value optionsRoot, Value configRoot)
         : state(state), autoArgs(autoArgs), optionsRoot(optionsRoot), configRoot(configRoot),
-          underscoreType(state.symbols.create("_type"))
+          underscoreType(state.symbols.create("_type")), submoduleAttr(state.symbols.create("__nixos-option-submodule-attr"))
     {}
     EvalState & state;
     Bindings & autoArgs;
     Value optionsRoot;
     Value configRoot;
     Symbol underscoreType;
+    Symbol submoduleAttr;
 };
 
 Value evaluateValue(Context & ctx, Value & v)
@@ -126,26 +127,31 @@ Value evaluateValue(Context & ctx, Value & v)
     return called;
 }
 
-bool isOption(Context & ctx, const Value & v)
+bool isType(Context & ctx, const Value & v, const std::string & type)
 {
     if (v.type != tAttrs) {
         return false;
     }
-    const auto & atualType = v.attrs->find(ctx.underscoreType);
-    if (atualType == v.attrs->end()) {
+    const auto & actualType = v.attrs->find(ctx.underscoreType);
+    if (actualType == v.attrs->end()) {
         return false;
     }
     try {
-        Value evaluatedType = evaluateValue(ctx, *atualType->value);
+        Value evaluatedType = evaluateValue(ctx, *actualType->value);
         if (evaluatedType.type != tString) {
             return false;
         }
-        return static_cast<std::string>(evaluatedType.string.s) == "option";
+        return static_cast<std::string>(evaluatedType.string.s) == type;
     } catch (Error &) {
         return false;
     }
 }
 
+bool isOption(Context & ctx, const Value & v)
+{
+    return isType(ctx, v, "option");
+}
+
 // Add quotes to a component of a path.
 // These are needed for paths like:
 //    fileSystems."/".fsType
@@ -519,11 +525,24 @@ Value findAlongOptionPath(Context & ctx, const std::string & path)
             } 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));
+                auto symbol = ctx.state.symbols.create(attr);
+                const auto & next = v.attrs->find(symbol);
                 if (next == v.attrs->end()) {
+                    try {
+                        const auto & value = findAlongAttrPath(ctx.state, path, ctx.autoArgs, ctx.configRoot);
+                        Value &dummyOpt = *ctx.state.allocValue();
+                        ctx.state.mkAttrs(dummyOpt, 1);
+                        Value *type = ctx.state.allocAttr(dummyOpt, ctx.state.symbols.create("_type"));
+                        nix::mkString(*type, ctx.submoduleAttr);
+                        v = dummyOpt;
+                        break;
+                    } catch (Error & e) {
+                        // do nothing
+                    }
                     throw OptionPathError("Attribute not found", attr, path);
+                } else {
+                   v = *next->value;
                 }
-                v = *next->value;
             }
         } catch (OptionPathError & e) {
             throw OptionPathError("At '%s' in path '%s': %s", attr, path, e.msg());
@@ -537,7 +556,9 @@ void printOne(Context & ctx, Out & out, const std::string & path)
     try {
         Value option = findAlongOptionPath(ctx, path);
         option = evaluateValue(ctx, option);
-        if (isOption(ctx, option)) {
+        if (isType(ctx, option, ctx.submoduleAttr)) {
+            printAttr(ctx, out, path, ctx.configRoot);
+        } else if (isOption(ctx, option)) {
             printOption(ctx, out, path, option);
         } else {
             printListing(out, option);

From 09ac7cb55ff97dcef07d9ddd37ccb08be2a8dad1 Mon Sep 17 00:00:00 2001
From: Chuck <chuck@intelligence.org>
Date: Thu, 12 Dec 2019 09:39:09 -0800
Subject: [PATCH 2/4] nixos/nixos-option: Show values inside aggregate options
 uniformly

1. This makes aggregates of submodules (including the very important
"nixos-option users.users.<username>" case) behave the same way as any
other you-need-to-keep-typing-to-get-to-an-option-leaf (eg:
"nixos-option environment").

Before e0780c5:

  $ nixos-option users.users.root
  error: At 'root' in path 'users.users.root': Attribute not found
  An error occurred while looking for attribute names. Are you sure that 'users.users.root' exists?

After e0780c5 but before this change, this query just printed out a raw
thing, which is behavior that belongs in "nix eval", "nix-instantiate
--eval", or "nix repl <<<":

  $ nixos-option users.users.root
  {
    _module = {
      args = { name = "root"; };
      check = true;
    };
    createHome = false;
    cryptHomeLuks = null;
    description = "System administrator";
    ...

After this change:

  $ nixos-option users.users.root
  This attribute set contains:
  createHome
  cryptHomeLuks
  description
  extraGroups
  group
  hashedPassword
  ...

2. For aggregates of other types (not submodules), print out the option
that contains them rather than printing an error message.

Before:

  $ nixos-option environment.shellAliases.l
  error: At 'l' in path 'environment.shellAliases.l': Attribute not found
  An error occurred while looking for attribute names. Are you sure that 'environment.shellAliases.l' exists?

After:

  $ nixos-option environment.shellAliases.l
  Note: showing environment.shellAliases instead of environment.shellAliases.l
  Value:
  {
    l = "ls -alh";
    ll = "ls -l";
    ls = "ls --color=tty";
  }
  ...
---
 .../tools/nixos-option/nixos-option.cc        | 68 +++++++++----------
 1 file changed, 32 insertions(+), 36 deletions(-)

diff --git a/nixos/modules/installer/tools/nixos-option/nixos-option.cc b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
index 89f9c88be964..5aa3c766d103 100644
--- a/nixos/modules/installer/tools/nixos-option/nixos-option.cc
+++ b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
@@ -106,14 +106,13 @@ struct Context
 {
     Context(EvalState & state, Bindings & autoArgs, Value optionsRoot, Value configRoot)
         : state(state), autoArgs(autoArgs), optionsRoot(optionsRoot), configRoot(configRoot),
-          underscoreType(state.symbols.create("_type")), submoduleAttr(state.symbols.create("__nixos-option-submodule-attr"))
+          underscoreType(state.symbols.create("_type"))
     {}
     EvalState & state;
     Bindings & autoArgs;
     Value optionsRoot;
     Value configRoot;
     Symbol underscoreType;
-    Symbol submoduleAttr;
 };
 
 Value evaluateValue(Context & ctx, Value & v)
@@ -127,7 +126,7 @@ Value evaluateValue(Context & ctx, Value & v)
     return called;
 }
 
-bool isType(Context & ctx, const Value & v, const std::string & type)
+bool isOption(Context & ctx, const Value & v)
 {
     if (v.type != tAttrs) {
         return false;
@@ -141,17 +140,12 @@ bool isType(Context & ctx, const Value & v, const std::string & type)
         if (evaluatedType.type != tString) {
             return false;
         }
-        return static_cast<std::string>(evaluatedType.string.s) == type;
+        return static_cast<std::string>(evaluatedType.string.s) == "option";
     } catch (Error &) {
         return false;
     }
 }
 
-bool isOption(Context & ctx, const Value & v)
-{
-    return isType(ctx, v, "option");
-}
-
 // Add quotes to a component of a path.
 // These are needed for paths like:
 //    fileSystems."/".fsType
@@ -300,9 +294,11 @@ 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;
-        attrsOut << name << " = ";
-        printValue(ctx, attrsOut, *a->value, appendPath(path, name));
-        attrsOut << ";" << Out::sep;
+        if (!forbiddenRecursionName(name)) {
+            attrsOut << name << " = ";
+            printValue(ctx, attrsOut, *a->value, appendPath(path, name));
+            attrsOut << ";" << Out::sep;
+        }
     }
 }
 
@@ -503,10 +499,16 @@ Value getSubOptions(Context & ctx, Value & option)
 
 // Carefully walk an option path, looking for sub-options when a path walks past
 // an option value.
-Value findAlongOptionPath(Context & ctx, const std::string & path)
+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 {
@@ -518,48 +520,42 @@ Value findAlongOptionPath(Context & ctx, const std::string & path)
             if (isOption(ctx, v) && optionTypeIs(ctx, v, "submodule")) {
                 v = getSubOptions(ctx, v);
             }
-            if (isOption(ctx, v) && isAggregateOptionType(ctx, v) && !lastAttribute) {
-                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 {
-                auto symbol = ctx.state.symbols.create(attr);
-                const auto & next = v.attrs->find(symbol);
+                const auto & next = v.attrs->find(ctx.state.symbols.create(attr));
                 if (next == v.attrs->end()) {
-                    try {
-                        const auto & value = findAlongAttrPath(ctx.state, path, ctx.autoArgs, ctx.configRoot);
-                        Value &dummyOpt = *ctx.state.allocValue();
-                        ctx.state.mkAttrs(dummyOpt, 1);
-                        Value *type = ctx.state.allocAttr(dummyOpt, ctx.state.symbols.create("_type"));
-                        nix::mkString(*type, ctx.submoduleAttr);
-                        v = dummyOpt;
-                        break;
-                    } catch (Error & e) {
-                        // do nothing
-                    }
                     throw OptionPathError("Attribute not found", attr, path);
-                } else {
-                   v = *next->value;
                 }
+                v = *next->value;
             }
+            processedPath = appendPath(processedPath, attr);
         } catch (OptionPathError & e) {
             throw OptionPathError("At '%s' in path '%s': %s", attr, path, e.msg());
         }
     }
-    return v;
+    return {v, processedPath};
 }
 
 void printOne(Context & ctx, Out & out, const std::string & path)
 {
     try {
-        Value option = findAlongOptionPath(ctx, path);
+        auto result = findAlongOptionPath(ctx, path);
+        Value & option = result.option;
         option = evaluateValue(ctx, option);
-        if (isType(ctx, option, ctx.submoduleAttr)) {
-            printAttr(ctx, out, path, ctx.configRoot);
-        } else if (isOption(ctx, option)) {
-            printOption(ctx, out, path, 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);
         }

From 9dd23e87430bb77023d32dcfd374e6b68b0de56c Mon Sep 17 00:00:00 2001
From: Chuck <chuck@intelligence.org>
Date: Tue, 17 Dec 2019 11:08:24 -0800
Subject: [PATCH 3/4] nixos/nixos-option: Refactor: Move functions around

---
 .../tools/nixos-option/nixos-option.cc        | 186 +++++++++---------
 1 file changed, 93 insertions(+), 93 deletions(-)

diff --git a/nixos/modules/installer/tools/nixos-option/nixos-option.cc b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
index 5aa3c766d103..a5c0b65baeb8 100644
--- a/nixos/modules/installer/tools/nixos-option/nixos-option.cc
+++ b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
@@ -197,6 +197,99 @@ void recurse(const std::function<bool(const std::string & path, std::variant<Val
     }
 }
 
+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") || optionTypeIs(ctx, v, "loaOf");
+}
+
+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
 void mapOptions(const std::function<void(const std::string & path)> & f, Context & ctx, Value root)
 {
@@ -452,99 +545,6 @@ void printListing(Out & out, Value & v)
     }
 }
 
-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") || optionTypeIs(ctx, v, "loaOf");
-}
-
-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};
-}
-
 void printOne(Context & ctx, Out & out, const std::string & path)
 {
     try {

From ed51fd0033b5d5e2ad40cca02cc9864db845b586 Mon Sep 17 00:00:00 2001
From: Chuck <chuck@intelligence.org>
Date: Tue, 17 Dec 2019 12:08:07 -0800
Subject: [PATCH 4/4] nixos/nixos-option: Convert --all into -r

---
 nixos/doc/manual/man-nixos-option.xml         | 27 +++++-----
 nixos/doc/manual/release-notes/rl-2003.xml    |  2 +-
 .../tools/nixos-option/nixos-option.cc        | 54 +++++++++++--------
 3 files changed, 47 insertions(+), 36 deletions(-)

diff --git a/nixos/doc/manual/man-nixos-option.xml b/nixos/doc/manual/man-nixos-option.xml
index beabf020c92a..0c6a6950a056 100644
--- a/nixos/doc/manual/man-nixos-option.xml
+++ b/nixos/doc/manual/man-nixos-option.xml
@@ -14,12 +14,16 @@
  <refsynopsisdiv>
   <cmdsynopsis>
    <command>nixos-option</command>
+
    <arg>
-    <option>-I</option> <replaceable>path</replaceable>
+    <group choice='req'>
+     <arg choice='plain'><option>-r</option></arg>
+     <arg choice='plain'><option>--recursive</option></arg>
+    </group>
    </arg>
 
    <arg>
-    <option>--all</option>
+    <option>-I</option> <replaceable>path</replaceable>
    </arg>
 
    <arg>
@@ -45,6 +49,15 @@
    This command accepts the following options:
   </para>
   <variablelist>
+   <varlistentry>
+    <term><option>-r</option></term>
+    <term><option>--recursive</option></term>
+    <listitem>
+     <para>
+      Print all the values at or below the specified path recursively.
+     </para>
+    </listitem>
+   </varlistentry>
    <varlistentry>
     <term>
      <option>-I</option> <replaceable>path</replaceable>
@@ -56,16 +69,6 @@
      </para>
     </listitem>
    </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--all</option>
-    </term>
-    <listitem>
-     <para>
-      Print the values of all options.
-     </para>
-    </listitem>
-   </varlistentry>
   </variablelist>
  </refsection>
  <refsection>
diff --git a/nixos/doc/manual/release-notes/rl-2003.xml b/nixos/doc/manual/release-notes/rl-2003.xml
index b8af15f59c95..417b8524ab82 100644
--- a/nixos/doc/manual/release-notes/rl-2003.xml
+++ b/nixos/doc/manual/release-notes/rl-2003.xml
@@ -52,7 +52,7 @@
    <listitem>
     <para>
       <command>nixos-option</command> has been rewritten in C++, speeding it up, improving correctness,
-      and adding a <option>--all</option> option which prints all options and their values.
+      and adding a <option>-r</option> option which prints all options and their values recursively.
     </para>
    </listitem>
   </itemizedlist>
diff --git a/nixos/modules/installer/tools/nixos-option/nixos-option.cc b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
index a5c0b65baeb8..1a7b07a74f8a 100644
--- a/nixos/modules/installer/tools/nixos-option/nixos-option.cc
+++ b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
@@ -290,9 +290,14 @@ FindAlongOptionPathRet findAlongOptionPath(Context & ctx, const std::string & pa
     return {v, processedPath};
 }
 
-// Calls f on all the option names
-void mapOptions(const std::function<void(const std::string & path)> & f, Context & ctx, Value root)
+// 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));
@@ -301,7 +306,7 @@ void mapOptions(const std::function<void(const std::string & path)> & f, Context
             }
             return !isOpt;
         },
-        ctx, root, "");
+        ctx, root.option, root.path);
 }
 
 // Calls f on all the config values inside one option.
@@ -475,17 +480,26 @@ void printConfigValue(Context & ctx, Out & out, const std::string & path, std::v
     out << ";\n";
 }
 
-void printAll(Context & ctx, Out & out)
+// 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](const std::string & optionPath) {
+        [&ctx, &out, &path](const std::string & optionPath) {
             mapConfigValuesInOption(
-                [&ctx, &out](const std::string & configPath, std::variant<Value, std::exception_ptr> v) {
-                    printConfigValue(ctx, out, configPath, v);
+                [&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, ctx.optionsRoot);
+        ctx, path);
 }
 
 void printAttr(Context & ctx, Out & out, const std::string & path, Value & root)
@@ -569,7 +583,7 @@ void printOne(Context & ctx, Out & out, const std::string & path)
 
 int main(int argc, char ** argv)
 {
-    bool all = false;
+    bool recursive = false;
     std::string path = ".";
     std::string optionsExpr = "(import <nixpkgs/nixos> {}).options";
     std::string configExpr = "(import <nixpkgs/nixos> {}).config";
@@ -585,8 +599,8 @@ int main(int argc, char ** argv)
             nix::showManPage("nixos-option");
         } else if (*arg == "--version") {
             nix::printVersion("nixos-option");
-        } else if (*arg == "--all") {
-            all = true;
+        } else if (*arg == "-r" || *arg == "--recursive") {
+            recursive = true;
         } else if (*arg == "--path") {
             path = nix::getArg(*arg, arg, end);
         } else if (*arg == "--options_expr") {
@@ -615,18 +629,12 @@ int main(int argc, char ** argv)
     Context ctx{*state, *myArgs.getAutoArgs(*state), optionsRoot, configRoot};
     Out out(std::cout);
 
-    if (all) {
-        if (!args.empty()) {
-            throw UsageError("--all cannot be used with arguments");
-        }
-        printAll(ctx, out);
-    } else {
-        if (args.empty()) {
-            printOne(ctx, out, "");
-        }
-        for (const auto & arg : args) {
-            printOne(ctx, out, arg);
-        }
+    auto print = recursive ? printRecursive : printOne;
+    if (args.empty()) {
+        print(ctx, out, "");
+    }
+    for (const auto & arg : args) {
+        print(ctx, out, arg);
     }
 
     ctx.state.printStats();