at 21.11-pre 12 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.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}