Implement environment variable substitution for unix platforms

I want to be able to use this to redirect the bazel config file based
on the environment, for integration with our existing
environment-based mechanisms for selecting build configurations.

I found some opinions in comments which said this was too difficult,
which I have amended.

Closes #7318.

PiperOrigin-RevId: 237467945
diff --git a/src/main/cpp/BUILD b/src/main/cpp/BUILD
index 1af898d..7135055 100644
--- a/src/main/cpp/BUILD
+++ b/src/main/cpp/BUILD
@@ -125,13 +125,18 @@
     # The system bazelrc can be voided by setting BAZEL_SYSTEM_BAZELRC_PATH to
     # /dev/null.
     copts = select({
-        # We need to escape for multiple levels, here, this becomes
+        # For windows platforms, this can include environment
+        # variables in the form %ProgramData%. We need to escape for
+        # multiple levels, here, this becomes
         # /DBAZEL_SYSTEM_BAZELRC_PATH="%%ProgramData%%/bazel.bazelrc"',
-        # and the double % get reduced down to 1 by the compiler. A forward
-        # slash is used because \b is a special character, backspace.
+        # and the double % get reduced down to 1 by the compiler. A
+        # forward slash is used because \b is a special character,
+        # backspace.
         "//src/conditions:windows": [
             "/DBAZEL_SYSTEM_BAZELRC_PATH#\\\"%%ProgramData%%/bazel.bazelrc\\\"",
         ],
+        # For posix platforms, this can include environment variables in the
+        # form ${var_name}. Braces are required.
         "//conditions:default": [
             "-DBAZEL_SYSTEM_BAZELRC_PATH=\\\"/etc/bazel.bazelrc\\\"",
         ],
diff --git a/src/main/cpp/option_processor.cc b/src/main/cpp/option_processor.cc
index 181afd2..6c4e3f4 100644
--- a/src/main/cpp/option_processor.cc
+++ b/src/main/cpp/option_processor.cc
@@ -220,7 +220,7 @@
 
 std::string FindSystemWideRc(const std::string& system_bazelrc_path) {
   const std::string path =
-      blaze_util::MakeAbsoluteAndResolveWindowsEnvvars(system_bazelrc_path);
+      blaze_util::MakeAbsoluteAndResolveEnvvars(system_bazelrc_path);
   if (blaze_util::CanReadFile(path)) {
     return path;
   }
@@ -325,11 +325,11 @@
 
   // Get the system rc (unless --nosystem_rc).
   if (SearchNullaryOption(cmd_line->startup_args, "system_rc", true)) {
-    // MakeAbsoluteAndResolveWindowsEnvvars will standardize the form of the
+    // MakeAbsoluteAndResolveEnvvars will standardize the form of the
     // provided path. This also means we accept relative paths, which is
     // is convenient for testing.
     const std::string system_rc =
-        blaze_util::MakeAbsoluteAndResolveWindowsEnvvars(system_bazelrc_path_);
+        blaze_util::MakeAbsoluteAndResolveEnvvars(system_bazelrc_path_);
     rc_files.push_back(system_rc);
   }
 
diff --git a/src/main/cpp/util/path_platform.h b/src/main/cpp/util/path_platform.h
index c3f098c..329380f 100644
--- a/src/main/cpp/util/path_platform.h
+++ b/src/main/cpp/util/path_platform.h
@@ -54,15 +54,16 @@
 //   MakeAbsolute("C:/foo") ---> "C:/foo"
 std::string MakeAbsolute(const std::string &path);
 
-// Returns the given path in absolute form, taking into account a possible
-// starting environment variable on the windows platform, so that we can
-// accept standard path variables like %USERPROFILE%. We do not support
-// unix-style envvars here: recreating that logic is error-prone and not
-// worthwhile, since they are less critical to standard paths as in Windows.
+// Returns the given path in absolute form, taking into account a
+// possible starting environment variable, so that we can accept
+// standard path variables like %USERPROFILE% or ${BAZEL}. For
+// simplicity, we implement only those two forms, not $BAZEL.
 //
 //   MakeAbsolute("foo") in wd "/bar" --> "/bar/foo"
-//   MakeAbsolute("%USERPROFILE%/foo") --> "C:\Users\bazel-user\foo"
-std::string MakeAbsoluteAndResolveWindowsEnvvars(const std::string &path);
+//   MakeAbsoluteAndResolveEnvvars("%USERPROFILE%/foo") -->
+//       "C:\Users\bazel-user\foo"
+//   MakeAbsoluteAndResolveEnvvars("${BAZEL}/foo") --> "/opt/bazel/foo"
+std::string MakeAbsoluteAndResolveEnvvars(const std::string &path);
 
 // TODO(bazel-team) consider changing the path(_platform) header split to be a
 // path.h and path_windows.h split, which would make it clearer what functions
diff --git a/src/main/cpp/util/path_posix.cc b/src/main/cpp/util/path_posix.cc
index 9c0b440..11577c3 100644
--- a/src/main/cpp/util/path_posix.cc
+++ b/src/main/cpp/util/path_posix.cc
@@ -16,6 +16,7 @@
 
 #include <limits.h>  // PATH_MAX
 
+#include <stdlib.h>  // getenv
 #include <string.h>  // strncmp
 #include <unistd.h>  // access, open, close, fsync
 #include "src/main/cpp/util/errors.h"
@@ -67,8 +68,30 @@
   return JoinPath(blaze_util::GetCwd(), path);
 }
 
-std::string MakeAbsoluteAndResolveWindowsEnvvars(const std::string &path) {
-  return MakeAbsolute(path);
+std::string ResolveEnvvars(const std::string &path) {
+  std::string result = path;
+  size_t start = 0;
+  while ((start = result.find("${", start)) != std::string::npos) {
+    // Just match to the next }
+    size_t end = result.find("}", start + 1);
+    if (end == std::string::npos) {
+      BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
+          << "ResolveEnvvars(" << path << "): incomplete variable at position "
+          << start;
+    }
+    // Extract the variable name
+    const std::string name = result.substr(start + 2, end - start - 2);
+    // Get the value from the environment
+    const char *c_value = getenv(name.c_str());
+    const std::string value = std::string(c_value ? c_value : "");
+    result.replace(start, end - start + 1, value);
+    start += value.length();
+  }
+  return result;
+}
+
+std::string MakeAbsoluteAndResolveEnvvars(const std::string &path) {
+  return MakeAbsolute(ResolveEnvvars(path));
 }
 
 }  // namespace blaze_util
diff --git a/src/main/cpp/util/path_windows.cc b/src/main/cpp/util/path_windows.cc
index e1bde22..0f42276 100644
--- a/src/main/cpp/util/path_windows.cc
+++ b/src/main/cpp/util/path_windows.cc
@@ -88,7 +88,7 @@
       WstringToCstring(RemoveUncPrefixMaybe(wpath.c_str())).get());
 }
 
