Add expand_template rule (#330)

Resolves: https://github.com/bazelbuild/bazel-skylib/issues/191

An example of how this can be useful can be found here: https://github.com/catchorg/Catch2/pull/2387/files

Could be also helpful here: https://github.com/AcademySoftwareFoundation/openexr/blob/8587f4eed1d396a9c545d59e7b231d9d4b2cd8ef/BUILD.bazel#L21
diff --git a/README.md b/README.md
index 58e0878..3c4ec2b 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,7 @@
 * [analysis_test](docs/analysis_test_doc.md)
 * [build_test](docs/build_test_doc.md)
 * [copy_file](docs/copy_file_doc.md)
+* [expand_template](docs/expand_template_doc.md)
 * [write_file](docs/write_file_doc.md)
 
 ## Writing a new module
diff --git a/docs/BUILD b/docs/BUILD
index 873e26c..284db3c 100644
--- a/docs/BUILD
+++ b/docs/BUILD
@@ -38,6 +38,11 @@
 )
 
 stardoc_with_diff_test(
+    bzl_library_target = "//rules:expand_template",
+    out_label = "//docs:expand_template_doc.md",
+)
+
+stardoc_with_diff_test(
     bzl_library_target = "//rules:native_binary",
     out_label = "//docs:native_binary_doc.md",
 )
