about summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
authorMatt Sturgeon2024-05-28 18:22:43 +0100
committerMatt Sturgeon2024-07-25 23:43:33 +0100
commitaad87c2aa8d00fe8a9c4d8198cf166d652f8f28c (patch)
tree393fab90dfcd7380d2d231fdbeed75d55e6547f5 /lib
parentd1e70dde57184785861ce9cf4e0ec10e378ffa65 (diff)
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.
Diffstat (limited to 'lib')
-rw-r--r--lib/default.nix2
-rw-r--r--lib/strings.nix62
-rw-r--r--lib/tests/misc.nix66
3 files changed, 129 insertions, 1 deletions
diff --git a/lib/default.nix b/lib/default.nix
index d637ca203f0e..63a31101eee7 100644
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -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
diff --git a/lib/strings.nix b/lib/strings.nix
index 67bb669d04e0..18ef707750bb 100644
--- a/lib/strings.nix
+++ b/lib/strings.nix
@@ -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.
 
diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix
index 4294d29b47ef..d59f5586b82d 100644
--- a/lib/tests/misc.nix
+++ b/lib/tests/misc.nix
@@ -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" ];