Add string_replace builtin function

This change introduces a string_replace builtin function that can
be used to replace occurrences of a string within a string with
another string.

Change-Id: I7b884863c6bc28cd02157fdd51052224399a195d
Reviewed-on: https://gn-review.googlesource.com/2560
Reviewed-by: Brett Wilson <brettw@chromium.org>
Commit-Queue: Petr Hosek <phosek@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 48017d5..f2b84f9 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -52,6 +52,7 @@
     *   [set_defaults: Set default values for a target type.](#set_defaults)
     *   [set_sources_assignment_filter: Set a pattern to filter source files.](#set_sources_assignment_filter)
     *   [split_list: Splits a list into N different sub-lists.](#split_list)
+    *   [string_replace: Replaces substring in the given string.](#string_replace)
     *   [template: Define a template rule.](#template)
     *   [tool: Specify arguments to a toolchain tool.](#tool)
     *   [toolchain: Defines a toolchain.](#toolchain)
@@ -2605,6 +2606,27 @@
   Will print:
     [[1, 2], [3, 4], [5, 6]
 ```
+### <a name="string_replace"></a>**string_replace**: Replaces substring in the given string.
+
+```
+  result = string_replace(str, old, new[, max])
+
+  Returns a copy of the string str in which the occurrences of old have been
+  replaced with new, optionally restricting the number of replacements. The
+  replacement is performed sequentially, so if new contains old, it won't be
+  replaced.
+```
+
+#### **Example**
+
+```
+  The code:
+    mystr = "Hello, world!"
+    print(string_replace(mystr, "world", "GN"))
+
+  Will print:
+    Hello, GN!
+```
 ### <a name="template"></a>**template**: Define a template rule.
 
 ```
diff --git a/tools/gn/functions.cc b/tools/gn/functions.cc
index b5bb1e3..7fc8b62 100644
--- a/tools/gn/functions.cc
+++ b/tools/gn/functions.cc
@@ -7,6 +7,7 @@
 #include <stddef.h>
 #include <iostream>
 #include <memory>
+#include <regex>
 #include <utility>
 
 #include "base/environment.h"
@@ -1079,6 +1080,75 @@
   return result;
 }
 
+// string_replace --------------------------------------------------------------
+
+const char kStringReplace[] = "string_replace";
+const char kStringReplace_HelpShort[] =
+    "string_replace: Replaces substring in the given string.";
+const char kStringReplace_Help[] =
+    R"(string_replace: Replaces substring in the given string.
+
+  result = string_replace(str, old, new[, max])
+
+  Returns a copy of the string str in which the occurrences of old have been
+  replaced with new, optionally restricting the number of replacements. The
+  replacement is performed sequentially, so if new contains old, it won't be
+  replaced.
+
+Example
+
+  The code:
+    mystr = "Hello, world!"
+    print(string_replace(mystr, "world", "GN"))
+
+  Will print:
+    Hello, GN!
+)";
+
+Value RunStringReplace(Scope* scope,
+                       const FunctionCallNode* function,
+                       const std::vector<Value>& args,
+                       Err* err) {
+  if (args.size() < 3 || args.size() > 4) {
+    *err = Err(function, "Wrong number of arguments to string_replace().");
+    return Value();
+  }
+
+  if (!args[0].VerifyTypeIs(Value::STRING, err))
+    return Value();
+  const std::string str = args[0].string_value();
+
+  if (!args[1].VerifyTypeIs(Value::STRING, err))
+    return Value();
+  const std::string& old = args[1].string_value();
+
+  if (!args[2].VerifyTypeIs(Value::STRING, err))
+    return Value();
+  const std::string& new_ = args[2].string_value();
+
+  int64_t max = INT64_MAX;
+  if (args.size() > 3) {
+    if (!args[3].VerifyTypeIs(Value::INTEGER, err))
+      return Value();
+    max = args[3].int_value();
+    if (max <= 0) {
+      *err = Err(function, "Requested number of replacements is not positive.");
+      return Value();
+    }
+  }
+
+  int64_t n = 0;
+  std::string val(str);
+  size_t start_pos = 0;
+  while((start_pos = val.find(old, start_pos)) != std::string::npos) {
+    val.replace(start_pos, old.length(), new_);
+    start_pos += new_.length();
+    if (++n >= max)
+      break;
+  }
+  return Value(function, std::move(val));
+}
+
 // -----------------------------------------------------------------------------
 
 FunctionInfo::FunctionInfo()
@@ -1186,6 +1256,7 @@
     INSERT_FUNCTION(SetDefaultToolchain, false)
     INSERT_FUNCTION(SetSourcesAssignmentFilter, false)
     INSERT_FUNCTION(SplitList, false)
+    INSERT_FUNCTION(StringReplace, false)
     INSERT_FUNCTION(Template, false)
     INSERT_FUNCTION(Tool, false)
     INSERT_FUNCTION(Toolchain, false)
diff --git a/tools/gn/functions.h b/tools/gn/functions.h
index 4638276..5c707c4 100644
--- a/tools/gn/functions.h
+++ b/tools/gn/functions.h
@@ -329,6 +329,14 @@
                        BlockNode* block,
                        Err* err);
 
+extern const char kReplaceSubstr[];
+extern const char kReplaceSubstr_HelpShort[];
+extern const char kReplaceSubstr_Help[];
+Value RunReplaceSubstr(Scope* scope,
+                       const FunctionCallNode* function,
+                       const std::vector<Value>& args_list,
+                       Err* err);
+
 extern const char kTarget[];
 extern const char kTarget_HelpShort[];
 extern const char kTarget_Help[];
diff --git a/tools/gn/functions_unittest.cc b/tools/gn/functions_unittest.cc
index 8bd3e3a..589986c 100644
--- a/tools/gn/functions_unittest.cc
+++ b/tools/gn/functions_unittest.cc
@@ -125,6 +125,39 @@
       setup.print_output());
 }
 
+TEST(Functions, StringReplace) {
+  TestWithScope setup;
+
+  TestParseInput input(
+      // Replace all occurrences of string.
+      "out1 = string_replace(\"abbcc\", \"b\", \"d\")\n"
+      "print(out1)\n"
+
+      // Replace only the first occurrence.
+      "out2 = string_replace(\"abbcc\", \"b\", \"d\", 1)\n"
+      "print(out2)\n"
+
+      // Duplicate string to be replaced.
+      "out3 = string_replace(\"abbcc\", \"b\", \"bb\")\n"
+      "print(out3)\n"
+
+      // Handle overlapping occurrences.
+      "out4 = string_replace(\"aaa\", \"aa\", \"b\")\n"
+      "print(out4)\n");
+  ASSERT_FALSE(input.has_error());
+
+  Err err;
+  input.parsed()->Execute(setup.scope(), &err);
+  ASSERT_FALSE(err.has_error()) << err.message();
+
+  EXPECT_EQ(
+      "addcc\n"
+      "adbcc\n"
+      "abbbbcc\n"
+      "ba\n",
+      setup.print_output());
+}
+
 TEST(Functions, DeclareArgs) {
   TestWithScope setup;
   Err err;