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