lib.strings: small performance cleanups (#519085)

This commit is contained in:
Emily
2026-06-24 21:31:39 +00:00
committed by GitHub
+112 -105
View File
@@ -6,8 +6,6 @@ let
inherit (builtins) length;
inherit (lib.trivial) warnIf;
asciiTable = import ./ascii-table.nix;
in
@@ -16,6 +14,7 @@ rec {
inherit (builtins)
compareVersions
concatMap
elem
elemAt
filter
@@ -564,7 +563,10 @@ rec {
:::
*/
makeSearchPath =
subDir: paths: concatStringsSep ":" (map (path: path + "/" + subDir) (filter (x: x != null) paths));
subDir: paths:
concatStringsSep ":" (
concatMap (path: if path != null then [ (path + "/" + subDir) ] else [ ]) paths
);
/**
Construct a Unix-style search path by appending the given
@@ -602,8 +604,14 @@ rec {
:::
*/
makeSearchPathOutput =
output: subDir: pkgs:
makeSearchPath subDir (map (lib.getOutput output) pkgs);
output:
let
getOutput' = lib.getOutput output;
in
subDir: pkgs:
concatStringsSep ":" (
concatMap (path: if path != null then [ (getOutput' path + "/" + subDir) ] else [ ]) pkgs
);
/**
Construct a library search path (such as RPATH) containing the
@@ -721,15 +729,17 @@ rec {
:::
*/
normalizePath =
let
startsWithSlash = hasSuffix "/";
in
s:
warnIf (isPath s)
''
if isPath s then
throw ''
lib.strings.normalizePath: The argument (${toString s}) is a path value, but only strings are supported.
Path values are always normalised in Nix, so there's no need to call this function on them.
This function also copies the path to the Nix store and returns the store path, the same as "''${path}" will, which may not be what you want.
This behavior is deprecated and will throw an error in the future.''
(
builtins.foldl' (x: y: if y == "/" && hasSuffix "/" x then x else x + y) "" (stringToCharacters s)
Path values are always normalised in Nix, so there's no need to call this function on them.''
else
builtins.foldl' (x: y: if y == "/" && startsWithSlash x then x else x + y) "" (
stringToCharacters s
);
/**
@@ -796,17 +806,18 @@ rec {
:::
*/
hasPrefix =
pref: str:
# Before 23.05, paths would be copied to the store before converting them
# to strings and comparing. This was surprising and confusing.
warnIf (isPath pref)
''
pref:
let
lenPrefix = stringLength pref;
in
if isPath pref then
# Before 23.05, paths would be copied to the store before converting them
# to strings and comparing. This was surprising and confusing.
throw ''
lib.strings.hasPrefix: The first argument (${toString pref}) is a path value, but only strings are supported.
There is almost certainly a bug in the calling code, since this function always returns `false` in such a case.
This function also copies the path to the Nix store, which may not be what you want.
This behavior is deprecated and will throw an error in the future.
You might want to use `lib.path.hasPrefix` instead, which correctly supports paths.''
(substring 0 (stringLength pref) str == pref);
else
str: substring 0 lenPrefix str == pref;
/**
Determine whether a string has given suffix.
@@ -839,20 +850,23 @@ rec {
:::
*/
hasSuffix =
suffix: content:
suffix:
let
lenContent = stringLength content;
lenSuffix = stringLength suffix;
in
# Before 23.05, paths would be copied to the store before converting them
# to strings and comparing. This was surprising and confusing.
warnIf (isPath suffix)
''
if isPath suffix then
# Before 23.05, paths would be copied to the store before converting them
# to strings and comparing. This was surprising and confusing.
throw ''
lib.strings.hasSuffix: The first argument (${toString suffix}) is a path value, but only strings are supported.
There is almost certainly a bug in the calling code, since this function always returns `false` in such a case.
This function also copies the path to the Nix store, which may not be what you want.
This behavior is deprecated and will throw an error in the future.''
(lenContent >= lenSuffix && substring (lenContent - lenSuffix) lenContent content == suffix);
There is almost certainly a bug in the calling code, since this function always returns `false` in such a case.
This function also copies the path to the Nix store, which may not be what you want.''
else
content:
let
lenContent = stringLength content;
in
lenContent >= lenSuffix && substring (lenContent - lenSuffix) lenContent content == suffix;
/**
Determine whether a string contains the given infix
@@ -889,16 +903,19 @@ rec {
:::
*/
hasInfix =
infix: content:
# Before 23.05, paths would be copied to the store before converting them
# to strings and comparing. This was surprising and confusing.
warnIf (isPath infix)
''
infix:
let
escapedInfix = escapeRegex infix;
in
if isPath infix then
# Before 23.05, paths would be copied to the store before converting them
# to strings and comparing. This was surprising and confusing.
throw ''
lib.strings.hasInfix: The first argument (${toString infix}) is a path value, but only strings are supported.
There is almost certainly a bug in the calling code, since this function always returns `false` in such a case.
This function also copies the path to the Nix store, which may not be what you want.
This behavior is deprecated and will throw an error in the future.''
(builtins.match ".*${escapeRegex infix}.*" "${content}" != null);
This function also copies the path to the Nix store, which may not be what you want.''
else
content: builtins.match ".*${escapedInfix}.*" "${content}" != null;
/**
Convert a string `s` to a list of characters (i.e. singleton strings).
@@ -1303,8 +1320,11 @@ rec {
:::
*/
toShellVar =
name: value:
lib.throwIfNot (isValidPosixName name) "toShellVar: ${name} is not a valid shell variable name" (
name:
if (!isValidPosixName name) then
throw "toShellVar: ${name} is not a valid shell variable name"
else
value:
if isAttrs value && !isStringLike value then
"declare -A ${name}=(${
concatStringsSep " " (lib.mapAttrsToList (n: v: "[${escapeShellArg n}]=${escapeShellArg v}") value)
@@ -1312,8 +1332,7 @@ rec {
else if isList value then
"declare -a ${name}=(${escapeShellArgs value})"
else
"${name}=${escapeShellArg value}"
);
"${name}=${escapeShellArg value}";
/**
Translate an attribute set `vars` into corresponding shell variable declarations
@@ -1575,9 +1594,9 @@ rec {
(
let
firstChar = substring 0 1 str;
rest = substring 1 (stringLength str) str;
rest = substring 1 (-1) str; # -1 takes till the end of the string
in
addContextFrom str (toUpper firstChar + toLower rest)
toUpper firstChar + toLower rest
);
/**
@@ -1634,7 +1653,7 @@ rec {
first = if length parts > 0 then toLower (head parts) else "";
rest = if length parts > 1 then map toSentenceCase (tail parts) else [ ];
in
concatStrings (map (addContextFrom str) ([ first ] ++ rest))
concatStrings ([ first ] ++ rest)
);
/**
@@ -1721,13 +1740,11 @@ rec {
:::
*/
splitString =
sep: s:
sep:
let
splits = builtins.filter builtins.isString (
builtins.split (escapeRegex (toString sep)) (toString s)
);
escapedSep = escapeRegex (toString sep);
in
map (addContextFrom s) splits;
s: map (addContextFrom s) (filter isString (split escapedSep (toString s)));
/**
Splits a string into substrings based on a predicate that examines adjacent characters.
@@ -1794,31 +1811,27 @@ rec {
predicate: keepSplit: str:
let
len = stringLength str;
withContext = addContextFrom str;
# Helper function that processes the string character by character
go =
pos: currentPart: result:
# Base case: reached end of string
if pos == len then
result ++ [ currentPart ]
result ++ [ (withContext currentPart) ]
else
let
currChar = substring pos 1 str;
prevChar = if pos > 0 then substring (pos - 1) 1 str else "";
isSplit = predicate prevChar currChar;
in
if isSplit then
if predicate prevChar currChar then
# Split here - add current part to results and start a new one
let
newResult = result ++ [ currentPart ];
newCurrentPart = if keepSplit then currChar else "";
in
go (pos + 1) newCurrentPart newResult
go (pos + 1) (if keepSplit then currChar else "") (result ++ [ (withContext currentPart) ])
else
# Keep building current part
go (pos + 1) (currentPart + currChar) result;
in
if len == 0 then [ (addContextFrom str "") ] else map (addContextFrom str) (go 0 "" [ ]);
if len == 0 then [ (withContext "") ] else go 0 "" [ ];
/**
Returns a string without the specified prefix, if the prefix matches.
@@ -1851,25 +1864,24 @@ rec {
:::
*/
removePrefix =
prefix: str:
# Before 23.05, paths would be copied to the store before converting them
# to strings and comparing. This was surprising and confusing.
warnIf (isPath prefix)
''
prefix:
let
preLen = stringLength prefix;
in
if isPath prefix then
# Before 23.05, paths would be copied to the store before converting them
# to strings and comparing. This was surprising and confusing.
throw ''
lib.strings.removePrefix: The first argument (${toString prefix}) is a path value, but only strings are supported.
There is almost certainly a bug in the calling code, since this function never removes any prefix in such a case.
This function also copies the path to the Nix store, which may not be what you want.
This behavior is deprecated and will throw an error in the future.''
(
let
preLen = stringLength prefix;
in
if substring 0 preLen str == prefix then
# -1 will take the string until the end
substring preLen (-1) str
else
str
);
This function also copies the path to the Nix store, which may not be what you want.''
else
str:
if substring 0 preLen str == prefix then
# -1 will take the string until the end
substring preLen (-1) str
else
str;
/**
Returns a string without the specified suffix, if the suffix matches.
@@ -1902,25 +1914,26 @@ rec {
:::
*/
removeSuffix =
suffix: str:
# Before 23.05, paths would be copied to the store before converting them
# to strings and comparing. This was surprising and confusing.
warnIf (isPath suffix)
''
suffix:
let
sufLen = stringLength suffix;
in
if isPath suffix then
# Before 23.05, paths would be copied to the store before converting them
# to strings and comparing. This was surprising and confusing.
throw ''
lib.strings.removeSuffix: The first argument (${toString suffix}) is a path value, but only strings are supported.
There is almost certainly a bug in the calling code, since this function never removes any suffix in such a case.
This function also copies the path to the Nix store, which may not be what you want.
This behavior is deprecated and will throw an error in the future.''
(
let
sufLen = stringLength suffix;
sLen = stringLength str;
in
if sufLen <= sLen && suffix == substring (sLen - sufLen) sufLen str then
substring 0 (sLen - sufLen) str
else
str
);
This function also copies the path to the Nix store, which may not be what you want.''
else
str:
let
sLen = stringLength str;
in
if sufLen <= sLen && suffix == substring (sLen - sufLen) sufLen str then
substring 0 (sLen - sufLen) str
else
str;
/**
Returns true if string `v1` denotes a version older than `v2`.
@@ -1986,7 +1999,7 @@ rec {
:::
*/
versionAtLeast = v1: v2: !versionOlder v1 v2;
versionAtLeast = v1: v2: compareVersions v2 v1 != 1;
/**
This function takes an argument `x` that's either a derivation or a
@@ -2149,8 +2162,9 @@ rec {
"LIST"
];
in
type: feature: value:
type:
assert (elem (toUpper type) types);
feature: value:
assert (isString feature);
assert (isString value);
"-D${feature}:${toUpper type}=${value}";
@@ -2186,9 +2200,8 @@ rec {
*/
cmakeBool =
condition: flag:
assert (lib.isString condition);
assert (lib.isBool flag);
cmakeOptionType "bool" condition (lib.toUpper (lib.boolToString flag));
cmakeOptionType "bool" condition (if flag then "TRUE" else "FALSE");
/**
Create a `"-D<feature>:STRING=<value>"` string that can be passed to typical
@@ -2220,11 +2233,7 @@ rec {
:::
*/
cmakeFeature =
feature: value:
assert (lib.isString feature);
assert (lib.isString value);
cmakeOptionType "string" feature value;
cmakeFeature = cmakeOptionType "string";
/**
Create a `"-D<feature>=<value>"` string that can be passed to typical Meson
@@ -2294,7 +2303,6 @@ rec {
*/
mesonBool =
condition: flag:
assert (lib.isString condition);
assert (lib.isBool flag);
mesonOption condition (lib.boolToString flag);
@@ -2331,7 +2339,6 @@ rec {
*/
mesonEnable =
feature: flag:
assert (lib.isString feature);
assert (lib.isBool flag);
mesonOption feature (if flag then "enabled" else "disabled");