aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/functions-string.cxx
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2024-04-01 10:23:37 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2024-04-01 10:23:37 +0200
commit221a53ed3d18217d06f7d8e0bdf9ce315ca2413c (patch)
tree389225f308a7eaba4469be80513b768839854b13 /libbuild2/functions-string.cxx
parentd28970114e5807f57ae339764ac05384ef163379 (diff)
Add $string.replace() function
Diffstat (limited to 'libbuild2/functions-string.cxx')
-rw-r--r--libbuild2/functions-string.cxx132
1 files changed, 131 insertions, 1 deletions
diff --git a/libbuild2/functions-string.cxx b/libbuild2/functions-string.cxx
index 367923f..0458724 100644
--- a/libbuild2/functions-string.cxx
+++ b/libbuild2/functions-string.cxx
@@ -8,6 +8,103 @@ using namespace std;
namespace build2
{
+ static string
+ replace (string&& s, value&& fv, value&& tv, optional<names>&& fs)
+ {
+ bool ic (false), fo (false), lo (false);
+ if (fs)
+ {
+ for (name& f: *fs)
+ {
+ string s (convert<string> (move (f)));
+
+ if (s == "icase")
+ ic = true;
+ else if (s == "first_only")
+ fo = true;
+ else if (s == "last_only")
+ lo = true;
+ else
+ throw invalid_argument ("invalid flag '" + s + '\'');
+ }
+ }
+
+ string f (convert<string> (move (fv)));
+ string t (convert<string> (move (tv)));
+
+ if (f.empty ())
+ throw invalid_argument ("empty <from> substring");
+
+ if (!s.empty ())
+ {
+ // Note that we don't cache s.size () since the string size will be
+ // changing as we are replacing. In fact, we may end up with an empty
+ // string after a replacement.
+
+ size_t fn (f.size ());
+
+ // Look for the substring forward in the [p, n) range.
+ //
+ auto find = [&s, &f, fn, ic] (size_t p) -> size_t
+ {
+ for (size_t n (s.size ()); p != n; ++p)
+ {
+ if (n - p >= fn &&
+ (ic
+ ? icasecmp (f, s.c_str () + p, fn)
+ : s.compare (p, fn, f)) == 0)
+ return p;
+ }
+
+ return string::npos;
+ };
+
+ // Look for the substring backard in the [0, n) range.
+ //
+ auto rfind = [&s, &f, fn, ic] (size_t n) -> size_t
+ {
+ if (n >= fn)
+ {
+ n -= fn; // Don't consider characters out of range.
+
+ for (size_t p (n);; )
+ {
+ if ((ic
+ ? icasecmp (f, s.c_str () + p, fn)
+ : s.compare (p, fn, f)) == 0)
+ return p;
+
+ if (--p == 0)
+ break;
+ }
+ }
+
+ return string::npos;
+ };
+
+ if (fo || lo)
+ {
+ size_t p (lo ? rfind (s.size ()) : find (0));
+
+ if (fo && lo && p != string::npos)
+ {
+ if (p != find (0))
+ p = string::npos;
+ }
+
+ if (p != string::npos)
+ s.replace (p, fn, t);
+ }
+ else
+ {
+ for (size_t p (0); (p = find (0)) != string::npos; p += fn)
+ s.replace (p, fn, t);
+ }
+ }
+
+ return s;
+ }
+
static size_t
find_index (const strings& vs, value&& v, optional<names>&& fs)
{
@@ -32,7 +129,7 @@ namespace build2
}));
return i != vs.end () ? i - vs.begin () : vs.size ();
- };
+ }
void
string_functions (function_map& m)
@@ -76,6 +173,39 @@ namespace build2
convert<string> (move (y))) == 0;
};
+ // $string.replace(<untyped>, <from>, <to> [, <flags>])
+ // $replace(<string>, <from>, <to> [, <flags>])
+ //
+ // Replace occurences of substring <from> with <to> in a string. The
+ // <from> substring must not be empty.
+ //
+ // The following flags are supported:
+ //
+ // icase - compare ignoring case
+ //
+ // first_only - only replace the first match
+ //
+ // last_only - only replace the last match
+ //
+ //
+ // If both `first_only` and `last_only` flags are specified, then <from>
+ // is replaced only if it occurs in the string once.
+ //
+ // See also `$regex.replace()`.
+ //
+ f["replace"] += [](string s, value f, value t, optional<names> fs)
+ {
+ return replace (move (s), move (f), move (t), move (fs));
+ };
+
+ f[".replace"] += [](names s, value f, value t, optional<names> fs)
+ {
+ return names {
+ name (
+ replace (
+ convert<string> (move (s)), move (f), move (t), move (fs)))};
+ };
+
// $string.trim(<untyped>)
// $trim(<string>)
//