/* Functions that generate widespread file * formats from nix data structures. * * They all follow a similar interface: * generator { config-attrs } data * * `config-attrs` are “holes” in the generators * with sensible default implementations that * can be overwritten. The default implementations * are mostly generators themselves, called with * their respective default values; they can be reused. * * Tests can be found in ./tests/misc.nix * Documentation in the manual, #sec-generators */ { lib }: with (lib).trivial; let libAttr = lib.attrsets; inherit (builtins) addErrorContext attrNames concatLists concatStringsSep elem filter head isAttrs isFloat isInt isList isPath isString length mapAttrs match replaceStrings split tail ; inherit (lib.attrsets) isDerivation mapAttrsToList recursiveUpdate ; inherit (lib.lists) init flatten foldl last optionals reverseList toList ; inherit (lib.strings) concatMapStringsSep concatStrings escape escapeNixIdentifier floatToString hasInfix splitString ; inherit (lib.trivial) isFunction # Note: not the builtin, considers `__functor` in attrsets. functionArgs # Note: not the builtin; considers `__functor` in attrsets. ; inherit (lib) assertMsg gvariant ; in rec { ## -- HELPER FUNCTIONS & DEFAULTS -- /* Convert a value to a sensible default string representation. * The builtin `toString` function has some strange defaults, * suitable for bash scripts but not much else. */ mkValueStringDefault = {}: v: let err = t: v: abort ("generators.mkValueStringDefault: " + "${t} not supported: ${toPretty {} v}"); in if isInt v then toString v # convert derivations to store paths else if isDerivation v then toString v # we default to not quoting strings else if isString v then v # isString returns "1", which is not a good default else if true == v then "true" # here it returns to "", which is even less of a good default else if false == v then "false" else if null == v then "null" # if you have lists you probably want to replace this else if isList v then err "lists" v # same as for lists, might want to replace else if isAttrs v then err "attrsets" v # functions can’t be printed of course else if isFunction v then err "functions" v # Floats currently can't be converted to precise strings, # condition warning on nix version once this isn't a problem anymore # See https://github.com/NixOS/nix/pull/3480 else if isFloat v then floatToString v else err "this value is" (toString v); /* Generate a line of key k and value v, separated by * character sep. If sep appears in k, it is escaped. * Helper for synaxes with different separators. * * mkValueString specifies how values should be formatted. * * mkKeyValueDefault {} ":" "f:oo" "bar" * > "f\:oo:bar" */ mkKeyValueDefault = { mkValueString ? mkValueStringDefault {} }: sep: k: v: "${escape [sep] k}${sep}${mkValueString v}"; ## -- FILE FORMAT GENERATORS -- /* Generate a key-value-style config file from an attrset. * * mkKeyValue is the same as in toINI. */ toKeyValue = { mkKeyValue ? mkKeyValueDefault {} "=", listsAsDuplicateKeys ? false, indent ? "" }: let mkLine = k: v: indent + mkKeyValue k v + "\n"; mkLines = if listsAsDuplicateKeys then k: v: map (mkLine k) (if isList v then v else [v]) else k: v: [ (mkLine k v) ]; in attrs: concatStrings (concatLists (libAttr.mapAttrsToList mkLines attrs)); /* Generate an INI-style config file from an * attrset of sections to an attrset of key-value pairs. * * generators.toINI {} { * foo = { hi = "${pkgs.hello}"; ciao = "bar"; }; * baz = { "also, integers" = 42; }; * } * *> [baz] *> also, integers=42 *> *> [foo] *> ciao=bar *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10 * * The mk* configuration attributes can generically change * the way sections and key-value strings are generated. * * For more examples see the test cases in ./tests/misc.nix. */ toINI = { # apply transformations (e.g. escapes) to section names mkSectionName ? (name: escape [ "[" "]" ] name), # format a setting line from key and value mkKeyValue ? mkKeyValueDefault {} "=", # allow lists as values for duplicate keys listsAsDuplicateKeys ? false }: attrsOfAttrs: let # map function to string for each key val mapAttrsToStringsSep = sep: mapFn: attrs: concatStringsSep sep (libAttr.mapAttrsToList mapFn attrs); mkSection = sectName: sectValues: '' [${mkSectionName sectName}] '' + toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } sectValues; in # map input to ini sections mapAttrsToStringsSep "\n" mkSection attrsOfAttrs; /* Generate an INI-style config file from an attrset * specifying the global section (no header), and an * attrset of sections to an attrset of key-value pairs. * * generators.toINIWithGlobalSection {} { * globalSection = { * someGlobalKey = "hi"; * }; * sections = { * foo = { hi = "${pkgs.hello}"; ciao = "bar"; }; * baz = { "also, integers" = 42; }; * } * *> someGlobalKey=hi *> *> [baz] *> also, integers=42 *> *> [foo] *> ciao=bar *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10 * * The mk* configuration attributes can generically change * the way sections and key-value strings are generated. * * For more examples see the test cases in ./tests/misc.nix. * * If you don’t need a global section, you can also use * `generators.toINI` directly, which only takes * the part in `sections`. */ toINIWithGlobalSection = { # apply transformations (e.g. escapes) to section names mkSectionName ? (name: escape [ "[" "]" ] name), # format a setting line from key and value mkKeyValue ? mkKeyValueDefault {} "=", # allow lists as values for duplicate keys listsAsDuplicateKeys ? false }: { globalSection, sections ? {} }: ( if globalSection == {} then "" else (toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } globalSection) + "\n") + (toINI { inherit mkSectionName mkKeyValue listsAsDuplicateKeys; } sections); /* Generate a git-config file from an attrset. * * It has two major differences from the regular INI format: * * 1. values are indented with tabs * 2. sections can have sub-sections * * generators.toGitINI { * url."ssh://git@github.com/".insteadOf = "https://github.com"; * user.name = "edolstra"; * } * *> [url "ssh://git@github.com/"] *> insteadOf = "https://github.com" *> *> [user] *> name = "edolstra" */ toGitINI = attrs: let mkSectionName = name: let containsQuote = hasInfix ''"'' name; sections = splitString "." name; section = head sections; subsections = tail sections; subsection = concatStringsSep "." subsections; in if containsQuote || subsections == [ ] then name else ''${section} "${subsection}"''; mkValueString = v: let escapedV = '' "${ replaceStrings [ "\n" " " ''"'' "\\" ] [ "\\n" "\\t" ''\"'' "\\\\" ] v }"''; in mkValueStringDefault { } (if isString v then escapedV else v); # generation for multiple ini values mkKeyValue = k: v: let mkKeyValue = mkKeyValueDefault { inherit mkValueString; } " = " k; in concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (toList v)); # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI gitFlattenAttrs = let recurse = path: value: if isAttrs value && !isDerivation value then mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value else if length path > 1 then { ${concatStringsSep "." (reverseList (tail path))}.${head path} = value; } else { ${head path} = value; }; in attrs: foldl recursiveUpdate { } (flatten (recurse [ ] attrs)); toINI_ = toINI { inherit mkKeyValue mkSectionName; }; in toINI_ (gitFlattenAttrs attrs); # mkKeyValueDefault wrapper that handles dconf INI quirks. # The main differences of the format is that it requires strings to be quoted. mkDconfKeyValue = mkKeyValueDefault { mkValueString = v: toString (gvariant.mkValue v); } "="; # Generates INI in dconf keyfile style. See https://help.gnome.org/admin/system-admin-guide/stable/dconf-keyfiles.html.en # for details. toDconfINI = toINI { mkKeyValue = mkDconfKeyValue; }; /* Generates JSON from an arbitrary (non-function) value. * For more information see the documentation of the builtin. */ toJSON = {}: builtins.toJSON; /* YAML has been a strict superset of JSON since 1.2, so we * use toJSON. Before it only had a few differences referring * to implicit typing rules, so it should work with older * parsers as well. */ toYAML = toJSON; withRecursion = { /* If this option is not null, the given value will stop evaluating at a certain depth */ depthLimit /* If this option is true, an error will be thrown, if a certain given depth is exceeded */ , throwOnDepthLimit ? true }: assert isInt depthLimit; let specialAttrs = [ "__functor" "__functionArgs" "__toString" "__pretty" ]; stepIntoAttr = evalNext: name: if elem name specialAttrs then id else evalNext; transform = depth: if depthLimit != null && depth > depthLimit then if throwOnDepthLimit then throw "Exceeded maximum eval-depth limit of ${toString depthLimit} while trying to evaluate with `generators.withRecursion'!" else const "" else id; mapAny = depth: v: let evalNext = x: mapAny (depth + 1) (transform (depth + 1) x); in if isAttrs v then mapAttrs (stepIntoAttr evalNext) v else if isList v then map evalNext v else transform (depth + 1) v; in mapAny 0; /* Pretty print a value, akin to `builtins.trace`. * Should probably be a builtin as well. * The pretty-printed string should be suitable for rendering default values * in the NixOS manual. In particular, it should be as close to a valid Nix expression * as possible. */ toPretty = { /* If this option is true, attrsets like { __pretty = fn; val = …; } will use fn to convert val to a pretty printed representation. (This means fn is type Val -> String.) */ allowPrettyValues ? false, /* If this option is true, the output is indented with newlines for attribute sets and lists */ multiline ? true, /* Initial indentation level */ indent ? "" }: let go = indent: v: let introSpace = if multiline then "\n${indent} " else " "; outroSpace = if multiline then "\n${indent}" else " "; in if isInt v then toString v # toString loses precision on floats, so we use toJSON instead. This isn't perfect # as the resulting string may not parse back as a float (e.g. 42, 1e-06), but for # pretty-printing purposes this is acceptable. else if isFloat v then builtins.toJSON v else if isString v then let lines = filter (v: ! isList v) (split "\n" v); escapeSingleline = escape [ "\\" "\"" "\${" ]; escapeMultiline = replaceStrings [ "\${" "''" ] [ "''\${" "'''" ]; singlelineResult = "\"" + concatStringsSep "\\n" (map escapeSingleline lines) + "\""; multilineResult = let escapedLines = map escapeMultiline lines; # The last line gets a special treatment: if it's empty, '' is on its own line at the "outer" # indentation level. Otherwise, '' is appended to the last line. lastLine = last escapedLines; in "''" + introSpace + concatStringsSep introSpace (init escapedLines) + (if lastLine == "" then outroSpace else introSpace + lastLine) + "''"; in if multiline && length lines > 1 then multilineResult else singlelineResult else if true == v then "true" else if false == v then "false" else if null == v then "null" else if isPath v then toString v else if isList v then if v == [] then "[ ]" else "[" + introSpace + concatMapStringsSep introSpace (go (indent + " ")) v + outroSpace + "]" else if isFunction v then let fna = functionArgs v; showFnas = concatStringsSep ", " (libAttr.mapAttrsToList (name: hasDefVal: if hasDefVal then name + "?" else name) fna); in if fna == {} then "" else "" else if isAttrs v then # apply pretty values if allowed if allowPrettyValues && v ? __pretty && v ? val then v.__pretty v.val else if v == {} then "{ }" else if v ? type && v.type == "derivation" then "" else "{" + introSpace + concatStringsSep introSpace (libAttr.mapAttrsToList (name: value: "${escapeNixIdentifier name} = ${ addErrorContext "while evaluating an attribute `${name}`" (go (indent + " ") value) };") v) + outroSpace + "}" else abort "generators.toPretty: should never happen (v = ${v})"; in go indent; # PLIST handling toPlist = {}: v: let expr = ind: x: if x == null then "" else if isBool x then bool ind x else if isInt x then int ind x else if isString x then str ind x else if isList x then list ind x else if isAttrs x then attrs ind x else if isPath x then str ind (toString x) else if isFloat x then float ind x else abort "generators.toPlist: should never happen (v = ${v})"; literal = ind: x: ind + x; bool = ind: x: literal ind (if x then "" else ""); int = ind: x: literal ind "${toString x}"; str = ind: x: literal ind "${x}"; key = ind: x: literal ind "${x}"; float = ind: x: literal ind "${toString x}"; indent = ind: expr "\t${ind}"; item = ind: concatMapStringsSep "\n" (indent ind); list = ind: x: concatStringsSep "\n" [ (literal ind "") (item ind x) (literal ind "") ]; attrs = ind: x: concatStringsSep "\n" [ (literal ind "") (attr ind x) (literal ind "") ]; attr = let attrFilter = name: value: name != "_module" && value != null; in ind: x: concatStringsSep "\n" (flatten (mapAttrsToList (name: value: optionals (attrFilter name value) [ (key "\t${ind}" name) (expr "\t${ind}" value) ]) x)); in '' ${expr "" v} ''; /* Translate a simple Nix expression to Dhall notation. * Note that integers are translated to Integer and never * the Natural type. */ toDhall = { }@args: v: let concatItems = concatStringsSep ", "; in if isAttrs v then "{ ${ concatItems (mapAttrsToList (key: value: "${key} = ${toDhall args value}") v) } }" else if isList v then "[ ${concatItems (map (toDhall args) v)} ]" else if isInt v then "${if v < 0 then "" else "+"}${toString v}" else if isBool v then (if v then "True" else "False") else if isFunction v then abort "generators.toDhall: cannot convert a function to Dhall" else if v == null then abort "generators.toDhall: cannot convert a null to Dhall" else builtins.toJSON v; /* Translate a simple Nix expression to Lua representation with occasional Lua-inlines that can be constructed by mkLuaInline function. Configuration: * multiline - by default is true which results in indented block-like view. * indent - initial indent. * asBindings - by default generate single value, but with this use attrset to set global vars. Attention: Regardless of multiline parameter there is no trailing newline. Example: generators.toLua {} { cmd = [ "typescript-language-server" "--stdio" ]; settings.workspace.library = mkLuaInline ''vim.api.nvim_get_runtime_file("", true)''; } -> { ["cmd"] = { "typescript-language-server", "--stdio" }, ["settings"] = { ["workspace"] = { ["library"] = (vim.api.nvim_get_runtime_file("", true)) } } } Type: toLua :: AttrSet -> Any -> String */ toLua = { /* If this option is true, the output is indented with newlines for attribute sets and lists */ multiline ? true, /* Initial indentation level */ indent ? "", /* Interpret as variable bindings */ asBindings ? false, }@args: v: let innerIndent = "${indent} "; introSpace = if multiline then "\n${innerIndent}" else " "; outroSpace = if multiline then "\n${indent}" else " "; innerArgs = args // { indent = if asBindings then indent else innerIndent; asBindings = false; }; concatItems = concatStringsSep ",${introSpace}"; isLuaInline = { _type ? null, ... }: _type == "lua-inline"; generatedBindings = assert assertMsg (badVarNames == []) "Bad Lua var names: ${toPretty {} badVarNames}"; concatStrings ( mapAttrsToList (key: value: "${indent}${key} = ${toLua innerArgs value}\n") v ); # https://en.wikibooks.org/wiki/Lua_Programming/variable#Variable_names matchVarName = match "[[:alpha:]_][[:alnum:]_]*(\\.[[:alpha:]_][[:alnum:]_]*)*"; badVarNames = filter (name: matchVarName name == null) (attrNames v); in if asBindings then generatedBindings else if v == null then "nil" else if isInt v || isFloat v || isString v || isBool v then builtins.toJSON v else if isList v then (if v == [ ] then "{}" else "{${introSpace}${concatItems (map (value: "${toLua innerArgs value}") v)}${outroSpace}}") else if isAttrs v then ( if isLuaInline v then "(${v.expr})" else if v == { } then "{}" else if libAttr.isDerivation v then ''"${toString v}"'' else "{${introSpace}${concatItems ( mapAttrsToList (key: value: "[${builtins.toJSON key}] = ${toLua innerArgs value}") v )}${outroSpace}}" ) else abort "generators.toLua: type ${typeOf v} is unsupported"; /* Mark string as Lua expression to be inlined when processed by toLua. Type: mkLuaInline :: String -> AttrSet */ mkLuaInline = expr: { _type = "lua-inline"; inherit expr; }; }