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}