at 21.11-pre 22 kB view raw
1/* String manipulation functions. */ 2{ lib }: 3let 4 5inherit (builtins) length; 6 7in 8 9rec { 10 11 inherit (builtins) 12 compareVersions 13 elem 14 elemAt 15 filter 16 fromJSON 17 head 18 isInt 19 isList 20 isString 21 match 22 parseDrvName 23 readFile 24 replaceStrings 25 split 26 storeDir 27 stringLength 28 substring 29 tail 30 toJSON 31 typeOf 32 unsafeDiscardStringContext 33 ; 34 35 /* Concatenate a list of strings. 36 37 Type: concatStrings :: [string] -> string 38 39 Example: 40 concatStrings ["foo" "bar"] 41 => "foobar" 42 */ 43 concatStrings = builtins.concatStringsSep ""; 44 45 /* Map a function over a list and concatenate the resulting strings. 46 47 Type: concatMapStrings :: (a -> string) -> [a] -> string 48 49 Example: 50 concatMapStrings (x: "a" + x) ["foo" "bar"] 51 => "afooabar" 52 */ 53 concatMapStrings = f: list: concatStrings (map f list); 54 55 /* Like `concatMapStrings` except that the f functions also gets the 56 position as a parameter. 57 58 Type: concatImapStrings :: (int -> a -> string) -> [a] -> string 59 60 Example: 61 concatImapStrings (pos: x: "${toString pos}-${x}") ["foo" "bar"] 62 => "1-foo2-bar" 63 */ 64 concatImapStrings = f: list: concatStrings (lib.imap1 f list); 65 66 /* Place an element between each element of a list 67 68 Type: intersperse :: a -> [a] -> [a] 69 70 Example: 71 intersperse "/" ["usr" "local" "bin"] 72 => ["usr" "/" "local" "/" "bin"]. 73 */ 74 intersperse = 75 # Separator to add between elements 76 separator: 77 # Input list 78 list: 79 if list == [] || length list == 1 80 then list 81 else tail (lib.concatMap (x: [separator x]) list); 82 83 /* Concatenate a list of strings with a separator between each element 84 85 Type: concatStringsSep :: string -> [string] -> string 86 87 Example: 88 concatStringsSep "/" ["usr" "local" "bin"] 89 => "usr/local/bin" 90 */ 91 concatStringsSep = builtins.concatStringsSep or (separator: list: 92 concatStrings (intersperse separator list)); 93 94 /* Maps a function over a list of strings and then concatenates the 95 result with the specified separator interspersed between 96 elements. 97 98 Type: concatMapStringsSep :: string -> (string -> string) -> [string] -> string 99 100 Example: 101 concatMapStringsSep "-" (x: toUpper x) ["foo" "bar" "baz"] 102 => "FOO-BAR-BAZ" 103 */ 104 concatMapStringsSep = 105 # Separator to add between elements 106 sep: 107 # Function to map over the list 108 f: 109 # List of input strings 110 list: concatStringsSep sep (map f list); 111 112 /* Same as `concatMapStringsSep`, but the mapping function 113 additionally receives the position of its argument. 114 115 Type: concatIMapStringsSep :: string -> (int -> string -> string) -> [string] -> string 116 117 Example: 118 concatImapStringsSep "-" (pos: x: toString (x / pos)) [ 6 6 6 ] 119 => "6-3-2" 120 */ 121 concatImapStringsSep = 122 # Separator to add between elements 123 sep: 124 # Function that receives elements and their positions 125 f: 126 # List of input strings 127 list: concatStringsSep sep (lib.imap1 f list); 128 129 /* Construct a Unix-style, colon-separated search path consisting of 130 the given `subDir` appended to each of the given paths. 131 132 Type: makeSearchPath :: string -> [string] -> string 133 134 Example: 135 makeSearchPath "bin" ["/root" "/usr" "/usr/local"] 136 => "/root/bin:/usr/bin:/usr/local/bin" 137 makeSearchPath "bin" [""] 138 => "/bin" 139 */ 140 makeSearchPath = 141 # Directory name to append 142 subDir: 143 # List of base paths 144 paths: 145 concatStringsSep ":" (map (path: path + "/" + subDir) (filter (x: x != null) paths)); 146 147 /* Construct a Unix-style search path by appending the given 148 `subDir` to the specified `output` of each of the packages. If no 149 output by the given name is found, fallback to `.out` and then to 150 the default. 151 152 Type: string -> string -> [package] -> string 153 154 Example: 155 makeSearchPathOutput "dev" "bin" [ pkgs.openssl pkgs.zlib ] 156 => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r-dev/bin:/nix/store/wwh7mhwh269sfjkm6k5665b5kgp7jrk2-zlib-1.2.8/bin" 157 */ 158 makeSearchPathOutput = 159 # Package output to use 160 output: 161 # Directory name to append 162 subDir: 163 # List of packages 164 pkgs: makeSearchPath subDir (map (lib.getOutput output) pkgs); 165 166 /* Construct a library search path (such as RPATH) containing the 167 libraries for a set of packages 168 169 Example: 170 makeLibraryPath [ "/usr" "/usr/local" ] 171 => "/usr/lib:/usr/local/lib" 172 pkgs = import <nixpkgs> { } 173 makeLibraryPath [ pkgs.openssl pkgs.zlib ] 174 => "/nix/store/9rz8gxhzf8sw4kf2j2f1grr49w8zx5vj-openssl-1.0.1r/lib:/nix/store/wwh7mhwh269sfjkm6k5665b5kgp7jrk2-zlib-1.2.8/lib" 175 */ 176 makeLibraryPath = makeSearchPathOutput "lib" "lib"; 177 178 /* Construct a binary search path (such as $PATH) containing the 179 binaries for a set of packages. 180 181 Example: 182 makeBinPath ["/root" "/usr" "/usr/local"] 183 => "/root/bin:/usr/bin:/usr/local/bin" 184 */ 185 makeBinPath = makeSearchPathOutput "bin" "bin"; 186 187 /* Depending on the boolean `cond', return either the given string 188 or the empty string. Useful to concatenate against a bigger string. 189 190 Type: optionalString :: bool -> string -> string 191 192 Example: 193 optionalString true "some-string" 194 => "some-string" 195 optionalString false "some-string" 196 => "" 197 */ 198 optionalString = 199 # Condition 200 cond: 201 # String to return if condition is true 202 string: if cond then string else ""; 203 204 /* Determine whether a string has given prefix. 205 206 Type: hasPrefix :: string -> string -> bool 207 208 Example: 209 hasPrefix "foo" "foobar" 210 => true 211 hasPrefix "foo" "barfoo" 212 => false 213 */ 214 hasPrefix = 215 # Prefix to check for 216 pref: 217 # Input string 218 str: substring 0 (stringLength pref) str == pref; 219 220 /* Determine whether a string has given suffix. 221 222 Type: hasSuffix :: string -> string -> bool 223 224 Example: 225 hasSuffix "foo" "foobar" 226 => false 227 hasSuffix "foo" "barfoo" 228 => true 229 */ 230 hasSuffix = 231 # Suffix to check for 232 suffix: 233 # Input string 234 content: 235 let 236 lenContent = stringLength content; 237 lenSuffix = stringLength suffix; 238 in lenContent >= lenSuffix && 239 substring (lenContent - lenSuffix) lenContent content == suffix; 240 241 /* Determine whether a string contains the given infix 242 243 Type: hasInfix :: string -> string -> bool 244 245 Example: 246 hasInfix "bc" "abcd" 247 => true 248 hasInfix "ab" "abcd" 249 => true 250 hasInfix "cd" "abcd" 251 => true 252 hasInfix "foo" "abcd" 253 => false 254 */ 255 hasInfix = infix: content: 256 let 257 drop = x: substring 1 (stringLength x) x; 258 in hasPrefix infix content 259 || content != "" && hasInfix infix (drop content); 260 261 /* Convert a string to a list of characters (i.e. singleton strings). 262 This allows you to, e.g., map a function over each character. However, 263 note that this will likely be horribly inefficient; Nix is not a 264 general purpose programming language. Complex string manipulations 265 should, if appropriate, be done in a derivation. 266 Also note that Nix treats strings as a list of bytes and thus doesn't 267 handle unicode. 268 269 Type: stringToCharacters :: string -> [string] 270 271 Example: 272 stringToCharacters "" 273 => [ ] 274 stringToCharacters "abc" 275 => [ "a" "b" "c" ] 276 stringToCharacters "💩" 277 => [ "" "" "" "" ] 278 */ 279 stringToCharacters = s: 280 map (p: substring p 1 s) (lib.range 0 (stringLength s - 1)); 281 282 /* Manipulate a string character by character and replace them by 283 strings before concatenating the results. 284 285 Type: stringAsChars :: (string -> string) -> string -> string 286 287 Example: 288 stringAsChars (x: if x == "a" then "i" else x) "nax" 289 => "nix" 290 */ 291 stringAsChars = 292 # Function to map over each individual character 293 f: 294 # Input string 295 s: concatStrings ( 296 map f (stringToCharacters s) 297 ); 298 299 /* Escape occurrence of the elements of `list` in `string` by 300 prefixing it with a backslash. 301 302 Type: escape :: [string] -> string -> string 303 304 Example: 305 escape ["(" ")"] "(foo)" 306 => "\\(foo\\)" 307 */ 308 escape = list: replaceChars list (map (c: "\\${c}") list); 309 310 /* Quote string to be used safely within the Bourne shell. 311 312 Type: escapeShellArg :: string -> string 313 314 Example: 315 escapeShellArg "esc'ape\nme" 316 => "'esc'\\''ape\nme'" 317 */ 318 escapeShellArg = arg: "'${replaceStrings ["'"] ["'\\''"] (toString arg)}'"; 319 320 /* Quote all arguments to be safely passed to the Bourne shell. 321 322 Type: escapeShellArgs :: [string] -> string 323 324 Example: 325 escapeShellArgs ["one" "two three" "four'five"] 326 => "'one' 'two three' 'four'\\''five'" 327 */ 328 escapeShellArgs = concatMapStringsSep " " escapeShellArg; 329 330 /* Turn a string into a Nix expression representing that string 331 332 Type: string -> string 333 334 Example: 335 escapeNixString "hello\${}\n" 336 => "\"hello\\\${}\\n\"" 337 */ 338 escapeNixString = s: escape ["$"] (toJSON s); 339 340 /* Turn a string into an exact regular expression 341 342 Type: string -> string 343 344 Example: 345 escapeRegex "[^a-z]*" 346 => "\\[\\^a-z]\\*" 347 */ 348 escapeRegex = escape (stringToCharacters "\\[{()^$?*+|."); 349 350 /* Quotes a string if it can't be used as an identifier directly. 351 352 Type: string -> string 353 354 Example: 355 escapeNixIdentifier "hello" 356 => "hello" 357 escapeNixIdentifier "0abc" 358 => "\"0abc\"" 359 */ 360 escapeNixIdentifier = s: 361 # Regex from https://github.com/NixOS/nix/blob/d048577909e383439c2549e849c5c2f2016c997e/src/libexpr/lexer.l#L91 362 if match "[a-zA-Z_][a-zA-Z0-9_'-]*" s != null 363 then s else escapeNixString s; 364 365 # Obsolete - use replaceStrings instead. 366 replaceChars = builtins.replaceStrings or ( 367 del: new: s: 368 let 369 substList = lib.zipLists del new; 370 subst = c: 371 let found = lib.findFirst (sub: sub.fst == c) null substList; in 372 if found == null then 373 c 374 else 375 found.snd; 376 in 377 stringAsChars subst s); 378 379 # Case conversion utilities. 380 lowerChars = stringToCharacters "abcdefghijklmnopqrstuvwxyz"; 381 upperChars = stringToCharacters "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 382 383 /* Converts an ASCII string to lower-case. 384 385 Type: toLower :: string -> string 386 387 Example: 388 toLower "HOME" 389 => "home" 390 */ 391 toLower = replaceChars upperChars lowerChars; 392 393 /* Converts an ASCII string to upper-case. 394 395 Type: toUpper :: string -> string 396 397 Example: 398 toUpper "home" 399 => "HOME" 400 */ 401 toUpper = replaceChars lowerChars upperChars; 402 403 /* Appends string context from another string. This is an implementation 404 detail of Nix. 405 406 Strings in Nix carry an invisible `context` which is a list of strings 407 representing store paths. If the string is later used in a derivation 408 attribute, the derivation will properly populate the inputDrvs and 409 inputSrcs. 410 411 Example: 412 pkgs = import <nixpkgs> { }; 413 addContextFrom pkgs.coreutils "bar" 414 => "bar" 415 */ 416 addContextFrom = a: b: substring 0 0 a + b; 417 418 /* Cut a string with a separator and produces a list of strings which 419 were separated by this separator. 420 421 Example: 422 splitString "." "foo.bar.baz" 423 => [ "foo" "bar" "baz" ] 424 splitString "/" "/usr/local/bin" 425 => [ "" "usr" "local" "bin" ] 426 */ 427 splitString = _sep: _s: 428 let 429 sep = builtins.unsafeDiscardStringContext _sep; 430 s = builtins.unsafeDiscardStringContext _s; 431 splits = builtins.filter builtins.isString (builtins.split (escapeRegex sep) s); 432 in 433 map (v: addContextFrom _sep (addContextFrom _s v)) splits; 434 435 /* Return a string without the specified prefix, if the prefix matches. 436 437 Type: string -> string -> string 438 439 Example: 440 removePrefix "foo." "foo.bar.baz" 441 => "bar.baz" 442 removePrefix "xxx" "foo.bar.baz" 443 => "foo.bar.baz" 444 */ 445 removePrefix = 446 # Prefix to remove if it matches 447 prefix: 448 # Input string 449 str: 450 let 451 preLen = stringLength prefix; 452 sLen = stringLength str; 453 in 454 if hasPrefix prefix str then 455 substring preLen (sLen - preLen) str 456 else 457 str; 458 459 /* Return a string without the specified suffix, if the suffix matches. 460 461 Type: string -> string -> string 462 463 Example: 464 removeSuffix "front" "homefront" 465 => "home" 466 removeSuffix "xxx" "homefront" 467 => "homefront" 468 */ 469 removeSuffix = 470 # Suffix to remove if it matches 471 suffix: 472 # Input string 473 str: 474 let 475 sufLen = stringLength suffix; 476 sLen = stringLength str; 477 in 478 if sufLen <= sLen && suffix == substring (sLen - sufLen) sufLen str then 479 substring 0 (sLen - sufLen) str 480 else 481 str; 482 483 /* Return true if string v1 denotes a version older than v2. 484 485 Example: 486 versionOlder "1.1" "1.2" 487 => true 488 versionOlder "1.1" "1.1" 489 => false 490 */ 491 versionOlder = v1: v2: compareVersions v2 v1 == 1; 492 493 /* Return true if string v1 denotes a version equal to or newer than v2. 494 495 Example: 496 versionAtLeast "1.1" "1.0" 497 => true 498 versionAtLeast "1.1" "1.1" 499 => true 500 versionAtLeast "1.1" "1.2" 501 => false 502 */ 503 versionAtLeast = v1: v2: !versionOlder v1 v2; 504 505 /* This function takes an argument that's either a derivation or a 506 derivation's "name" attribute and extracts the name part from that 507 argument. 508 509 Example: 510 getName "youtube-dl-2016.01.01" 511 => "youtube-dl" 512 getName pkgs.youtube-dl 513 => "youtube-dl" 514 */ 515 getName = x: 516 let 517 parse = drv: (parseDrvName drv).name; 518 in if isString x 519 then parse x 520 else x.pname or (parse x.name); 521 522 /* This function takes an argument that's either a derivation or a 523 derivation's "name" attribute and extracts the version part from that 524 argument. 525 526 Example: 527 getVersion "youtube-dl-2016.01.01" 528 => "2016.01.01" 529 getVersion pkgs.youtube-dl 530 => "2016.01.01" 531 */ 532 getVersion = x: 533 let 534 parse = drv: (parseDrvName drv).version; 535 in if isString x 536 then parse x 537 else x.version or (parse x.name); 538 539 /* Extract name with version from URL. Ask for separator which is 540 supposed to start extension. 541 542 Example: 543 nameFromURL "https://nixos.org/releases/nix/nix-1.7/nix-1.7-x86_64-linux.tar.bz2" "-" 544 => "nix" 545 nameFromURL "https://nixos.org/releases/nix/nix-1.7/nix-1.7-x86_64-linux.tar.bz2" "_" 546 => "nix-1.7-x86" 547 */ 548 nameFromURL = url: sep: 549 let 550 components = splitString "/" url; 551 filename = lib.last components; 552 name = head (splitString sep filename); 553 in assert name != filename; name; 554 555 /* Create an --{enable,disable}-<feat> string that can be passed to 556 standard GNU Autoconf scripts. 557 558 Example: 559 enableFeature true "shared" 560 => "--enable-shared" 561 enableFeature false "shared" 562 => "--disable-shared" 563 */ 564 enableFeature = enable: feat: 565 assert isString feat; # e.g. passing openssl instead of "openssl" 566 "--${if enable then "enable" else "disable"}-${feat}"; 567 568 /* Create an --{enable-<feat>=<value>,disable-<feat>} string that can be passed to 569 standard GNU Autoconf scripts. 570 571 Example: 572 enableFeatureAs true "shared" "foo" 573 => "--enable-shared=foo" 574 enableFeatureAs false "shared" (throw "ignored") 575 => "--disable-shared" 576 */ 577 enableFeatureAs = enable: feat: value: enableFeature enable feat + optionalString enable "=${value}"; 578 579 /* Create an --{with,without}-<feat> string that can be passed to 580 standard GNU Autoconf scripts. 581 582 Example: 583 withFeature true "shared" 584 => "--with-shared" 585 withFeature false "shared" 586 => "--without-shared" 587 */ 588 withFeature = with_: feat: 589 assert isString feat; # e.g. passing openssl instead of "openssl" 590 "--${if with_ then "with" else "without"}-${feat}"; 591 592 /* Create an --{with-<feat>=<value>,without-<feat>} string that can be passed to 593 standard GNU Autoconf scripts. 594 595 Example: 596 withFeatureAs true "shared" "foo" 597 => "--with-shared=foo" 598 withFeatureAs false "shared" (throw "ignored") 599 => "--without-shared" 600 */ 601 withFeatureAs = with_: feat: value: withFeature with_ feat + optionalString with_ "=${value}"; 602 603 /* Create a fixed width string with additional prefix to match 604 required width. 605 606 This function will fail if the input string is longer than the 607 requested length. 608 609 Type: fixedWidthString :: int -> string -> string -> string 610 611 Example: 612 fixedWidthString 5 "0" (toString 15) 613 => "00015" 614 */ 615 fixedWidthString = width: filler: str: 616 let 617 strw = lib.stringLength str; 618 reqWidth = width - (lib.stringLength filler); 619 in 620 assert lib.assertMsg (strw <= width) 621 "fixedWidthString: requested string length (${ 622 toString width}) must not be shorter than actual length (${ 623 toString strw})"; 624 if strw == width then str else filler + fixedWidthString reqWidth filler str; 625 626 /* Format a number adding leading zeroes up to fixed width. 627 628 Example: 629 fixedWidthNumber 5 15 630 => "00015" 631 */ 632 fixedWidthNumber = width: n: fixedWidthString width "0" (toString n); 633 634 /* Convert a float to a string, but emit a warning when precision is lost 635 during the conversion 636 637 Example: 638 floatToString 0.000001 639 => "0.000001" 640 floatToString 0.0000001 641 => trace: warning: Imprecise conversion from float to string 0.000000 642 "0.000000" 643 */ 644 floatToString = float: let 645 result = toString float; 646 precise = float == fromJSON result; 647 in lib.warnIf (!precise) "Imprecise conversion from float to string ${result}" 648 result; 649 650 /* Check whether a value can be coerced to a string */ 651 isCoercibleToString = x: 652 elem (typeOf x) [ "path" "string" "null" "int" "float" "bool" ] || 653 (isList x && lib.all isCoercibleToString x) || 654 x ? outPath || 655 x ? __toString; 656 657 /* Check whether a value is a store path. 658 659 Example: 660 isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11/bin/python" 661 => false 662 isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11" 663 => true 664 isStorePath pkgs.python 665 => true 666 isStorePath [] || isStorePath 42 || isStorePath {} || 667 => false 668 */ 669 isStorePath = x: 670 if !(isList x) && isCoercibleToString x then 671 let str = toString x; in 672 substring 0 1 str == "/" 673 && dirOf str == storeDir 674 else 675 false; 676 677 /* Parse a string as an int. 678 679 Type: string -> int 680 681 Example: 682 toInt "1337" 683 => 1337 684 toInt "-4" 685 => -4 686 toInt "3.14" 687 => error: floating point JSON numbers are not supported 688 */ 689 # Obviously, it is a bit hacky to use fromJSON this way. 690 toInt = str: 691 let may_be_int = fromJSON str; in 692 if isInt may_be_int 693 then may_be_int 694 else throw "Could not convert ${str} to int."; 695 696 /* Read a list of paths from `file`, relative to the `rootPath`. 697 Lines beginning with `#` are treated as comments and ignored. 698 Whitespace is significant. 699 700 NOTE: This function is not performant and should be avoided. 701 702 Example: 703 readPathsFromFile /prefix 704 ./pkgs/development/libraries/qt-5/5.4/qtbase/series 705 => [ "/prefix/dlopen-resolv.patch" "/prefix/tzdir.patch" 706 "/prefix/dlopen-libXcursor.patch" "/prefix/dlopen-openssl.patch" 707 "/prefix/dlopen-dbus.patch" "/prefix/xdg-config-dirs.patch" 708 "/prefix/nix-profiles-library-paths.patch" 709 "/prefix/compose-search-path.patch" ] 710 */ 711 readPathsFromFile = lib.warn "lib.readPathsFromFile is deprecated, use a list instead" 712 (rootPath: file: 713 let 714 lines = lib.splitString "\n" (readFile file); 715 removeComments = lib.filter (line: line != "" && !(lib.hasPrefix "#" line)); 716 relativePaths = removeComments lines; 717 absolutePaths = map (path: rootPath + "/${path}") relativePaths; 718 in 719 absolutePaths); 720 721 /* Read the contents of a file removing the trailing \n 722 723 Type: fileContents :: path -> string 724 725 Example: 726 $ echo "1.0" > ./version 727 728 fileContents ./version 729 => "1.0" 730 */ 731 fileContents = file: removeSuffix "\n" (readFile file); 732 733 734 /* Creates a valid derivation name from a potentially invalid one. 735 736 Type: sanitizeDerivationName :: String -> String 737 738 Example: 739 sanitizeDerivationName "../hello.bar # foo" 740 => "-hello.bar-foo" 741 sanitizeDerivationName "" 742 => "unknown" 743 sanitizeDerivationName pkgs.hello 744 => "-nix-store-2g75chlbpxlrqn15zlby2dfh8hr9qwbk-hello-2.10" 745 */ 746 sanitizeDerivationName = string: lib.pipe string [ 747 # Get rid of string context. This is safe under the assumption that the 748 # resulting string is only used as a derivation name 749 unsafeDiscardStringContext 750 # Strip all leading "." 751 (x: elemAt (match "\\.*(.*)" x) 0) 752 # Split out all invalid characters 753 # https://github.com/NixOS/nix/blob/2.3.2/src/libstore/store-api.cc#L85-L112 754 # https://github.com/NixOS/nix/blob/2242be83c61788b9c0736a92bb0b5c7bbfc40803/nix-rust/src/store/path.rs#L100-L125 755 (split "[^[:alnum:]+._?=-]+") 756 # Replace invalid character ranges with a "-" 757 (concatMapStrings (s: if lib.isList s then "-" else s)) 758 # Limit to 211 characters (minus 4 chars for ".drv") 759 (x: substring (lib.max (stringLength x - 207) 0) (-1) x) 760 # If the result is empty, replace it with "unknown" 761 (x: if stringLength x == 0 then "unknown" else x) 762 ]; 763 764}