at 24.11-pre 20 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 }: 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 dont 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}