at 23.11-beta 19 kB view raw
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 dont 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}