Franken-Script to generate NixOS Options docs : with custom modules

So I had been working to get a full list of my NixOS Options.
Yes, I know there is the man pages and nixos-options, but I wanted a full list to do fuzzy searches with fzf.
The other caveat is that I wanted to build this without cloning nixpkgs and “cheating” by just editing the documentation code inline. I have resisted this temptation for ages now by using modules and overlays, and I wanted to keep it up.

The script is mostly just gutted sections of <nixpkgs/nixos/docs/manual>, but it adds a few critical additions, (tryEval on missing descriptions, and fake example paths) which normally cause crashes.
In any case here is the script in case it is useful to anyone.

I have it dumping JSON but you could easily swap out that last little bit for xml or whatever other output you want.

#! /usr/bin/env bash

echo $(nix-instantiate - --eval --show-trace <<EOF

with import <nixpkgs/nixos> { };
let
    extraSources = [];
    lib = pkgs.lib;

    optionsListVisible =
        lib.filter (opt: opt.visible && !opt.internal)
        (lib.optionAttrSetToDocList options);

    # Replace functions by the string <function>
    substFunction = x:
        if builtins.isAttrs x then lib.mapAttrs (name: substFunction) x
        else if builtins.isList x then map substFunction x
        else if lib.isFunction x then "<function>"
        else if isPath x then toString x
        else x;

    isPath = x: (builtins.typeOf x) == "path";

    optionsListDesc = lib.flip map optionsListVisible (opt: opt // {
        description = let attempt = builtins.tryEval opt.description; in
            if attempt.success then attempt.value else "N/A";
        declarations = map stripAnyPrefixes opt.declarations;
        }
        // lib.optionalAttrs (opt ? example) {
            example = substFunction opt.example; }
        // lib.optionalAttrs (opt ? default) {
            default = substFunction opt.default; }
        // lib.optionalAttrs (opt ? type) {
            type = substFunction opt.type; }
        // lib.optionalAttrs
            (opt ? relatedPackages && opt.relatedPackages != []) {
                relatedPackages = genRelatedPackages opt.relatedPackages; }
        );

    genRelatedPackages = packages: let
        unpack = p: if lib.isString p then { name = p; }
                    else if lib.isList p then { path = p; }
                    else p;
        describe = args: let
            title = args.title or null;
            name = args.name or (lib.concatStringsSep "." args.path);
            path = args.path or [ args.name ];
            package = args.package or (lib.attrByPath path (throw
                "Invalid package attribute path '\${toString path}'") pkgs);
            in "<listitem>"
            + "<para><literal>\${lib.optionalString (title != null)
                "\${title} aka "}pkgs.\${name} (\${package.meta.name})</literal>"
            + lib.optionalString (!package.meta.available)
                " <emphasis>[UNAVAILABLE]</emphasis>"
            + ": \${package.meta.description or "???"}.</para>"
            + lib.optionalString (args ? comment)
                "\n<para>\${args.comment}</para>"
            + lib.optionalString (package.meta ? longDescription)
                "\n<programlisting>\${package.meta.longDescription}"
                + "</programlisting>"
            + "</listitem>";
        in "<itemizedlist>\${lib.concatStringsSep "\n" (map (p:
            describe (unpack p)) packages)}</itemizedlist>";

    optionLess = a: b:
        let
        ise = lib.hasPrefix "enable";
        isp = lib.hasPrefix "package";
        cmp = lib.splitByAndCompare ise lib.compare
            (lib.splitByAndCompare isp lib.compare lib.compare);
        in lib.compareLists cmp a.loc b.loc < 0;


    prefixesToStrip = map (p: "\${toString p}/") ([ ../../.. ] ++ extraSources);
    stripAnyPrefixes = lib.flip (lib.fold lib.removePrefix) prefixesToStrip;

###############################################################################

    # This is the REAL meat of what we were after.
    # Output this however you want.
    optionsList = lib.sort optionLess optionsListDesc;

    optionsJSON = builtins.unsafeDiscardStringContext (builtins.toJSON
        (builtins.listToAttrs (map (o: {
            name = o.name;
            value = removeAttrs o [
            # Select the fields you want to drop here:
                "name" "visible" "internal" "loc" "readOnly" ];
        }) optionsList)));

in optionsJSON
EOF
) | sed 's/\\\\/\\/g' | sed 's/\\"/"/g' | head -c -2 | tail -c +2

# vim: ft=sh
9 Likes

would you mind sharing similar snippets that do the json parsing and fzf display as in Handy scripts for fuzzy searching nixpkgs and nixos-options - #5 by Growpotkin ? This is really awesome. Great job.

2 Likes

FWIW I put together this little expression for using the generated JSON:

Erm the ‘gfzfopts’ invocation should be adjusted to taste, haha ;).

This expects options.json to be in the same directory,
I was planning to rig it to be generated during nixos-rebuild or so…

Hope this helps!

1 Like

You can build the JSON file directly to avoid all the sed foo:

6 Likes

Thanks for the tip!
The other additions you made are really nice too.
This little script is shaping up quite nicely :slight_smile:

1 Like

Can you repost the script?