1/* Functions that generate widespread file
2 * formats from nix data structures.
3 *
4 * They all follow a similar interface:
5 * generator { config-attrs } data
6 *
7 * `config-attrs` are “holes” in the generators
8 * with sensible default implementations that
9 * can be overwritten. The default implementations
10 * are mostly generators themselves, called with
11 * their respective default values; they can be reused.
12 *
13 * Tests can be found in ./tests/misc.nix
14 * Documentation in the manual, #sec-generators
15 */
16{ lib }:
17with (lib).trivial;
18let
19 libStr = lib.strings;
20 libAttr = lib.attrsets;
21
22 inherit (lib) isFunction;
23in
24
25rec {
26
27 ## -- HELPER FUNCTIONS & DEFAULTS --
28
29 /* Convert a value to a sensible default string representation.
30 * The builtin `toString` function has some strange defaults,
31 * suitable for bash scripts but not much else.
32 */
33 mkValueStringDefault = {}: v: with builtins;
34 let err = t: v: abort
35 ("generators.mkValueStringDefault: " +
36 "${t} not supported: ${toPretty {} v}");
37 in if isInt v then toString v
38 # convert derivations to store paths
39 else if lib.isDerivation v then toString v
40 # we default to not quoting strings
41 else if isString v then v
42 # isString returns "1", which is not a good default
43 else if true == v then "true"
44 # here it returns to "", which is even less of a good default
45 else if false == v then "false"
46 else if null == v then "null"
47 # if you have lists you probably want to replace this
48 else if isList v then err "lists" v
49 # same as for lists, might want to replace
50 else if isAttrs v then err "attrsets" v
51 # functions can’t be printed of course
52 else if isFunction v then err "functions" v
53 # Floats currently can't be converted to precise strings,
54 # condition warning on nix version once this isn't a problem anymore
55 # See https://github.com/NixOS/nix/pull/3480
56 else if isFloat v then libStr.floatToString v
57 else err "this value is" (toString v);
58
59
60 /* Generate a line of key k and value v, separated by
61 * character sep. If sep appears in k, it is escaped.
62 * Helper for synaxes with different separators.
63 *
64 * mkValueString specifies how values should be formatted.
65 *
66 * mkKeyValueDefault {} ":" "f:oo" "bar"
67 * > "f\:oo:bar"
68 */
69 mkKeyValueDefault = {
70 mkValueString ? mkValueStringDefault {}
71 }: sep: k: v:
72 "${libStr.escape [sep] k}${sep}${mkValueString v}";
73
74
75 ## -- FILE FORMAT GENERATORS --
76
77
78 /* Generate a key-value-style config file from an attrset.
79 *
80 * mkKeyValue is the same as in toINI.
81 */
82 toKeyValue = {
83 mkKeyValue ? mkKeyValueDefault {} "=",
84 listsAsDuplicateKeys ? false,
85 indent ? ""
86 }:
87 let mkLine = k: v: indent + mkKeyValue k v + "\n";
88 mkLines = if listsAsDuplicateKeys
89 then k: v: map (mkLine k) (if lib.isList v then v else [v])
90 else k: v: [ (mkLine k v) ];
91 in attrs: libStr.concatStrings (lib.concatLists (libAttr.mapAttrsToList mkLines attrs));
92
93
94 /* Generate an INI-style config file from an
95 * attrset of sections to an attrset of key-value pairs.
96 *
97 * generators.toINI {} {
98 * foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
99 * baz = { "also, integers" = 42; };
100 * }
101 *
102 *> [baz]
103 *> also, integers=42
104 *>
105 *> [foo]
106 *> ciao=bar
107 *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
108 *
109 * The mk* configuration attributes can generically change
110 * the way sections and key-value strings are generated.
111 *
112 * For more examples see the test cases in ./tests/misc.nix.
113 */
114 toINI = {
115 # apply transformations (e.g. escapes) to section names
116 mkSectionName ? (name: libStr.escape [ "[" "]" ] name),
117 # format a setting line from key and value
118 mkKeyValue ? mkKeyValueDefault {} "=",
119 # allow lists as values for duplicate keys
120 listsAsDuplicateKeys ? false
121 }: attrsOfAttrs:
122 let
123 # map function to string for each key val
124 mapAttrsToStringsSep = sep: mapFn: attrs:
125 libStr.concatStringsSep sep
126 (libAttr.mapAttrsToList mapFn attrs);
127 mkSection = sectName: sectValues: ''
128 [${mkSectionName sectName}]
129 '' + toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } sectValues;
130 in
131 # map input to ini sections
132 mapAttrsToStringsSep "\n" mkSection attrsOfAttrs;
133
134 /* Generate an INI-style config file from an attrset
135 * specifying the global section (no header), and an
136 * attrset of sections to an attrset of key-value pairs.
137 *
138 * generators.toINIWithGlobalSection {} {
139 * globalSection = {
140 * someGlobalKey = "hi";
141 * };
142 * sections = {
143 * foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
144 * baz = { "also, integers" = 42; };
145 * }
146 *
147 *> someGlobalKey=hi
148 *>
149 *> [baz]
150 *> also, integers=42
151 *>
152 *> [foo]
153 *> ciao=bar
154 *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
155 *
156 * The mk* configuration attributes can generically change
157 * the way sections and key-value strings are generated.
158 *
159 * For more examples see the test cases in ./tests/misc.nix.
160 *
161 * If you don’t need a global section, you can also use
162 * `generators.toINI` directly, which only takes
163 * the part in `sections`.
164 */
165 toINIWithGlobalSection = {
166 # apply transformations (e.g. escapes) to section names
167 mkSectionName ? (name: libStr.escape [ "[" "]" ] name),
168 # format a setting line from key and value
169 mkKeyValue ? mkKeyValueDefault {} "=",
170 # allow lists as values for duplicate keys
171 listsAsDuplicateKeys ? false
172 }: { globalSection, sections ? {} }:
173 ( if globalSection == {}
174 then ""
175 else (toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } globalSection)
176 + "\n")
177 + (toINI { inherit mkSectionName mkKeyValue listsAsDuplicateKeys; } sections);
178
179 /* Generate a git-config file from an attrset.
180 *
181 * It has two major differences from the regular INI format:
182 *
183 * 1. values are indented with tabs
184 * 2. sections can have sub-sections
185 *
186 * generators.toGitINI {
187 * url."ssh://git@github.com/".insteadOf = "https://github.com";
188 * user.name = "edolstra";
189 * }
190 *
191 *> [url "ssh://git@github.com/"]
192 *> insteadOf = "https://github.com"
193 *>
194 *> [user]
195 *> name = "edolstra"
196 */
197 toGitINI = attrs:
198 with builtins;
199 let
200 mkSectionName = name:
201 let
202 containsQuote = libStr.hasInfix ''"'' name;
203 sections = libStr.splitString "." name;
204 section = head sections;
205 subsections = tail sections;
206 subsection = concatStringsSep "." subsections;
207 in if containsQuote || subsections == [ ] then
208 name
209 else
210 ''${section} "${subsection}"'';
211
212 mkValueString = v:
213 let
214 escapedV = ''
215 "${
216 replaceStrings [ "\n" " " ''"'' "\\" ] [ "\\n" "\\t" ''\"'' "\\\\" ] v
217 }"'';
218 in mkValueStringDefault { } (if isString v then escapedV else v);
219
220 # generation for multiple ini values
221 mkKeyValue = k: v:
222 let mkKeyValue = mkKeyValueDefault { inherit mkValueString; } " = " k;
223 in concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (lib.toList v));
224
225 # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI
226 gitFlattenAttrs = let
227 recurse = path: value:
228 if isAttrs value && !lib.isDerivation value then
229 lib.mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value
230 else if length path > 1 then {
231 ${concatStringsSep "." (lib.reverseList (tail path))}.${head path} = value;
232 } else {
233 ${head path} = value;
234 };
235 in attrs: lib.foldl lib.recursiveUpdate { } (lib.flatten (recurse [ ] attrs));
236
237 toINI_ = toINI { inherit mkKeyValue mkSectionName; };
238 in
239 toINI_ (gitFlattenAttrs attrs);
240
241 # mkKeyValueDefault wrapper that handles dconf INI quirks.
242 # The main differences of the format is that it requires strings to be quoted.
243 mkDconfKeyValue = mkKeyValueDefault { mkValueString = v: toString (lib.gvariant.mkValue v); } "=";
244
245 # Generates INI in dconf keyfile style. See https://help.gnome.org/admin/system-admin-guide/stable/dconf-keyfiles.html.en
246 # for details.
247 toDconfINI = toINI { mkKeyValue = mkDconfKeyValue; };
248
249 /* Generates JSON from an arbitrary (non-function) value.
250 * For more information see the documentation of the builtin.
251 */
252 toJSON = {}: builtins.toJSON;
253
254
255 /* YAML has been a strict superset of JSON since 1.2, so we
256 * use toJSON. Before it only had a few differences referring
257 * to implicit typing rules, so it should work with older
258 * parsers as well.
259 */
260 toYAML = toJSON;
261
262 withRecursion =
263 {
264 /* If this option is not null, the given value will stop evaluating at a certain depth */
265 depthLimit
266 /* If this option is true, an error will be thrown, if a certain given depth is exceeded */
267 , throwOnDepthLimit ? true
268 }:
269 assert builtins.isInt depthLimit;
270 let
271 specialAttrs = [
272 "__functor"
273 "__functionArgs"
274 "__toString"
275 "__pretty"
276 ];
277 stepIntoAttr = evalNext: name:
278 if builtins.elem name specialAttrs
279 then id
280 else evalNext;
281 transform = depth:
282 if depthLimit != null && depth > depthLimit then
283 if throwOnDepthLimit
284 then throw "Exceeded maximum eval-depth limit of ${toString depthLimit} while trying to evaluate with `generators.withRecursion'!"
285 else const "<unevaluated>"
286 else id;
287 mapAny = with builtins; depth: v:
288 let
289 evalNext = x: mapAny (depth + 1) (transform (depth + 1) x);
290 in
291 if isAttrs v then mapAttrs (stepIntoAttr evalNext) v
292 else if isList v then map evalNext v
293 else transform (depth + 1) v;
294 in
295 mapAny 0;
296
297 /* Pretty print a value, akin to `builtins.trace`.
298 * Should probably be a builtin as well.
299 * The pretty-printed string should be suitable for rendering default values
300 * in the NixOS manual. In particular, it should be as close to a valid Nix expression
301 * as possible.
302 */
303 toPretty = {
304 /* If this option is true, attrsets like { __pretty = fn; val = …; }
305 will use fn to convert val to a pretty printed representation.
306 (This means fn is type Val -> String.) */
307 allowPrettyValues ? false,
308 /* If this option is true, the output is indented with newlines for attribute sets and lists */
309 multiline ? true,
310 /* Initial indentation level */
311 indent ? ""
312 }:
313 let
314 go = indent: v: with builtins;
315 let isPath = v: typeOf v == "path";
316 introSpace = if multiline then "\n${indent} " else " ";
317 outroSpace = if multiline then "\n${indent}" else " ";
318 in if isInt v then toString v
319 # toString loses precision on floats, so we use toJSON instead. This isn't perfect
320 # as the resulting string may not parse back as a float (e.g. 42, 1e-06), but for
321 # pretty-printing purposes this is acceptable.
322 else if isFloat v then builtins.toJSON v
323 else if isString v then
324 let
325 lines = filter (v: ! isList v) (builtins.split "\n" v);
326 escapeSingleline = libStr.escape [ "\\" "\"" "\${" ];
327 escapeMultiline = libStr.replaceStrings [ "\${" "''" ] [ "''\${" "'''" ];
328 singlelineResult = "\"" + concatStringsSep "\\n" (map escapeSingleline lines) + "\"";
329 multilineResult = let
330 escapedLines = map escapeMultiline lines;
331 # The last line gets a special treatment: if it's empty, '' is on its own line at the "outer"
332 # indentation level. Otherwise, '' is appended to the last line.
333 lastLine = lib.last escapedLines;
334 in "''" + introSpace + concatStringsSep introSpace (lib.init escapedLines)
335 + (if lastLine == "" then outroSpace else introSpace + lastLine) + "''";
336 in
337 if multiline && length lines > 1 then multilineResult else singlelineResult
338 else if true == v then "true"
339 else if false == v then "false"
340 else if null == v then "null"
341 else if isPath v then toString v
342 else if isList v then
343 if v == [] then "[ ]"
344 else "[" + introSpace
345 + libStr.concatMapStringsSep introSpace (go (indent + " ")) v
346 + outroSpace + "]"
347 else if isFunction v then
348 let fna = lib.functionArgs v;
349 showFnas = concatStringsSep ", " (libAttr.mapAttrsToList
350 (name: hasDefVal: if hasDefVal then name + "?" else name)
351 fna);
352 in if fna == {} then "<function>"
353 else "<function, args: {${showFnas}}>"
354 else if isAttrs v then
355 # apply pretty values if allowed
356 if allowPrettyValues && v ? __pretty && v ? val
357 then v.__pretty v.val
358 else if v == {} then "{ }"
359 else if v ? type && v.type == "derivation" then
360 "<derivation ${v.name or "???"}>"
361 else "{" + introSpace
362 + libStr.concatStringsSep introSpace (libAttr.mapAttrsToList
363 (name: value:
364 "${libStr.escapeNixIdentifier name} = ${
365 builtins.addErrorContext "while evaluating an attribute `${name}`"
366 (go (indent + " ") value)
367 };") v)
368 + outroSpace + "}"
369 else abort "generators.toPretty: should never happen (v = ${v})";
370 in go indent;
371
372 # PLIST handling
373 toPlist = {}: v: let
374 isFloat = builtins.isFloat or (x: false);
375 isPath = x: builtins.typeOf x == "path";
376 expr = ind: x: with builtins;
377 if x == null then "" else
378 if isBool x then bool ind x else
379 if isInt x then int ind x else
380 if isString x then str ind x else
381 if isList x then list ind x else
382 if isAttrs x then attrs ind x else
383 if isPath x then str ind (toString x) else
384 if isFloat x then float ind x else
385 abort "generators.toPlist: should never happen (v = ${v})";
386
387 literal = ind: x: ind + x;
388
389 bool = ind: x: literal ind (if x then "<true/>" else "<false/>");
390 int = ind: x: literal ind "<integer>${toString x}</integer>";
391 str = ind: x: literal ind "<string>${x}</string>";
392 key = ind: x: literal ind "<key>${x}</key>";
393 float = ind: x: literal ind "<real>${toString x}</real>";
394
395 indent = ind: expr "\t${ind}";
396
397 item = ind: libStr.concatMapStringsSep "\n" (indent ind);
398
399 list = ind: x: libStr.concatStringsSep "\n" [
400 (literal ind "<array>")
401 (item ind x)
402 (literal ind "</array>")
403 ];
404
405 attrs = ind: x: libStr.concatStringsSep "\n" [
406 (literal ind "<dict>")
407 (attr ind x)
408 (literal ind "</dict>")
409 ];
410
411 attr = let attrFilter = name: value: name != "_module" && value != null;
412 in ind: x: libStr.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList
413 (name: value: lib.optionals (attrFilter name value) [
414 (key "\t${ind}" name)
415 (expr "\t${ind}" value)
416 ]) x));
417
418 in ''<?xml version="1.0" encoding="UTF-8"?>
419<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
420<plist version="1.0">
421${expr "" v}
422</plist>'';
423
424 /* Translate a simple Nix expression to Dhall notation.
425 * Note that integers are translated to Integer and never
426 * the Natural type.
427 */
428 toDhall = { }@args: v:
429 with builtins;
430 let concatItems = lib.strings.concatStringsSep ", ";
431 in if isAttrs v then
432 "{ ${
433 concatItems (lib.attrsets.mapAttrsToList
434 (key: value: "${key} = ${toDhall args value}") v)
435 } }"
436 else if isList v then
437 "[ ${concatItems (map (toDhall args) v)} ]"
438 else if isInt v then
439 "${if v < 0 then "" else "+"}${toString v}"
440 else if isBool v then
441 (if v then "True" else "False")
442 else if isFunction v then
443 abort "generators.toDhall: cannot convert a function to Dhall"
444 else if v == null then
445 abort "generators.toDhall: cannot convert a null to Dhall"
446 else
447 builtins.toJSON v;
448
449 /*
450 Translate a simple Nix expression to Lua representation with occasional
451 Lua-inlines that can be constructed by mkLuaInline function.
452
453 Configuration:
454 * multiline - by default is true which results in indented block-like view.
455 * indent - initial indent.
456 * asBindings - by default generate single value, but with this use attrset to set global vars.
457
458 Attention:
459 Regardless of multiline parameter there is no trailing newline.
460
461 Example:
462 generators.toLua {}
463 {
464 cmd = [ "typescript-language-server" "--stdio" ];
465 settings.workspace.library = mkLuaInline ''vim.api.nvim_get_runtime_file("", true)'';
466 }
467 ->
468 {
469 ["cmd"] = {
470 "typescript-language-server",
471 "--stdio"
472 },
473 ["settings"] = {
474 ["workspace"] = {
475 ["library"] = (vim.api.nvim_get_runtime_file("", true))
476 }
477 }
478 }
479
480 Type:
481 toLua :: AttrSet -> Any -> String
482 */
483 toLua = {
484 /* If this option is true, the output is indented with newlines for attribute sets and lists */
485 multiline ? true,
486 /* Initial indentation level */
487 indent ? "",
488 /* Interpret as variable bindings */
489 asBindings ? false,
490 }@args: v:
491 with builtins;
492 let
493 innerIndent = "${indent} ";
494 introSpace = if multiline then "\n${innerIndent}" else " ";
495 outroSpace = if multiline then "\n${indent}" else " ";
496 innerArgs = args // {
497 indent = if asBindings then indent else innerIndent;
498 asBindings = false;
499 };
500 concatItems = concatStringsSep ",${introSpace}";
501 isLuaInline = { _type ? null, ... }: _type == "lua-inline";
502
503 generatedBindings =
504 assert lib.assertMsg (badVarNames == []) "Bad Lua var names: ${toPretty {} badVarNames}";
505 libStr.concatStrings (
506 lib.attrsets.mapAttrsToList (key: value: "${indent}${key} = ${toLua innerArgs value}\n") v
507 );
508
509 # https://en.wikibooks.org/wiki/Lua_Programming/variable#Variable_names
510 matchVarName = match "[[:alpha:]_][[:alnum:]_]*(\\.[[:alpha:]_][[:alnum:]_]*)*";
511 badVarNames = filter (name: matchVarName name == null) (attrNames v);
512 in
513 if asBindings then
514 generatedBindings
515 else if v == null then
516 "nil"
517 else if isInt v || isFloat v || isString v || isBool v then
518 builtins.toJSON v
519 else if isList v then
520 (if v == [ ] then "{}" else
521 "{${introSpace}${concatItems (map (value: "${toLua innerArgs value}") v)}${outroSpace}}")
522 else if isAttrs v then
523 (
524 if isLuaInline v then
525 "(${v.expr})"
526 else if v == { } then
527 "{}"
528 else
529 "{${introSpace}${concatItems (
530 lib.attrsets.mapAttrsToList (key: value: "[${builtins.toJSON key}] = ${toLua innerArgs value}") v
531 )}${outroSpace}}"
532 )
533 else
534 abort "generators.toLua: type ${typeOf v} is unsupported";
535
536 /*
537 Mark string as Lua expression to be inlined when processed by toLua.
538
539 Type:
540 mkLuaInline :: String -> AttrSet
541 */
542 mkLuaInline = expr: { _type = "lua-inline"; inherit expr; };
543}