lib.strings: add trim and trimWith

`strings.trim` returns a copy of the string with all leading and trailing
whitespace removed.

`strings.trimWith` does the same thing, but calling code can decide
whether to trim the start and/or end of the string.
This commit is contained in:
Matt Sturgeon 2024-05-28 18:22:43 +01:00
parent d1e70dde57
commit aad87c2aa8
No known key found for this signature in database
GPG Key ID: 4F91844CED1A8299
3 changed files with 129 additions and 1 deletions

View File

@ -105,7 +105,7 @@ let
hasInfix hasPrefix hasSuffix stringToCharacters stringAsChars escape
escapeShellArg escapeShellArgs
isStorePath isStringLike
isValidPosixName toShellVar toShellVars
isValidPosixName toShellVar toShellVars trim trimWith
escapeRegex escapeURL escapeXML replaceChars lowerChars
upperChars toLower toUpper addContextFrom splitString
removePrefix removeSuffix versionOlder versionAtLeast

View File

@ -157,6 +157,68 @@ rec {
*/
replicate = n: s: concatStrings (lib.lists.replicate n s);
/*
Remove leading and trailing whitespace from a string.
Whitespace is defined as any of the following characters:
" ", "\t" "\r" "\n"
Type: trim :: string -> string
Example:
trim " hello, world! "
=> "hello, world!"
*/
trim = trimWith {
start = true;
end = true;
};
/*
Remove leading and/or trailing whitespace from a string.
Whitespace is defined as any of the following characters:
" ", "\t" "\r" "\n"
Type: trimWith :: Attrs -> string -> string
Example:
trimWith { start = true; } " hello, world! "}
=> "hello, world! "
trimWith { end = true; } " hello, world! "}
=> " hello, world!"
*/
trimWith =
{
# Trim leading whitespace
start ? false,
# Trim trailing whitespace
end ? false,
}:
s:
let
# Define our own whitespace character class instead of using
# `[:space:]`, which is not well-defined.
chars = " \t\r\n";
# To match up until trailing whitespace, we need to capture a
# group that ends with a non-whitespace character.
regex =
if start && end then
"[${chars}]*(.*[^${chars}])[${chars}]*"
else if start then
"[${chars}]*(.*)"
else if end then
"(.*[^${chars}])[${chars}]*"
else
"(.*)";
# If the string was empty or entirely whitespace,
# then the regex may not match and `res` will be `null`.
res = match regex s;
in
optionalString (res != null) (head res);
/* Construct a Unix-style, colon-separated search path consisting of
the given `subDir` appended to each of the given paths.

View File

@ -369,6 +369,72 @@ runTests {
expected = "hellohellohellohellohello";
};
# Test various strings are trimmed correctly
testTrimString = {
expr =
let
testValues = f: mapAttrs (_: f) {
empty = "";
cr = "\r";
lf = "\n";
tab = "\t";
spaces = " ";
leading = " Hello, world";
trailing = "Hello, world ";
mixed = " Hello, world ";
mixed-tabs = " \t\tHello, world \t \t ";
multiline = " Hello,\n world! ";
multiline-crlf = " Hello,\r\n world! ";
};
in
{
leading = testValues (strings.trimWith { start = true; });
trailing = testValues (strings.trimWith { end = true; });
both = testValues strings.trim;
};
expected = {
leading = {
empty = "";
cr = "";
lf = "";
tab = "";
spaces = "";
leading = "Hello, world";
trailing = "Hello, world ";
mixed = "Hello, world ";
mixed-tabs = "Hello, world \t \t ";
multiline = "Hello,\n world! ";
multiline-crlf = "Hello,\r\n world! ";
};
trailing = {
empty = "";
cr = "";
lf = "";
tab = "";
spaces = "";
leading = " Hello, world";
trailing = "Hello, world";
mixed = " Hello, world";
mixed-tabs = " \t\tHello, world";
multiline = " Hello,\n world!";
multiline-crlf = " Hello,\r\n world!";
};
both = {
empty = "";
cr = "";
lf = "";
tab = "";
spaces = "";
leading = "Hello, world";
trailing = "Hello, world";
mixed = "Hello, world";
mixed-tabs = "Hello, world";
multiline = "Hello,\n world!";
multiline-crlf = "Hello,\r\n world!";
};
};
};
testSplitStringsSimple = {
expr = strings.splitString "." "a.b.c.d";
expected = [ "a" "b" "c" "d" ];