-std::string MakeAbsoluteAndResolveWindowsEnvvars(const std::string& path) {
+std::string MakeAbsoluteAndResolveEnvvars(const std::string& path) {
   // Get the size of the expanded string, so we know how big of a buffer to
   // provide. The returned size includes the null terminator.
   std::unique_ptr<CHAR[]> resolved(new CHAR[MAX_PATH]);
diff --git a/src/test/cpp/util/path_posix_test.cc b/src/test/cpp/util/path_posix_test.cc
index df79b59..7c08490 100644
--- a/src/test/cpp/util/path_posix_test.cc
+++ b/src/test/cpp/util/path_posix_test.cc
@@ -159,14 +159,28 @@
   EXPECT_EQ(MakeAbsolute(""), "");
 }
 
-TEST(PathPosixTest, MakeAbsoluteAndResolveWindowsEnvvars) {
-  // Check that Unix-style envvars are not resolved.
-  EXPECT_EQ(MakeAbsoluteAndResolveWindowsEnvvars("$PATH"),
-            JoinPath(GetCwd(), "$PATH"));
-  EXPECT_EQ(MakeAbsoluteAndResolveWindowsEnvvars("${PATH}"),
-            JoinPath(GetCwd(), "${PATH}"));
+TEST(PathPosixTest, MakeAbsoluteAndResolveEnvvars) {
+  // Check that Unix-style envvars are resolved.
+  const std::string tmpdir = getenv("TEST_TMPDIR");
+  const std::string expected_tmpdir_bar = ConvertPath(tmpdir + "/bar");
+  setenv("PATH_POSIX_TEST_ENV", "${TEST_TMPDIR}", 1);
+
+  // Using an existing environment variable
+  EXPECT_EQ(expected_tmpdir_bar,
+            MakeAbsoluteAndResolveEnvvars("${TEST_TMPDIR}/bar"));
+  // Using an undefined environment variable (case-sensitive)
+  EXPECT_EQ("/bar", MakeAbsoluteAndResolveEnvvars("${test_tmpdir}/bar"));
+
+  // This style of variable is not supported
+  EXPECT_EQ(JoinPath(GetCwd(), "$TEST_TMPDIR/bar"),
+            MakeAbsoluteAndResolveEnvvars("$TEST_TMPDIR/bar"));
+
+  // Only one layer of variables is expanded, we do not recurse
+  EXPECT_EQ(JoinPath(GetCwd(), "${TEST_TMPDIR}/bar"),
+            MakeAbsoluteAndResolveEnvvars("${PATH_POSIX_TEST_ENV}/bar"));
+
   // Check that Windows-style envvars are not resolved when not on Windows.
-  EXPECT_EQ(MakeAbsoluteAndResolveWindowsEnvvars("%PATH%"),
+  EXPECT_EQ(MakeAbsoluteAndResolveEnvvars("%PATH%"),
             JoinPath(GetCwd(), "%PATH%"));
 }
 
diff --git a/src/test/cpp/util/path_windows_test.cc b/src/test/cpp/util/path_windows_test.cc
index 6cd027e..ef29083 100644
--- a/src/test/cpp/util/path_windows_test.cc
+++ b/src/test/cpp/util/path_windows_test.cc
@@ -392,7 +392,7 @@
   EXPECT_EQ("", MakeAbsolute(""));
 }
 
-TEST(PathWindowsTest, MakeAbsoluteAndResolveWindowsEnvvars_WithTmpdir) {
+TEST(PathWindowsTest, MakeAbsoluteAndResolveEnvvars_WithTmpdir) {
   // We cannot test the system-default paths like %ProgramData% because these
   // are wiped from the test environment. TestTmpdir is set by Bazel though,
   // so serves as a fine substitute.
@@ -402,20 +402,20 @@
   const std::string expected_tmpdir_bar = ConvertPath(tmpdir + "\\bar");
 
   EXPECT_EQ(expected_tmpdir_bar,
-            MakeAbsoluteAndResolveWindowsEnvvars("%TEST_TMPDIR%\\bar"));
+            MakeAbsoluteAndResolveEnvvars("%TEST_TMPDIR%\\bar"));
   EXPECT_EQ(expected_tmpdir_bar,
-            MakeAbsoluteAndResolveWindowsEnvvars("%Test_Tmpdir%\\bar"));
+            MakeAbsoluteAndResolveEnvvars("%Test_Tmpdir%\\bar"));
   EXPECT_EQ(expected_tmpdir_bar,
-            MakeAbsoluteAndResolveWindowsEnvvars("%test_tmpdir%\\bar"));
+            MakeAbsoluteAndResolveEnvvars("%test_tmpdir%\\bar"));
   EXPECT_EQ(expected_tmpdir_bar,
-            MakeAbsoluteAndResolveWindowsEnvvars("%test_tmpdir%/bar"));
+            MakeAbsoluteAndResolveEnvvars("%test_tmpdir%/bar"));
 }
 
-TEST(PathWindowsTest, MakeAbsoluteAndResolveWindowsEnvvars_LongPaths) {
+TEST(PathWindowsTest, MakeAbsoluteAndResolveEnvvars_LongPaths) {
   const std::string long_path = "c:\\" + std::string(MAX_PATH, 'a');
   blaze::SetEnv("long", long_path);
 
-  EXPECT_EQ(long_path, MakeAbsoluteAndResolveWindowsEnvvars("%long%"));
+  EXPECT_EQ(long_path, MakeAbsoluteAndResolveEnvvars("%long%"));
 }
 
 }  // namespace blaze_util