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 lib.foldl' (x: y: x + y) "" (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 -> (a -> string) -> [a] -> 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 -> a -> string) -> [a] -> 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 /* Escapes a string such that it is safe to include verbatim in an XML
366 document.
367
368 Type: string -> string
369
370 Example:
371 escapeXML ''"test" 'test' < & >''
372 => ""test" 'test' < & >"
373 */
374 escapeXML = builtins.replaceStrings
375 ["\"" "'" "<" ">" "&"]
376 [""" "'" "<" ">" "&"];
377
378 # Obsolete - use replaceStrings instead.
379 replaceChars = builtins.replaceStrings or (
380 del: new: s:
381 let
382 substList = lib.zipLists del new;
383 subst = c:
384 let found = lib.findFirst (sub: sub.fst == c) null substList; in
385 if found == null then
386 c
387 else
388 found.snd;
389 in
390 stringAsChars subst s);
391
392 # Case conversion utilities.
393 lowerChars = stringToCharacters "abcdefghijklmnopqrstuvwxyz";
394 upperChars = stringToCharacters "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
395
396 /* Converts an ASCII string to lower-case.
397
398 Type: toLower :: string -> string
399
400 Example:
401 toLower "HOME"
402 => "home"
403 */
404 toLower = replaceChars upperChars lowerChars;
405
406 /* Converts an ASCII string to upper-case.
407
408 Type: toUpper :: string -> string
409
410 Example:
411 toUpper "home"
412 => "HOME"
413 */
414 toUpper = replaceChars lowerChars upperChars;
415
416 /* Appends string context from another string. This is an implementation
417 detail of Nix.
418
419 Strings in Nix carry an invisible `context` which is a list of strings
420 representing store paths. If the string is later used in a derivation
421 attribute, the derivation will properly populate the inputDrvs and
422 inputSrcs.
423
424 Example:
425 pkgs = import <nixpkgs> { };
426 addContextFrom pkgs.coreutils "bar"
427 => "bar"
428 */
429 addContextFrom = a: b: substring 0 0 a + b;
430
431 /* Cut a string with a separator and produces a list of strings which
432 were separated by this separator.
433
434 Example:
435 splitString "." "foo.bar.baz"
436 => [ "foo" "bar" "baz" ]
437 splitString "/" "/usr/local/bin"
438 => [ "" "usr" "local" "bin" ]
439 */
440 splitString = _sep: _s:
441 let
442 sep = builtins.unsafeDiscardStringContext _sep;
443 s = builtins.unsafeDiscardStringContext _s;
444 splits = builtins.filter builtins.isString (builtins.split (escapeRegex sep) s);
445 in
446 map (v: addContextFrom _sep (addContextFrom _s v)) splits;
447
448 /* Return a string without the specified prefix, if the prefix matches.
449
450 Type: string -> string -> string
451
452 Example:
453 removePrefix "foo." "foo.bar.baz"
454 => "bar.baz"
455 removePrefix "xxx" "foo.bar.baz"
456 => "foo.bar.baz"
457 */
458 removePrefix =
459 # Prefix to remove if it matches
460 prefix:
461 # Input string
462 str:
463 let
464 preLen = stringLength prefix;
465 sLen = stringLength str;
466 in
467 if hasPrefix prefix str then
468 substring preLen (sLen - preLen) str
469 else
470 str;
471
472 /* Return a string without the specified suffix, if the suffix matches.
473
474 Type: string -> string -> string
475
476 Example:
477 removeSuffix "front" "homefront"
478 => "home"
479 removeSuffix "xxx" "homefront"
480 => "homefront"
481 */
482 removeSuffix =
483 # Suffix to remove if it matches
484 suffix:
485 # Input string
486 str:
487 let
488 sufLen = stringLength suffix;
489 sLen = stringLength str;
490 in
491 if sufLen <= sLen && suffix == substring (sLen - sufLen) sufLen str then
492 substring 0 (sLen - sufLen) str
493 else
494 str;
495
496 /* Return true if string v1 denotes a version older than v2.
497
498 Example:
499 versionOlder "1.1" "1.2"
500 => true
501 versionOlder "1.1" "1.1"
502 => false
503 */
504 versionOlder = v1: v2: compareVersions v2 v1 == 1;
505
506 /* Return true if string v1 denotes a version equal to or newer than v2.
507
508 Example:
509 versionAtLeast "1.1" "1.0"
510 => true
511 versionAtLeast "1.1" "1.1"
512 => true
513 versionAtLeast "1.1" "1.2"
514 => false
515 */
516 versionAtLeast = v1: v2: !versionOlder v1 v2;
517
518 /* This function takes an argument that's either a derivation or a
519 derivation's "name" attribute and extracts the name part from that
520 argument.
521
522 Example:
523 getName "youtube-dl-2016.01.01"
524 => "youtube-dl"
525 getName pkgs.youtube-dl
526 => "youtube-dl"
527 */
528 getName = x:
529 let
530 parse = drv: (parseDrvName drv).name;
531 in if isString x
532 then parse x
533 else x.pname or (parse x.name);
534
535 /* This function takes an argument that's either a derivation or a
536 derivation's "name" attribute and extracts the version part from that
537 argument.
538
539 Example:
540 getVersion "youtube-dl-2016.01.01"
541 => "2016.01.01"
542 getVersion pkgs.youtube-dl
543 => "2016.01.01"
544 */
545 getVersion = x:
546 let
547 parse = drv: (parseDrvName drv).version;
548 in if isString x
549 then parse x
550 else x.version or (parse x.name);
551
552 /* Extract name with version from URL. Ask for separator which is
553 supposed to start extension.
554
555 Example:
556 nameFromURL "https://nixos.org/releases/nix/nix-1.7/nix-1.7-x86_64-linux.tar.bz2" "-"
557 => "nix"
558 nameFromURL "https://nixos.org/releases/nix/nix-1.7/nix-1.7-x86_64-linux.tar.bz2" "_"
559 => "nix-1.7-x86"
560 */
561 nameFromURL = url: sep:
562 let
563 components = splitString "/" url;
564 filename = lib.last components;
565 name = head (splitString sep filename);
566 in assert name != filename; name;
567
568 /* Create an --{enable,disable}-<feat> string that can be passed to
569 standard GNU Autoconf scripts.
570
571 Example:
572 enableFeature true "shared"
573 => "--enable-shared"
574 enableFeature false "shared"
575 => "--disable-shared"
576 */
577 enableFeature = enable: feat:
578 assert isString feat; # e.g. passing openssl instead of "openssl"
579 "--${if enable then "enable" else "disable"}-${feat}";
580
581 /* Create an --{enable-<feat>=<value>,disable-<feat>} string that can be passed to
582 standard GNU Autoconf scripts.
583
584 Example:
585 enableFeatureAs true "shared" "foo"
586 => "--enable-shared=foo"
587 enableFeatureAs false "shared" (throw "ignored")
588 => "--disable-shared"
589 */
590 enableFeatureAs = enable: feat: value: enableFeature enable feat + optionalString enable "=${value}";
591
592 /* Create an --{with,without}-<feat> string that can be passed to
593 standard GNU Autoconf scripts.
594
595 Example:
596 withFeature true "shared"
597 => "--with-shared"
598 withFeature false "shared"
599 => "--without-shared"
600 */
601 withFeature = with_: feat:
602 assert isString feat; # e.g. passing openssl instead of "openssl"
603 "--${if with_ then "with" else "without"}-${feat}";
604
605 /* Create an --{with-<feat>=<value>,without-<feat>} string that can be passed to
606 standard GNU Autoconf scripts.
607
608 Example:
609 withFeatureAs true "shared" "foo"
610 => "--with-shared=foo"
611 withFeatureAs false "shared" (throw "ignored")
612 => "--without-shared"
613 */
614 withFeatureAs = with_: feat: value: withFeature with_ feat + optionalString with_ "=${value}";
615
616 /* Create a fixed width string with additional prefix to match
617 required width.
618
619 This function will fail if the input string is longer than the
620 requested length.
621
622 Type: fixedWidthString :: int -> string -> string -> string
623
624 Example:
625 fixedWidthString 5 "0" (toString 15)
626 => "00015"
627 */
628 fixedWidthString = width: filler: str:
629 let
630 strw = lib.stringLength str;
631 reqWidth = width - (lib.stringLength filler);
632 in
633 assert lib.assertMsg (strw <= width)
634 "fixedWidthString: requested string length (${
635 toString width}) must not be shorter than actual length (${
636 toString strw})";
637 if strw == width then str else filler + fixedWidthString reqWidth filler str;
638
639 /* Format a number adding leading zeroes up to fixed width.
640
641 Example:
642 fixedWidthNumber 5 15
643 => "00015"
644 */
645 fixedWidthNumber = width: n: fixedWidthString width "0" (toString n);
646
647 /* Convert a float to a string, but emit a warning when precision is lost
648 during the conversion
649
650 Example:
651 floatToString 0.000001
652 => "0.000001"
653 floatToString 0.0000001
654 => trace: warning: Imprecise conversion from float to string 0.000000
655 "0.000000"
656 */
657 floatToString = float: let
658 result = toString float;
659 precise = float == fromJSON result;
660 in lib.warnIf (!precise) "Imprecise conversion from float to string ${result}"
661 result;
662
663 /* Check whether a value can be coerced to a string */
664 isCoercibleToString = x:
665 elem (typeOf x) [ "path" "string" "null" "int" "float" "bool" ] ||
666 (isList x && lib.all isCoercibleToString x) ||
667 x ? outPath ||
668 x ? __toString;
669
670 /* Check whether a value is a store path.
671
672 Example:
673 isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11/bin/python"
674 => false
675 isStorePath "/nix/store/d945ibfx9x185xf04b890y4f9g3cbb63-python-2.7.11"
676 => true
677 isStorePath pkgs.python
678 => true
679 isStorePath [] || isStorePath 42 || isStorePath {} || …
680 => false
681 */
682 isStorePath = x:
683 if !(isList x) && isCoercibleToString x then
684 let str = toString x; in
685 substring 0 1 str == "/"
686 && dirOf str == storeDir
687 else
688 false;
689
690 /* Parse a string as an int.
691
692 Type: string -> int
693
694 Example:
695 toInt "1337"
696 => 1337
697 toInt "-4"
698 => -4
699 toInt "3.14"
700 => error: floating point JSON numbers are not supported
701 */
702 # Obviously, it is a bit hacky to use fromJSON this way.
703 toInt = str:
704 let may_be_int = fromJSON str; in
705 if isInt may_be_int
706 then may_be_int
707 else throw "Could not convert ${str} to int.";
708
709 /* Read a list of paths from `file`, relative to the `rootPath`.
710 Lines beginning with `#` are treated as comments and ignored.
711 Whitespace is significant.
712
713 NOTE: This function is not performant and should be avoided.
714
715 Example:
716 readPathsFromFile /prefix
717 ./pkgs/development/libraries/qt-5/5.4/qtbase/series
718 => [ "/prefix/dlopen-resolv.patch" "/prefix/tzdir.patch"
719 "/prefix/dlopen-libXcursor.patch" "/prefix/dlopen-openssl.patch"
720 "/prefix/dlopen-dbus.patch" "/prefix/xdg-config-dirs.patch"
721 "/prefix/nix-profiles-library-paths.patch"
722 "/prefix/compose-search-path.patch" ]
723 */
724 readPathsFromFile = lib.warn "lib.readPathsFromFile is deprecated, use a list instead"
725 (rootPath: file:
726 let
727 lines = lib.splitString "\n" (readFile file);
728 removeComments = lib.filter (line: line != "" && !(lib.hasPrefix "#" line));
729 relativePaths = removeComments lines;
730 absolutePaths = map (path: rootPath + "/${path}") relativePaths;
731 in
732 absolutePaths);
733
734 /* Read the contents of a file removing the trailing \n
735
736 Type: fileContents :: path -> string
737
738 Example:
739 $ echo "1.0" > ./version
740
741 fileContents ./version
742 => "1.0"
743 */
744 fileContents = file: removeSuffix "\n" (readFile file);
745
746
747 /* Creates a valid derivation name from a potentially invalid one.
748
749 Type: sanitizeDerivationName :: String -> String
750
751 Example:
752 sanitizeDerivationName "../hello.bar # foo"
753 => "-hello.bar-foo"
754 sanitizeDerivationName ""
755 => "unknown"
756 sanitizeDerivationName pkgs.hello
757 => "-nix-store-2g75chlbpxlrqn15zlby2dfh8hr9qwbk-hello-2.10"
758 */
759 sanitizeDerivationName = string: lib.pipe string [
760 # Get rid of string context. This is safe under the assumption that the
761 # resulting string is only used as a derivation name
762 unsafeDiscardStringContext
763 # Strip all leading "."
764 (x: elemAt (match "\\.*(.*)" x) 0)
765 # Split out all invalid characters
766 # https://github.com/NixOS/nix/blob/2.3.2/src/libstore/store-api.cc#L85-L112
767 # https://github.com/NixOS/nix/blob/2242be83c61788b9c0736a92bb0b5c7bbfc40803/nix-rust/src/store/path.rs#L100-L125
768 (split "[^[:alnum:]+._?=-]+")
769 # Replace invalid character ranges with a "-"
770 (concatMapStrings (s: if lib.isList s then "-" else s))
771 # Limit to 211 characters (minus 4 chars for ".drv")
772 (x: substring (lib.max (stringLength x - 207) 0) (-1) x)
773 # If the result is empty, replace it with "unknown"
774 (x: if stringLength x == 0 then "unknown" else x)
775 ];
776
777}