diff --git a/docs/expand_template_doc.md b/docs/expand_template_doc.md
new file mode 100755
index 0000000..b086829
--- /dev/null
+++ b/docs/expand_template_doc.md
@@ -0,0 +1,33 @@
+<!-- Generated with Stardoc: http://skydoc.bazel.build -->
+
+A rule that performes template expansion.
+
+
+<a id="#expand_template"></a>
+
+## expand_template
+
+<pre>
+expand_template(<a href="#expand_template-name">name</a>, <a href="#expand_template-template">template</a>, <a href="#expand_template-substitutions">substitutions</a>, <a href="#expand_template-out">out</a>)
+</pre>
+
+Template expansion
+
+This performs a simple search over the template file for the keys in substitutions,
+and replaces them with the corresponding values.
+
+There is no special syntax for the keys.
+To avoid conflicts, you would need to explicitly add delimiters to the key strings, for example "{KEY}" or "@KEY@".
+
+
+**PARAMETERS**
+
+
+| Name  | Description | Default Value |
+| :------------- | :------------- | :------------- |
+| <a id="expand_template-name"></a>name |  The name of the rule.   |  none |
+| <a id="expand_template-template"></a>template |  The template file to expand   |  none |
+| <a id="expand_template-substitutions"></a>substitutions |  A dictionary mapping strings to their substitutions   |  none |
+| <a id="expand_template-out"></a>out |  The destination of the expanded file   |  none |
+
+
diff --git a/rules/BUILD b/rules/BUILD
index f7017bb..b89495e 100644
--- a/rules/BUILD
+++ b/rules/BUILD
@@ -33,6 +33,11 @@
 )
 
 bzl_library(
+    name = "expand_template",
+    srcs = ["expand_template.bzl"],
+)
+
+bzl_library(
     name = "native_binary",
     srcs = ["native_binary.bzl"],
     deps = ["//rules/private:copy_file_private"],
diff --git a/rules/expand_template.bzl b/rules/expand_template.bzl
new file mode 100644
index 0000000..58f1920
--- /dev/null
+++ b/rules/expand_template.bzl
@@ -0,0 +1,55 @@
+# Copyright 2022 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A rule that performes template expansion.
+"""
+
+def _expand_template_impl(ctx):
+    ctx.actions.expand_template(
+        template = ctx.file.template,
+        output = ctx.outputs.out,
+        substitutions = ctx.attr.substitutions,
+    )
+
+_expand_template = rule(
+    implementation = _expand_template_impl,
+    attrs = {
+        "template": attr.label(mandatory = True, allow_single_file = True),
+        "substitutions": attr.string_dict(mandatory = True),
+        "out": attr.output(mandatory = True),
+    },
+    output_to_genfiles = True,
+)
+
+def expand_template(name, template, substitutions, out):
+    """Template expansion
+
+    This performs a simple search over the template file for the keys in substitutions,
+    and replaces them with the corresponding values.
+
+    There is no special syntax for the keys.
+    To avoid conflicts, you would need to explicitly add delimiters to the key strings, for example "{KEY}" or "@KEY@".
+
+    Args:
+      name: The name of the rule.
+      template: The template file to expand
+      out: The destination of the expanded file
+      substitutions: A dictionary mapping strings to their substitutions
+    """
+    _expand_template(
+        name = name,
+        template = template,
+        substitutions = substitutions,
+        out = out,
+    )
diff --git a/tests/expand_template/BUILD b/tests/expand_template/BUILD
new file mode 100644
index 0000000..0607111
--- /dev/null
+++ b/tests/expand_template/BUILD
@@ -0,0 +1,57 @@
+# Copyright 2022 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This package aids testing the 'diff_test' rule.
+
+load("//rules:expand_template.bzl", "expand_template")
+
+expand_template(
+    name = "filled_template",
+    out = "foo/test.yaml",
+    substitutions = {
+        "@name@": "test",
+        "@version@": "1.1.1",
+    },
+    template = "test.tpl.yaml",
+)
+
+sh_test(
+    name = "template_test",
+    srcs = ["template_test.sh"],
+    data = [
+        "foo/test.yaml",
+        ":filled_template",
+        "//tests:unittest.bash",
+    ],
+    deps = [
+        "@bazel_tools//tools/bash/runfiles",
+    ],
+)
+
+expand_template(
+    name = "version",
+    out = "version.h",
+    substitutions = {
+        "@VERSION@": "2.3.4",
+    },
+    template = "version.h.in",
+)
+
+cc_test(
+    name = "test",
+    srcs = [
+        "test.cc",
+        ":version",
+    ],
+)
diff --git a/tests/expand_template/template_test.sh b/tests/expand_template/template_test.sh
new file mode 100755
index 0000000..dcddf2c
--- /dev/null
+++ b/tests/expand_template/template_test.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+# Copyright 2022 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# --- begin runfiles.bash initialization v2 ---
+# Copy-pasted from the Bazel Bash runfiles library v2.
+set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash
+source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
+    source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
+    source "$0.runfiles/$f" 2>/dev/null || \
+    source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+    source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+    { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
+# --- end runfiles.bash initialization v2 ---
+
+source "$(rlocation bazel_skylib/tests/unittest.bash)" \
+  || { echo "Could not source bazel_skylib/tests/unittest.bash" >&2; exit 1; }
+
+function test_expand_template() {
+  cat "$(rlocation bazel_skylib/tests/expand_template/foo/test.yaml)" >"$TEST_log"
+  expect_log 'name: test'
+  expect_log 'version: 1.1.1'
+}
+
+run_suite "expand_template_tests test suite"
\ No newline at end of file
diff --git a/tests/expand_template/test.cc b/tests/expand_template/test.cc
new file mode 100644
index 0000000..3b13e7a
--- /dev/null
+++ b/tests/expand_template/test.cc
@@ -0,0 +1,27 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "tests/expand_template/version.h"
+
+#include <cstring>
+
+int main(int argc, char **argv) {
+  // VERSION should be "2.3.4"
+  if(strcmp(VERSION, "2.3.4") == 0) {
+    return 0; // success
+  }
+  else {
+    return 1; // failure
+  }
+}
diff --git a/tests/expand_template/test.tpl.yaml b/tests/expand_template/test.tpl.yaml
new file mode 100644
index 0000000..03d9fdd
--- /dev/null
+++ b/tests/expand_template/test.tpl.yaml
@@ -0,0 +1,2 @@
+name: @name@
+version: @version@
diff --git a/tests/expand_template/version.h.in b/tests/expand_template/version.h.in
new file mode 100644
index 0000000..deb8418
--- /dev/null
+++ b/tests/expand_template/version.h.in
@@ -0,0 +1 @@
+#define VERSION "@VERSION@"
\ No newline at end of file