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.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 # we default to not quoting strings
39 else if isString v then v
40 # isString returns "1", which is not a good default
41 else if true == v then "true"
42 # here it returns to "", which is even less of a good default
43 else if false == v then "false"
44 else if null == v then "null"
45 # if you have lists you probably want to replace this
46 else if isList v then err "lists" v
47 # same as for lists, might want to replace
48 else if isAttrs v then err "attrsets" v
49 # functions can’t be printed of course
50 else if isFunction v then err "functions" v
51 # Floats currently can't be converted to precise strings,
52 # condition warning on nix version once this isn't a problem anymore
53 # See https://github.com/NixOS/nix/pull/3480
54 else if isFloat v then libStr.floatToString v
55 else err "this value is" (toString v);
56
57
58 /* Generate a line of key k and value v, separated by
59 * character sep. If sep appears in k, it is escaped.
60 * Helper for synaxes with different separators.
61 *
62 * mkValueString specifies how values should be formatted.
63 *
64 * mkKeyValueDefault {} ":" "f:oo" "bar"
65 * > "f\:oo:bar"
66 */
67 mkKeyValueDefault = {
68 mkValueString ? mkValueStringDefault {}
69 }: sep: k: v:
70 "${libStr.escape [sep] k}${sep}${mkValueString v}";
71
72
73 ## -- FILE FORMAT GENERATORS --
74
75
76 /* Generate a key-value-style config file from an attrset.
77 *
78 * mkKeyValue is the same as in toINI.
79 */
80 toKeyValue = {
81 mkKeyValue ? mkKeyValueDefault {} "=",
82 listsAsDuplicateKeys ? false
83 }:
84 let mkLine = k: v: mkKeyValue k v + "\n";
85 mkLines = if listsAsDuplicateKeys
86 then k: v: map (mkLine k) (if lib.isList v then v else [v])
87 else k: v: [ (mkLine k v) ];
88 in attrs: libStr.concatStrings (lib.concatLists (libAttr.mapAttrsToList mkLines attrs));
89
90
91 /* Generate an INI-style config file from an
92 * attrset of sections to an attrset of key-value pairs.
93 *
94 * generators.toINI {} {
95 * foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
96 * baz = { "also, integers" = 42; };
97 * }
98 *
99 *> [baz]
100 *> also, integers=42
101 *>
102 *> [foo]
103 *> ciao=bar
104 *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
105 *
106 * The mk* configuration attributes can generically change
107 * the way sections and key-value strings are generated.
108 *
109 * For more examples see the test cases in ./tests.nix.
110 */
111 toINI = {
112 # apply transformations (e.g. escapes) to section names
113 mkSectionName ? (name: libStr.escape [ "[" "]" ] name),
114 # format a setting line from key and value
115 mkKeyValue ? mkKeyValueDefault {} "=",
116 # allow lists as values for duplicate keys
117 listsAsDuplicateKeys ? false
118 }: attrsOfAttrs:
119 let
120 # map function to string for each key val
121 mapAttrsToStringsSep = sep: mapFn: attrs:
122 libStr.concatStringsSep sep
123 (libAttr.mapAttrsToList mapFn attrs);
124 mkSection = sectName: sectValues: ''
125 [${mkSectionName sectName}]
126 '' + toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } sectValues;
127 in
128 # map input to ini sections
129 mapAttrsToStringsSep "\n" mkSection attrsOfAttrs;
130
131 /* Generate a git-config file from an attrset.
132 *
133 * It has two major differences from the regular INI format:
134 *
135 * 1. values are indented with tabs
136 * 2. sections can have sub-sections
137 *
138 * generators.toGitINI {
139 * url."ssh://git@github.com/".insteadOf = "https://github.com";
140 * user.name = "edolstra";
141 * }
142 *
143 *> [url "ssh://git@github.com/"]
144 *> insteadOf = https://github.com/
145 *>
146 *> [user]
147 *> name = edolstra
148 */
149 toGitINI = attrs:
150 with builtins;
151 let
152 mkSectionName = name:
153 let
154 containsQuote = libStr.hasInfix ''"'' name;
155 sections = libStr.splitString "." name;
156 section = head sections;
157 subsections = tail sections;
158 subsection = concatStringsSep "." subsections;
159 in if containsQuote || subsections == [ ] then
160 name
161 else
162 ''${section} "${subsection}"'';
163
164 # generation for multiple ini values
165 mkKeyValue = k: v:
166 let mkKeyValue = mkKeyValueDefault { } " = " k;
167 in concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (lib.toList v));
168
169 # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI
170 gitFlattenAttrs = let
171 recurse = path: value:
172 if isAttrs value then
173 lib.mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value
174 else if length path > 1 then {
175 ${concatStringsSep "." (lib.reverseList (tail path))}.${head path} = value;
176 } else {
177 ${head path} = value;
178 };
179 in attrs: lib.foldl lib.recursiveUpdate { } (lib.flatten (recurse [ ] attrs));
180
181 toINI_ = toINI { inherit mkKeyValue mkSectionName; };
182 in
183 toINI_ (gitFlattenAttrs attrs);
184
185 /* Generates JSON from an arbitrary (non-function) value.
186 * For more information see the documentation of the builtin.
187 */
188 toJSON = {}: builtins.toJSON;
189
190
191 /* YAML has been a strict superset of JSON since 1.2, so we
192 * use toJSON. Before it only had a few differences referring
193 * to implicit typing rules, so it should work with older
194 * parsers as well.
195 */
196 toYAML = {}@args: toJSON args;
197
198
199 /* Pretty print a value, akin to `builtins.trace`.
200 * Should probably be a builtin as well.
201 */
202 toPretty = {
203 /* If this option is true, attrsets like { __pretty = fn; val = …; }
204 will use fn to convert val to a pretty printed representation.
205 (This means fn is type Val -> String.) */
206 allowPrettyValues ? false,
207 /* If this option is true, the output is indented with newlines for attribute sets and lists */
208 multiline ? true
209 }@args: let
210 go = indent: v: with builtins;
211 let isPath = v: typeOf v == "path";
212 introSpace = if multiline then "\n${indent} " else " ";
213 outroSpace = if multiline then "\n${indent}" else " ";
214 in if isInt v then toString v
215 else if isFloat v then "~${toString v}"
216 else if isString v then
217 let
218 # Separate a string into its lines
219 newlineSplits = filter (v: ! isList v) (builtins.split "\n" v);
220 # For a '' string terminated by a \n, which happens when the closing '' is on a new line
221 multilineResult = "''" + introSpace + concatStringsSep introSpace (lib.init newlineSplits) + outroSpace + "''";
222 # For a '' string not terminated by a \n, which happens when the closing '' is not on a new line
223 multilineResult' = "''" + introSpace + concatStringsSep introSpace newlineSplits + "''";
224 # For single lines, replace all newlines with their escaped representation
225 singlelineResult = "\"" + libStr.escape [ "\"" ] (concatStringsSep "\\n" newlineSplits) + "\"";
226 in if multiline && length newlineSplits > 1 then
227 if lib.last newlineSplits == "" then multilineResult else multilineResult'
228 else singlelineResult
229 else if true == v then "true"
230 else if false == v then "false"
231 else if null == v then "null"
232 else if isPath v then toString v
233 else if isList v then
234 if v == [] then "[ ]"
235 else "[" + introSpace
236 + libStr.concatMapStringsSep introSpace (go (indent + " ")) v
237 + outroSpace + "]"
238 else if isFunction v then
239 let fna = lib.functionArgs v;
240 showFnas = concatStringsSep ", " (libAttr.mapAttrsToList
241 (name: hasDefVal: if hasDefVal then name + "?" else name)
242 fna);
243 in if fna == {} then "<function>"
244 else "<function, args: {${showFnas}}>"
245 else if isAttrs v then
246 # apply pretty values if allowed
247 if attrNames v == [ "__pretty" "val" ] && allowPrettyValues
248 then v.__pretty v.val
249 else if v == {} then "{ }"
250 else if v ? type && v.type == "derivation" then
251 "<derivation ${v.drvPath}>"
252 else "{" + introSpace
253 + libStr.concatStringsSep introSpace (libAttr.mapAttrsToList
254 (name: value:
255 "${libStr.escapeNixIdentifier name} = ${go (indent + " ") value};") v)
256 + outroSpace + "}"
257 else abort "generators.toPretty: should never happen (v = ${v})";
258 in go "";
259
260 # PLIST handling
261 toPlist = {}: v: let
262 isFloat = builtins.isFloat or (x: false);
263 expr = ind: x: with builtins;
264 if x == null then "" else
265 if isBool x then bool ind x else
266 if isInt x then int ind x else
267 if isString x then str ind x else
268 if isList x then list ind x else
269 if isAttrs x then attrs ind x else
270 if isFloat x then float ind x else
271 abort "generators.toPlist: should never happen (v = ${v})";
272
273 literal = ind: x: ind + x;
274
275 bool = ind: x: literal ind (if x then "<true/>" else "<false/>");
276 int = ind: x: literal ind "<integer>${toString x}</integer>";
277 str = ind: x: literal ind "<string>${x}</string>";
278 key = ind: x: literal ind "<key>${x}</key>";
279 float = ind: x: literal ind "<real>${toString x}</real>";
280
281 indent = ind: expr "\t${ind}";
282
283 item = ind: libStr.concatMapStringsSep "\n" (indent ind);
284
285 list = ind: x: libStr.concatStringsSep "\n" [
286 (literal ind "<array>")
287 (item ind x)
288 (literal ind "</array>")
289 ];
290
291 attrs = ind: x: libStr.concatStringsSep "\n" [
292 (literal ind "<dict>")
293 (attr ind x)
294 (literal ind "</dict>")
295 ];
296
297 attr = let attrFilter = name: value: name != "_module" && value != null;
298 in ind: x: libStr.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList
299 (name: value: lib.optional (attrFilter name value) [
300 (key "\t${ind}" name)
301 (expr "\t${ind}" value)
302 ]) x));
303
304 in ''<?xml version="1.0" encoding="UTF-8"?>
305<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
306<plist version="1.0">
307${expr "" v}
308</plist>'';
309
310 /* Translate a simple Nix expression to Dhall notation.
311 * Note that integers are translated to Integer and never
312 * the Natural type.
313 */
314 toDhall = { }@args: v:
315 with builtins;
316 let concatItems = lib.strings.concatStringsSep ", ";
317 in if isAttrs v then
318 "{ ${
319 concatItems (lib.attrsets.mapAttrsToList
320 (key: value: "${key} = ${toDhall args value}") v)
321 } }"
322 else if isList v then
323 "[ ${concatItems (map (toDhall args) v)} ]"
324 else if isInt v then
325 "${if v < 0 then "" else "+"}${toString v}"
326 else if isBool v then
327 (if v then "True" else "False")
328 else if isFunction v then
329 abort "generators.toDhall: cannot convert a function to Dhall"
330 else if isNull v then
331 abort "generators.toDhall: cannot convert a null to Dhall"
332 else
333 builtins.toJSON v;
334}