Prework for --experimentalEnvvar

This CL adds the --experimentalEnvvar flag in Options and passes it to
ManifestParserOptions, which passes it to ChunkParser where subninja chdir
parsing happens. This sets the stage for a future CL to parse environment
variable assignments.

Also add to struct Include a std::unordered_map to contain those environment
variable assignments, and add support to struct Edge for commands that
have a cmdEnviron. This sets the stage for a future CL to compute an
environment that differs from the startup environment variables when
ninja was started.

This is all the practical refactoring to make the meat of the
--experimentalEnvvar code simple, while not making any change to ninja
behavior; the flag --experimentalEnvvar does nothing.

Change-Id: I7654fbcb3389fc4a6384ac395ae0f8740b9e39ec
diff --git a/src/build.cc b/src/build.cc
index 068f635..f152317 100644
--- a/src/build.cc
+++ b/src/build.cc
@@ -496,8 +496,9 @@
 }
 
 bool RealCommandRunner::StartCommand(Edge* edge) {
-  string command = edge->EvaluateCommand();
-  Subprocess* subproc = subprocs_.Add(command, edge->use_console());
+  EdgeCommand c;
+  edge->EvaluateCommand(&c);
+  Subprocess* subproc = subprocs_.Add(c);
   if (!subproc)
     return false;
   subproc_to_edge_.insert(make_pair(subproc, edge));
@@ -767,7 +768,9 @@
 
   // start command computing and run it
   if (!command_runner_->StartCommand(edge)) {
-    err->assign("command '" + edge->EvaluateCommand() + "' failed.");
+    EdgeCommand c;
+    edge->EvaluateCommand(&c);
+    err->assign("command '" + c.command + "' failed.");
     return false;
   }
 
diff --git a/src/build_log.cc b/src/build_log.cc
index 17256ee..a5c8825 100644
--- a/src/build_log.cc
+++ b/src/build_log.cc
@@ -163,8 +163,9 @@
 
 bool BuildLog::RecordCommand(Edge* edge, int start_time, int end_time,
                              TimeStamp mtime) {
-  string command = edge->EvaluateCommand(true);
-  uint64_t command_hash = LogEntry::HashCommand(command);
+  EdgeCommand c;
+  edge->EvaluateCommand(&c, true);
+  uint64_t command_hash = LogEntry::HashCommand(c.command);
   for (Node* out : edge->outputs_) {
     HashedStrView path = out->globalPath().h;
     LogEntry* log_entry;
diff --git a/src/build_test.cc b/src/build_test.cc
index c403f9f..17ec834 100644
--- a/src/build_test.cc
+++ b/src/build_test.cc
@@ -624,7 +624,9 @@
   assert(active_edges_.size() < max_active_edges_);
   assert(find(active_edges_.begin(), active_edges_.end(), edge)
          == active_edges_.end());
-  commands_ran_.push_back(edge->EvaluateCommand());
+  EdgeCommand cmd;
+  edge->EvaluateCommand(&cmd);
+  commands_ran_.push_back(cmd.command);
   if (edge->rule().name() == "cat"  ||
       edge->rule().name() == "cat_rsp" ||
       edge->rule().name() == "cat_rsp_out" ||
@@ -1207,7 +1209,9 @@
   ASSERT_EQ(3u, edge->inputs_.size());
 
   // Expect the command line we generate to only use the original input.
-  ASSERT_EQ("cc foo.c", edge->EvaluateCommand());
+  EdgeCommand cmd;
+  edge->EvaluateCommand(&cmd);
+  ASSERT_EQ("cc foo.c", cmd.command);
 }
 
 TEST_F(BuildTest, DepFileOKWithPhonyOutputs) {
@@ -1286,7 +1290,9 @@
   EXPECT_EQ("otherfile", edge->inputs_[3]->path());
 
   // Expect the command line we generate to only use the original input.
-  ASSERT_EQ("cc foo.c", edge->EvaluateCommand());
+  EdgeCommand cmd;
+  edge->EvaluateCommand(&cmd);
+  ASSERT_EQ("cc foo.c", cmd.command);
 
   // explicit dep dirty, expect a rebuild.
   EXPECT_TRUE(builder_.Build(&err));
@@ -1406,7 +1412,9 @@
 
   // Expect the command line we generate to only use the original input, and
   // using the slashes from the manifest.
-  ASSERT_EQ("cc x\\y/z\\foo.c", edge->EvaluateCommand());
+  EdgeCommand cmd;
+  edge->EvaluateCommand(&cmd);
+  ASSERT_EQ("cc x\\y/z\\foo.c", cmd.command);
 }
 #endif
 
@@ -2491,7 +2499,9 @@
     ASSERT_EQ(3u, edge->inputs_.size());
 
     // Expect the command line we generate to only use the original input.
-    ASSERT_EQ("cc foo.c", edge->EvaluateCommand());
+    EdgeCommand cmd;
+    edge->EvaluateCommand(&cmd);
+    ASSERT_EQ("cc foo.c", cmd.command);
 
     deps_log.Close();
     builder.command_runner_.release();
@@ -2556,7 +2566,9 @@
 
     // Expect the command line we generate to only use the original input.
     // Note, slashes from manifest, not .d.
-    ASSERT_EQ("cc x\\y/z\\foo.c", edge->EvaluateCommand());
+    EdgeCommand cmd;
+    edge->EvaluateCommand(&cmd);
+    ASSERT_EQ("cc x\\y/z\\foo.c", cmd.command);
 
     deps_log.Close();
     builder.command_runner_.release();
diff --git a/src/eval_env.cc b/src/eval_env.cc
index 924da07..507fb6d 100644
--- a/src/eval_env.cc
+++ b/src/eval_env.cc
@@ -244,3 +244,14 @@
 void Scope::AddAllBuiltinRules() {
   AddRule(&State::kPhonyRule);
 }
+
+Scope::~Scope() {
+  if (cmdEnviron_) {
+    for (char** p = cmdEnviron_; *p; p++) {
+      delete[] *p;
+      *p = NULL;
+    }
+    delete[] cmdEnviron_;
+    cmdEnviron_ = NULL;
+  }
+}
diff --git a/src/eval_env.h b/src/eval_env.h
index db7edd6..d1f486d 100644
--- a/src/eval_env.h
+++ b/src/eval_env.h
@@ -151,14 +151,20 @@
   // Simple constructor for a new Scope.
   Scope(ScopePosition parent)
       : parent_(parent)
+      , cmdEnviron_(NULL)
       , chdirParent_(nullptr)
       , chdir_(parent.scope ? parent.scope->chdir_ : "") {}
 
   // Constructor which creates a new Scope that:
   //  - Does not search a parent ScopePosition when evaluating variables.
   //  - Does know the parent Scope for traversing the directory tree.
-  Scope(Scope* chdirParent, std::string chdir)
-      : parent_(nullptr), chdirParent_(chdirParent), chdir_(chdir) {}
+  Scope(Scope* chdirParent, std::string chdir, char** cmdEnviron)
+      : parent_(nullptr)
+      , cmdEnviron_(cmdEnviron)
+      , chdirParent_(chdirParent)
+      , chdir_(chdir) {}
+
+  ~Scope();
 
   /// Preallocate space in the hash tables so adding bindings and rules is more
   /// efficient.
@@ -238,11 +244,15 @@
   // Used for testing.
   Scope* parent() const { return parent_.scope; }
 
+  char** getCmdEnviron() const { return cmdEnviron_; }
+
 private:
   /// The position of this scope within its parent scope.
   /// (ScopePosition::parent will be nullptr for the root scope.)
   ScopePosition parent_;
 
+  char** cmdEnviron_ = NULL;
+
   // A Scope with chdir_ set to non-empty must not allow variable evaluation
   // to continue beyond its scope, so parent_ (above) is set to nullptr.
   //
diff --git a/src/graph.cc b/src/graph.cc
index 414d170..b1ed28b 100644
--- a/src/graph.cc
+++ b/src/graph.cc
@@ -659,12 +659,12 @@
   return true;
 }
 
-std::string Edge::EvaluateCommand(bool incl_rsp_file) {
-  std::string command;
+void Edge::EvaluateCommand(EdgeCommand* out, bool incl_rsp_file) {
   std::string err;
-  if (!EvaluateCommand(&command, incl_rsp_file, &err))
+  if (!EvaluateCommand(&out->command, incl_rsp_file, &err))
     Fatal("%s", err.c_str());
-  return command;
+  out->use_console = use_console();
+  out->env = cmdEnviron;
 }
 
 static const HashedStrView kRestat      { "restat" };
diff --git a/src/graph.h b/src/graph.h
index adf25cb..001b68d 100644
--- a/src/graph.h
+++ b/src/graph.h
@@ -297,6 +297,12 @@
 // Find all dependency paths between an input and output node.
 std::vector<DepPath> GetDependencyPaths(Node* in, Node* out);
 
+struct EdgeCommand {
+  std::string command;
+  bool use_console = false;
+  char** env = NULL;
+};
+
 struct EdgeEval {
   enum EvalPhase { kParseTime, kFinalScope };
   enum EscapeKind { kShellEscape, kDoNotEscape };
@@ -362,7 +368,7 @@
   /// Convenience method. This method must not be called from a worker thread,
   /// because it could abort with a fatal error. (For consistency with other
   /// Get*/Evaluate* methods, a better name might be GetCommand.)
-  std::string EvaluateCommand(bool incl_rsp_file = false);
+  void EvaluateCommand(EdgeCommand* out, bool incl_rsp_file = false);
 
   /// Attempts to evaluate info needed for scanning dependencies.
   bool PrecomputeDepScanInfo(std::string* err);
@@ -388,11 +394,26 @@
                         EdgeEval::EscapeKind escape=EdgeEval::kShellEscape);
 
 private:
+  char** cmdEnviron = NULL;
   std::string GetBindingImpl(const HashedStrView& key,
                              EdgeEval::EvalPhase phase,
                              EdgeEval::EscapeKind escape);
 
 public:
+  /// onPosResolvedToScope updates cmdEnviron from pos_.scope(). Then
+  /// Edge::EvaluateCommand has the cmdEnviron as a precomputed value.
+  void onPosResolvedToScope(Scope* scope) {
+    // This is NULL if scope is the root scope. However, comparing a Scope* to
+    // State::root_scope_ gets hairy (since the global State object is not
+    // yet declared). Instead, always call getCmdEnviron() and it will return
+    // NULL.
+    // (for posix, NULL is checked in Subprocess and environ is given instead.)
+    // (for win32, NULL is passed straight to CreateProcessA which then passes
+    // the environ to the command per its defined behavior.)
+
+    cmdEnviron = scope->getCmdEnviron();
+  }
+
   /// Convenience method for EvaluateVariable. On failure, it issues a fatal
   /// error. This function must not be called from a worker thread because:
   ///  - Error reporting should be deterministic, and
diff --git a/src/graph_test.cc b/src/graph_test.cc
index 47f4441..361123c 100644
--- a/src/graph_test.cc
+++ b/src/graph_test.cc
@@ -218,12 +218,14 @@
 "build a$ b: cat no'space with$ space$$ no\"space2\n"));
 
   Edge* edge = GetNode("a b")->in_edge();
+  EdgeCommand cmd;
+  edge->EvaluateCommand(&cmd);
 #if _WIN32
   EXPECT_EQ("cat no'space \"with space$\" \"no\\\"space2\" > \"a b\"",
-      edge->EvaluateCommand());
+      cmd.command);
 #else
   EXPECT_EQ("cat 'no'\\''space' 'with space$' 'no\"space2' > 'a b'",
-      edge->EvaluateCommand());
+      cmd.command);
 #endif
 }
 
@@ -278,7 +280,9 @@
 "  command = depfile is $depfile\n"
 "build out: r in\n"));
   Edge* edge = GetNode("out")->in_edge();
-  EXPECT_EQ("depfile is x", edge->EvaluateCommand());
+  EdgeCommand cmd;
+  edge->EvaluateCommand(&cmd);
+  EXPECT_EQ("depfile is x", cmd.command);
 }
 
 // Check that build statements can override rule builtins like depfile.
diff --git a/src/manifest_chunk_parser.cc b/src/manifest_chunk_parser.cc
index 2d0283e..5d0b928 100644
--- a/src/manifest_chunk_parser.cc
+++ b/src/manifest_chunk_parser.cc
@@ -28,6 +28,7 @@
 
 class ChunkParser {
   const LoadedFile& file_;
+  const bool experimentalEnvvar_;
   Lexer lexer_;
   const char* chunk_end_ = nullptr;
   std::vector<ParserItem>* out_ = nullptr;
@@ -64,8 +65,10 @@
 public:
   ChunkParser(const LoadedFile& file,
               StringPiece chunk_content,
-              std::vector<ParserItem>* out)
+              std::vector<ParserItem>* out,
+              bool experimentalEnvvar)
       : file_(file),
+        experimentalEnvvar_(experimentalEnvvar),
         lexer_(file.filename(), file.content(), chunk_content.data()),
         chunk_end_(chunk_content.data() + chunk_content.size()),
         out_(out) {}
@@ -115,6 +118,7 @@
   // 'ninja: build.ninja:NNN: unexpected indent'. This might be a slightly more
   // helpful message.
   if (lexer_.PeekToken(Lexer::INDENT)) {
+    (void)experimentalEnvvar_;  // TODO: add envvar here.
     if (!new_scope)
       return LexerError("indent after 'include' line is invalid.");
     if (!lexer_.PeekToken(Lexer::CHDIR))
@@ -379,6 +383,8 @@
 
   Clump* clump = MakeClump();
   edge->pos_ = clump->AllocNextPos();
+  // Can't edge->onPosResolvedToScope(clump->pos_.scope.scope) right here, since
+  // clump->pos_.scope.scope is not set until DfsParser::HandleClump().
   clump->edges_.push_back(edge);
   clump->edge_output_count_ += edge->explicit_outs_;
   return true;
@@ -417,8 +423,8 @@
 }
 
 void ParseChunk(const LoadedFile& file, StringPiece chunk_content,
-                std::vector<ParserItem>* out) {
-  ChunkParser parser(file, chunk_content, out);
+                std::vector<ParserItem>* out, bool experimentalEnvvar) {
+  ChunkParser parser(file, chunk_content, out, experimentalEnvvar);
   if (!parser.ParseChunk()) {
     assert(!out->empty());
     assert(out->back().kind == ParserItem::kError);
diff --git a/src/manifest_chunk_parser.h b/src/manifest_chunk_parser.h
index 5da677d..ebb970c 100644
--- a/src/manifest_chunk_parser.h
+++ b/src/manifest_chunk_parser.h
@@ -18,6 +18,7 @@
 #include <stdlib.h>
 
 #include <string>
+#include <unordered_map>
 #include <vector>
 
 #include "disk_interface.h"
@@ -44,6 +45,8 @@
   std::string chdir_plus_slash_;
   bool new_scope_ = false;
   size_t diag_pos_ = 0;
+
+  std::unordered_map<std::string, std::string> envvar;
 };
 
 struct DefaultTarget {
@@ -105,7 +108,7 @@
 /// Parse a chunk of a manifest. If the parser encounters a syntactic error, the
 /// final item will be an error object.
 void ParseChunk(const LoadedFile& file, StringPiece chunk_content,
-                std::vector<ParserItem>* out);
+                std::vector<ParserItem>* out, bool experimentalEnvvar);
 
 /// Split the input into chunks of declarations.
 std::vector<StringPiece> SplitManifestIntoChunks(StringPiece input);
diff --git a/src/manifest_parser.cc b/src/manifest_parser.cc
index 225e0a2..1c5e7de 100644
--- a/src/manifest_parser.cc
+++ b/src/manifest_parser.cc
@@ -47,7 +47,8 @@
 /// Split a single manifest file into chunks, parse the chunks in parallel, and
 /// return the resulting parser output.
 static std::vector<ParserItem> ParseManifestChunks(const LoadedFile& file,
-                                                   ThreadPool* thread_pool) {
+                                                   ThreadPool* thread_pool,
+                                                   bool experimentalEnvvar) {
   std::vector<ParserItem> result;
   const std::vector<StringPiece> chunk_views =
     manifest_chunk::SplitManifestIntoChunks(file.content_with_nul());
@@ -55,9 +56,9 @@
   METRIC_RECORD(".ninja load : parse chunks");
 
   for (std::vector<ParserItem>& chunk_items :
-      ParallelMap(thread_pool, chunk_views, [&file](StringPiece view) {
+      ParallelMap(thread_pool, chunk_views, [&file, experimentalEnvvar](StringPiece view) {
     std::vector<ParserItem> chunk_items;
-    manifest_chunk::ParseChunk(file, view, &chunk_items);
+    manifest_chunk::ParseChunk(file, view, &chunk_items, experimentalEnvvar);
     return chunk_items;
   })) {
     std::move(chunk_items.begin(), chunk_items.end(),
@@ -130,8 +131,8 @@
 }
 
 struct DfsParser {
-  DfsParser(ManifestFileSet* file_set, State* state, ThreadPool* thread_pool)
-      : file_set_(file_set), state_(state), thread_pool_(thread_pool) {}
+  DfsParser(ManifestFileSet* file_set, State* state, ThreadPool* thread_pool, const ManifestParserOptions& options)
+      : options_(options), file_set_(file_set), state_(state), thread_pool_(thread_pool) {}
 
 private:
   void HandleRequiredVersion(const RequiredVersion& item, Scope* scope);
@@ -146,6 +147,7 @@
                    std::string* err);
 
 public:
+  const ManifestParserOptions options_;
   /// Load the tree of manifest files and do initial parsing of all the chunks.
   /// This function always runs on the main thread.
   bool LoadManifestTree(const LoadedFile& file, Scope* scope,
@@ -183,7 +185,7 @@
     include.chdir_.str_ = StringPiece(include.chdir_plus_slash_);
 
     // Create scope so that subninja chdir variable lookups cannot see parent_
-    *child_scope = new Scope(scope, include.chdir_plus_slash_);
+    *child_scope = new Scope(scope, include.chdir_plus_slash_, NULL /*cmdEnviron*/);
     (*child_scope)->AddAllBuiltinRules();
   } else if (include.new_scope_) {
     *child_scope = new Scope(scope->GetCurrentEndOfScope());
@@ -277,6 +279,11 @@
       }
     }
   }
+  {
+    for (Edge* edge : clump->edges_) {
+      edge->onPosResolvedToScope(clump->pos_.scope.scope);
+    }
+  }
   for (Pool* pool : clump->pools_) {
     if (!HandlePool(pool, file, err)) {
       return false;
@@ -288,7 +295,7 @@
 bool DfsParser::LoadManifestTree(const LoadedFile& file, Scope* scope,
                                  std::vector<Clump*>* out_clumps,
                                  std::string* err) {
-  std::vector<ParserItem> items = ParseManifestChunks(file, thread_pool_);
+  std::vector<ParserItem> items = ParseManifestChunks(file, thread_pool_, options_.experimentalEnvvar);
   ReserveSpaceInScopeTables(scope, items);
 
   // With the chunks parsed, do a depth-first parse of the ninja manifest using
@@ -703,7 +710,7 @@
 
 bool ManifestLoader::Load(ManifestFileSet* file_set,
                           const LoadedFile& root_manifest, std::string* err) {
-  DfsParser dfs_parser(file_set, state_, thread_pool_);
+  DfsParser dfs_parser(file_set, state_, thread_pool_, options_);
   std::vector<Clump*> clumps;
   if (!dfs_parser.LoadManifestTree(root_manifest, &state_->root_scope_, &clumps,
                                    err)) {
diff --git a/src/manifest_parser.h b/src/manifest_parser.h
index 3f1895d..d9f31ae 100644
--- a/src/manifest_parser.h
+++ b/src/manifest_parser.h
@@ -33,6 +33,7 @@
 struct ManifestParserOptions {
   DupeEdgeAction dupe_edge_action_ = kDupeEdgeActionWarn;
   PhonyCycleAction phony_cycle_action_ = kPhonyCycleActionWarn;
+  bool experimentalEnvvar = false;
 };
 
 struct ManifestParser {
diff --git a/src/manifest_parser_perftest.cc b/src/manifest_parser_perftest.cc
index 67d11f9..40ba276 100644
--- a/src/manifest_parser_perftest.cc
+++ b/src/manifest_parser_perftest.cc
@@ -65,9 +65,13 @@
   // commands required for the requested targets. So include command
   // evaluation in the perftest by default.
   int optimization_guard = 0;
-  if (measure_command_evaluation)
-    for (size_t i = 0; i < state.edges_.size(); ++i)
-      optimization_guard += state.edges_[i]->EvaluateCommand().size();
+  if (measure_command_evaluation) {
+    for (size_t i = 0; i < state.edges_.size(); ++i) {
+      EdgeCommand c;
+      state.edges_[i]->EvaluateCommand(&c);
+      optimization_guard += c.command.size();
+    }
+  }
   return optimization_guard;
 }
 
diff --git a/src/manifest_parser_test.cc b/src/manifest_parser_test.cc
index 308cc11..a82ca49 100644
--- a/src/manifest_parser_test.cc
+++ b/src/manifest_parser_test.cc
@@ -152,7 +152,9 @@
   EXPECT_EQ("cat $in_newline > $out\n", *rule->GetBinding("command"));
 
   Edge* edge = state.edges_[0];
-  EXPECT_EQ("cat in\nin2 > out", edge->EvaluateCommand());
+  EdgeCommand cmd;
+  edge->EvaluateCommand(&cmd);
+  EXPECT_EQ("cat in\nin2 > out", cmd.command);
 }
 
 TEST_F(ParserTest, Variables) {
@@ -170,14 +172,16 @@
 "  extra = $nested2/3\n"));
 
   ASSERT_EQ(2u, state.edges_.size());
-  Edge* edge = state.edges_[0];
+  EdgeCommand cmd;
+  state.edges_[0]->EvaluateCommand(&cmd);
   EXPECT_EQ("ld one-letter-test -pthread -under -o a b c",
-            edge->EvaluateCommand());
+            cmd.command);
   EXPECT_EQ("1/2", state.root_scope_.LookupVariable("nested2"));
 
-  edge = state.edges_[1];
+  cmd.command.clear();
+  state.edges_[1]->EvaluateCommand(&cmd);
   EXPECT_EQ("ld one-letter-test 1/2/3 -under -o supernested x",
-            edge->EvaluateCommand());
+            cmd.command);
 }
 
 TEST_F(ParserTest, VariableScope) {
@@ -193,8 +197,12 @@
 ));
 
   ASSERT_EQ(2u, state.edges_.size());
-  EXPECT_EQ("cmd baz a inner", state.edges_[0]->EvaluateCommand());
-  EXPECT_EQ("cmd bar b outer", state.edges_[1]->EvaluateCommand());
+  EdgeCommand cmd;
+  state.edges_[0]->EvaluateCommand(&cmd);
+  EXPECT_EQ("cmd baz a inner", cmd.command);
+  cmd.command.clear();
+  state.edges_[1]->EvaluateCommand(&cmd);
+  EXPECT_EQ("cmd bar b outer", cmd.command);
 }
 
 TEST_F(ParserTest, Continuation) {
@@ -239,10 +247,12 @@
 "build $x: foo y\n"
 ));
   EXPECT_EQ("$dollar", state.root_scope_.LookupVariable("x"));
+  EdgeCommand cmd;
+  state.edges_[0]->EvaluateCommand(&cmd);
 #ifdef _WIN32
-  EXPECT_EQ("$dollarbar$baz$blah", state.edges_[0]->EvaluateCommand());
+  EXPECT_EQ("$dollarbar$baz$blah", cmd.command);
 #else
-  EXPECT_EQ("'$dollar'bar$baz$blah", state.edges_[0]->EvaluateCommand());
+  EXPECT_EQ("'$dollar'bar$baz$blah", cmd.command);
 #endif
 }
 
@@ -256,7 +266,9 @@
   EXPECT_EQ(state.edges_[0]->outputs_[0]->path(), "foo bar");
   EXPECT_EQ(state.edges_[0]->inputs_[0]->path(), "$one");
   EXPECT_EQ(state.edges_[0]->inputs_[1]->path(), "two$ three");
-  EXPECT_EQ(state.edges_[0]->EvaluateCommand(), "something");
+  EdgeCommand cmd;
+  state.edges_[0]->EvaluateCommand(&cmd);
+  EXPECT_EQ(cmd.command, "something");
 }
 
 TEST_F(ParserTest, CanonicalizeFile) {
@@ -951,9 +963,15 @@
   EXPECT_TRUE(inner);
 
   ASSERT_EQ(3u, state.edges_.size());
-  EXPECT_EQ("varref outer", state.edges_[0]->EvaluateCommand());
-  EXPECT_EQ("varref inner", state.edges_[1]->EvaluateCommand());
-  EXPECT_EQ("varref outer", state.edges_[2]->EvaluateCommand());
+  EdgeCommand cmd;
+  state.edges_[0]->EvaluateCommand(&cmd);
+  EXPECT_EQ("varref outer", cmd.command);
+  cmd.command.clear();
+  state.edges_[1]->EvaluateCommand(&cmd);
+  EXPECT_EQ("varref inner", cmd.command);
+  cmd.command.clear();
+  state.edges_[2]->EvaluateCommand(&cmd);
+  EXPECT_EQ("varref outer", cmd.command);
 
   // Verify inner scope links back to parent.
   ASSERT_TRUE(state.edges_[1]->pos_.scope()->parent());
@@ -1005,11 +1023,13 @@
   // Verify command includes correct chdir for subninja chdir.
   Edge* edge = inner2node->in_edge();
   EXPECT_TRUE(edge);
+  EdgeCommand cmd;
+  edge->EvaluateCommand(&cmd);
 #ifdef _WIN32
   EXPECT_EQ(NINJA_WIN32_CD_DELIM "b/" NINJA_WIN32_CD_DELIM
-            "foo \"inner\" \"def\" \"inner2\"", edge->EvaluateCommand());
+            "foo \"inner\" \"def\" \"inner2\"", cmd.command);
 #else /* _WIN32 */
-  EXPECT_EQ("cd \"b/\" && foo \"inner\" \"def\" \"inner2\"", edge->EvaluateCommand());
+  EXPECT_EQ("cd \"b/\" && foo \"inner\" \"def\" \"inner2\"", cmd.command);
 #endif /* _WIN32 */
 
   // Verify chdir scope does *not* allow variables from a parent scope to
@@ -1085,20 +1105,24 @@
 
   // Verify edge command includes correct 'cd test-a'
   Edge* edge = GetNode("test-a/final")->in_edge();
+  EdgeCommand cmd;
+  edge->EvaluateCommand(&cmd);
 #if _WIN32
   EXPECT_EQ(NINJA_WIN32_CD_DELIM "test-a/" NINJA_WIN32_CD_DELIM
-            "cat out1 > final", edge->EvaluateCommand());
+            "cat out1 > final", cmd.command);
 #else
-  EXPECT_EQ("cd \"test-a/\" && cat out1 > final", edge->EvaluateCommand());
+  EXPECT_EQ("cd \"test-a/\" && cat out1 > final", cmd.command);
 #endif
 
   // Verify edge command includes correct 'cd test-b'
+  cmd.command.clear();
   edge = GetNode("test-b/final")->in_edge();
+  edge->EvaluateCommand(&cmd);
 #if _WIN32
   EXPECT_EQ(NINJA_WIN32_CD_DELIM "test-b/" NINJA_WIN32_CD_DELIM
-            "cat out3 > final", edge->EvaluateCommand());
+            "cat out3 > final", cmd.command);
 #else
-  EXPECT_EQ("cd \"test-b/\" && cat out3 > final", edge->EvaluateCommand());
+  EXPECT_EQ("cd \"test-b/\" && cat out3 > final", cmd.command);
 #endif
 }
 
diff --git a/src/ninja.cc b/src/ninja.cc
index 53771f5..00372eb 100644
--- a/src/ninja.cc
+++ b/src/ninja.cc
@@ -91,6 +91,9 @@
 
   /// Whether to remain persistent.
   bool persistent;
+
+  /// Experimental, buggy envvar option.
+  bool experimentalEnvvar;
 };
 
 /// The Ninja main() loads up a series of data structures; various tools need
@@ -848,8 +851,11 @@
       PrintCommands((*in)->in_edge(), seen, mode);
   }
 
-  if (!edge->is_phony())
-    puts(edge->EvaluateCommand().c_str());
+  if (!edge->is_phony()) {
+    EdgeCommand c;
+    edge->EvaluateCommand(&c);
+    puts(c.command.c_str());
+  }
 }
 
 int NinjaMain::ToolCommands(const Options* options, int argc, char* argv[]) {
@@ -956,18 +962,18 @@
   ECM_NORMAL,
   ECM_EXPAND_RSPFILE
 };
-string EvaluateCommandWithRspfile(Edge* edge, EvaluateCommandMode mode) {
-  string command = edge->EvaluateCommand();
+void EvaluateCommandWithRspfile(EdgeCommand* out, Edge* edge, EvaluateCommandMode mode) {
+  edge->EvaluateCommand(out);
   if (mode == ECM_NORMAL)
-    return command;
+    return;
 
   string rspfile = edge->GetUnescapedRspfile();
   if (rspfile.empty())
-    return command;
+    return;
 
-  size_t index = command.find(rspfile);
-  if (index == 0 || index == string::npos || command[index - 1] != '@')
-    return command;
+  size_t index = out->command.find(rspfile);
+  if (index == 0 || index == string::npos || out->command[index - 1] != '@')
+    return;
 
   string rspfile_content = edge->GetBinding("rspfile_content");
   size_t newline_index = 0;
@@ -976,8 +982,7 @@
     rspfile_content.replace(newline_index, 1, 1, ' ');
     ++newline_index;
   }
-  command.replace(index - 1, rspfile.length() + 1, rspfile_content);
-  return command;
+  out->command.replace(index - 1, rspfile.length() + 1, rspfile_content);
 }
 
 int NinjaMain::ToolCompilationDatabase(const Options* options, int argc,
@@ -1036,7 +1041,9 @@
         printf("\n  {\n    \"directory\": \"");
         EncodeJSONString(&cwd[0]);
         printf("\",\n    \"command\": \"");
-        EncodeJSONString(EvaluateCommandWithRspfile(*e, eval_mode).c_str());
+        EdgeCommand cmd;
+        EvaluateCommandWithRspfile(&cmd, *e, eval_mode);
+        EncodeJSONString(cmd.command.c_str());
         printf("\",\n    \"file\": \"");
         EncodeJSONString((*e)->inputs_[0]->path().c_str());
         printf("\",\n    \"output\": \"");
@@ -1498,6 +1505,7 @@
     OPT_FRONTEND = 2,
     OPT_FRONTEND_FILE = 3,
     OPT_QUIET = 4,
+    OPT_EXPERIMENTALENVVAR = 5,
   };
   const option kLongOptions[] = {
 #ifndef _WIN32
@@ -1508,6 +1516,7 @@
     { "version", no_argument, NULL, OPT_VERSION },
     { "verbose", no_argument, NULL, 'v' },
     { "quiet", no_argument, NULL, OPT_QUIET },
+    { "experimentalEnvvar", no_argument, NULL, OPT_EXPERIMENTALENVVAR },
     { NULL, 0, NULL, 0 }
   };
 
@@ -1591,6 +1600,9 @@
       case OPT_FRONTEND_FILE:
         config->frontend_file = optarg;
         break;
+      case OPT_EXPERIMENTALENVVAR:
+        options->experimentalEnvvar = true;
+        break;
       case 'h':
       default:
         Usage(*config);
@@ -1690,6 +1702,7 @@
     if (options.phony_cycle_should_err) {
       parser_opts.phony_cycle_action_ = kPhonyCycleActionError;
     }
+    parser_opts.experimentalEnvvar = options.experimentalEnvvar;
     ManifestParser parser(&ninja.state_, &ninja.disk_interface_, parser_opts);
     string err;
     if (!parser.Load(options.input_file, &err)) {
diff --git a/src/state.cc b/src/state.cc
index df7966f..2ae3dde 100644
--- a/src/state.cc
+++ b/src/state.cc
@@ -85,6 +85,7 @@
 Edge* State::AddEdge(const Rule* rule) {
   Edge* edge = new Edge();
   edge->pos_.base = new BasePosition {{ &root_scope_, 0 }}; // leaked
+  edge->onPosResolvedToScope(&root_scope_);
   edge->rule_ = rule;
   edge->pool_ = &State::kDefaultPool;
   edge->id_ = edges_.size();
diff --git a/src/state_test.cc b/src/state_test.cc
index 898fdb1..170a7be 100644
--- a/src/state_test.cc
+++ b/src/state_test.cc
@@ -30,7 +30,9 @@
   state.AddIn(edge, state.root_scope_.GlobalPath("in2"), 0);
   state.AddOut(edge, state.root_scope_.GlobalPath("out"), 0);
 
-  EXPECT_EQ("cat in1 in2 > out", edge->EvaluateCommand());
+  EdgeCommand cmd;
+  edge->EvaluateCommand(&cmd);
+  EXPECT_EQ("cat in1 in2 > out", cmd.command);
 
   EXPECT_FALSE(state.GetNode(state.root_scope_.GlobalPath("in1"), 0)->dirty());
   EXPECT_FALSE(state.GetNode(state.root_scope_.GlobalPath("in2"), 0)->dirty());
diff --git a/src/status.cc b/src/status.cc
index 03e0642..4f8a5eb 100644
--- a/src/status.cc
+++ b/src/status.cc
@@ -75,7 +75,9 @@
       outputs += (*o)->globalPath().h.str_view().AsString() + " ";
 
     printer_.PrintOnNewLine("FAILED: " + outputs + "\n");
-    printer_.PrintOnNewLine(edge->EvaluateCommand() + "\n");
+    EdgeCommand c;
+    edge->EvaluateCommand(&c);
+    printer_.PrintOnNewLine(c.command + "\n");
   }
 
   if (!result->output.empty()) {
@@ -278,8 +280,10 @@
     f_ = fdopen(output_pipe[1], "wb");
 
     // Launch the frontend as a subprocess with write-end of the pipe as fd 3
-    subprocess_ = subprocess_set_.Add(config.frontend, /*use_console=*/true,
-                                      output_pipe[0]);
+    EdgeCommand c;
+    c.command = config.frontend;
+    c.use_console = true;
+    subprocess_ = subprocess_set_.Add(c, output_pipe[0]);
     close(output_pipe[0]);
   } else if (config.frontend_file != NULL) {
     f_ = fopen(config.frontend_file, "wb");
diff --git a/src/subprocess-posix.cc b/src/subprocess-posix.cc
index d7c1a13..72a169c 100644
--- a/src/subprocess-posix.cc
+++ b/src/subprocess-posix.cc
@@ -41,7 +41,7 @@
     Finish();
 }
 
-bool Subprocess::Start(SubprocessSet* set, const string& command,
+bool Subprocess::Start(SubprocessSet* set, const EdgeCommand& cmd,
                        int extra_fd) {
   int output_pipe[2];
   if (pipe(output_pipe) < 0)
@@ -116,9 +116,9 @@
   if (err != 0)
     Fatal("posix_spawnattr_setflags: %s", strerror(err));
 
-  const char* spawned_args[] = { "/bin/sh", "-c", command.c_str(), NULL };
+  const char* spawned_args[] = { "/bin/sh", "-c", cmd.command.c_str(), NULL };
   err = posix_spawn(&pid_, "/bin/sh", &action, &attr,
-        const_cast<char**>(spawned_args), environ);
+        const_cast<char**>(spawned_args), cmd.env ? cmd.env : environ);
   if (err != 0)
     Fatal("posix_spawn: %s", strerror(err));
 
@@ -231,10 +231,9 @@
     Fatal("sigprocmask: %s", strerror(errno));
 }
 
-Subprocess *SubprocessSet::Add(const string& command, bool use_console,
-                               int extra_fd) {
-  Subprocess *subprocess = new Subprocess(use_console);
-  if (!subprocess->Start(this, command, extra_fd)) {
+Subprocess *SubprocessSet::Add(const EdgeCommand& cmd, int extra_fd) {
+  Subprocess *subprocess = new Subprocess(cmd.use_console);
+  if (!subprocess->Start(this, cmd, extra_fd)) {
     delete subprocess;
     return 0;
   }
diff --git a/src/subprocess-win32.cc b/src/subprocess-win32.cc
index f83643f..4673334 100644
--- a/src/subprocess-win32.cc
+++ b/src/subprocess-win32.cc
@@ -73,7 +73,7 @@
   return output_write_child;
 }
 
-bool Subprocess::Start(SubprocessSet* set, const string& command) {
+bool Subprocess::Start(SubprocessSet* set, const EdgeCommand& cmd) {
   // chdir is passed to CreateProcessA. If it is NULL, no chdir is needed.
   //
   // BUG: CreateProcessA does the search for argv[0] before changing to chdir
@@ -104,7 +104,7 @@
       cleanedCommand = command.substr(endCD + delimSize);
     }
   } else {
-    cleanedCommand = command;
+    cleanedCommand = cmd.command;
   }
 
   HANDLE child_pipe = SetupPipe(set->ioport_);
@@ -143,7 +143,7 @@
   // lines greater than 8,191 chars.
   if (!CreateProcessA(NULL, (char*)cleanedCommand.c_str(), NULL, NULL,
                       /* inherit handles */ TRUE, process_flags,
-                      NULL, chdir,
+                      cmd.env, chdir,
                       &startup_info, &process_info)) {
     DWORD error = GetLastError();
     if (chdir) {
@@ -268,9 +268,9 @@
   return FALSE;
 }
 
-Subprocess *SubprocessSet::Add(const string& command, bool use_console) {
-  Subprocess *subprocess = new Subprocess(use_console);
-  if (!subprocess->Start(this, command)) {
+Subprocess *SubprocessSet::Add(const EdgeCommand& cmd) {
+  Subprocess *subprocess = new Subprocess(cmd.use_console);
+  if (!subprocess->Start(this, cmd)) {
     delete subprocess;
     return 0;
   }
diff --git a/src/subprocess.h b/src/subprocess.h
index db8a030..6e13207 100644
--- a/src/subprocess.h
+++ b/src/subprocess.h
@@ -35,6 +35,7 @@
 #  endif
 #endif
 
+#include "graph.h"
 #include "exit_status.h"
 
 /// Subprocess wraps a single async subprocess.  It is entirely
@@ -58,7 +59,7 @@
 
  private:
   Subprocess(bool use_console);
-  bool Start(struct SubprocessSet* set, const string& command, int extra_fd);
+  bool Start(struct SubprocessSet* set, const EdgeCommand& cmd, int extra_fd);
   void OnPipeReady();
 
   string buf_;
@@ -90,8 +91,7 @@
   SubprocessSet();
   ~SubprocessSet();
 
-  Subprocess* Add(const string& command, bool use_console = false,
-                  int extra_fd = -1);
+  Subprocess* Add(const EdgeCommand& cmd, int extra_fd = -1);
   bool DoWork();
   Subprocess* NextFinished();
   void Clear();
diff --git a/src/subprocess_test.cc b/src/subprocess_test.cc
index 839ec69..475504d 100644
--- a/src/subprocess_test.cc
+++ b/src/subprocess_test.cc
@@ -40,7 +40,9 @@
 
 // Run a command that fails and emits to stderr.
 TEST_F(SubprocessTest, BadCommandStderr) {
-  Subprocess* subproc = subprocs_.Add("cmd /c ninja_no_such_command");
+  EdgeCommand c;
+  c.command = "cmd /c ninja_no_such_command";
+  Subprocess* subproc = subprocs_.Add(c);
   ASSERT_NE((Subprocess *) 0, subproc);
 
   while (!subproc->Done()) {
@@ -54,7 +56,9 @@
 
 // Run a command that does not exist
 TEST_F(SubprocessTest, NoSuchCommand) {
-  Subprocess* subproc = subprocs_.Add("ninja_no_such_command");
+  EdgeCommand c;
+  c.command = "ninja_no_such_command";
+  Subprocess* subproc = subprocs_.Add(c);
   ASSERT_NE((Subprocess *) 0, subproc);
 
   while (!subproc->Done()) {
@@ -73,7 +77,9 @@
 #ifndef _WIN32
 
 TEST_F(SubprocessTest, InterruptChild) {
-  Subprocess* subproc = subprocs_.Add("kill -INT $$");
+  EdgeCommand c;
+  c.command = "kill -INT $$";
+  Subprocess* subproc = subprocs_.Add(c);
   ASSERT_NE((Subprocess *) 0, subproc);
 
   while (!subproc->Done()) {
@@ -84,7 +90,9 @@
 }
 
 TEST_F(SubprocessTest, InterruptParent) {
-  Subprocess* subproc = subprocs_.Add("kill -INT $PPID ; sleep 1");
+  EdgeCommand c;
+  c.command = "kill -INT $PPID ; sleep 1";
+  Subprocess* subproc = subprocs_.Add(c);
   ASSERT_NE((Subprocess *) 0, subproc);
 
   while (!subproc->Done()) {
@@ -97,7 +105,9 @@
 }
 
 TEST_F(SubprocessTest, InterruptChildWithSigTerm) {
-  Subprocess* subproc = subprocs_.Add("kill -TERM $$");
+  EdgeCommand c;
+  c.command = "kill -TERM $$";
+  Subprocess* subproc = subprocs_.Add(c);
   ASSERT_NE((Subprocess *) 0, subproc);
 
   while (!subproc->Done()) {
@@ -108,7 +118,9 @@
 }
 
 TEST_F(SubprocessTest, InterruptParentWithSigTerm) {
-  Subprocess* subproc = subprocs_.Add("kill -TERM $PPID ; sleep 1");
+  EdgeCommand c;
+  c.command = "kill -TERM $PPID ; sleep 1";
+  Subprocess* subproc = subprocs_.Add(c);
   ASSERT_NE((Subprocess *) 0, subproc);
 
   while (!subproc->Done()) {
@@ -121,7 +133,9 @@
 }
 
 TEST_F(SubprocessTest, InterruptChildWithSigHup) {
-  Subprocess* subproc = subprocs_.Add("kill -HUP $$");
+  EdgeCommand c;
+  c.command = "kill -HUP $$";
+  Subprocess* subproc = subprocs_.Add(c);
   ASSERT_NE((Subprocess *) 0, subproc);
 
   while (!subproc->Done()) {
@@ -132,7 +146,9 @@
 }
 
 TEST_F(SubprocessTest, InterruptParentWithSigHup) {
-  Subprocess* subproc = subprocs_.Add("kill -HUP $PPID ; sleep 1");
+  EdgeCommand c;
+  c.command = "kill -HUP $PPID ; sleep 1";
+  Subprocess* subproc = subprocs_.Add(c);
   ASSERT_NE((Subprocess *) 0, subproc);
 
   while (!subproc->Done()) {
@@ -147,8 +163,11 @@
 TEST_F(SubprocessTest, Console) {
   // Skip test if we don't have the console ourselves.
   if (isatty(0) && isatty(1) && isatty(2)) {
+    EdgeCommand c;
+    c.command = "test -t 0 -a -t 1 -a -t 2";
+    c.use_console = true;
     Subprocess* subproc =
-        subprocs_.Add("test -t 0 -a -t 1 -a -t 2", /*use_console=*/true);
+        subprocs_.Add(c);
     ASSERT_NE((Subprocess*)0, subproc);
 
     while (!subproc->Done()) {
@@ -162,7 +181,9 @@
 #endif
 
 TEST_F(SubprocessTest, SetWithSingle) {
-  Subprocess* subproc = subprocs_.Add(kSimpleCommand);
+  EdgeCommand c;
+  c.command = kSimpleCommand;
+  Subprocess* subproc = subprocs_.Add(c);
   ASSERT_NE((Subprocess *) 0, subproc);
 
   while (!subproc->Done()) {
@@ -188,7 +209,9 @@
   };
 
   for (int i = 0; i < 3; ++i) {
-    processes[i] = subprocs_.Add(kCommands[i]);
+    EdgeCommand c;
+    c.command = kCommands[i];
+    processes[i] = subprocs_.Add(c);
     ASSERT_NE((Subprocess *) 0, processes[i]);
   }
 
@@ -231,7 +254,9 @@
 
   vector<Subprocess*> procs;
   for (size_t i = 0; i < kNumProcs; ++i) {
-    Subprocess* subproc = subprocs_.Add("/bin/echo");
+    EdgeCommand c;
+    c.command = "/bin/echo";
+    Subprocess* subproc = subprocs_.Add(c);
     ASSERT_NE((Subprocess *) 0, subproc);
     procs.push_back(subproc);
   }
@@ -251,7 +276,9 @@
 // Verify that a command that attempts to read stdin correctly thinks
 // that stdin is closed.
 TEST_F(SubprocessTest, ReadStdin) {
-  Subprocess* subproc = subprocs_.Add("cat -");
+  EdgeCommand c;
+  c.command = "cat -";
+  Subprocess* subproc = subprocs_.Add(c);
   while (!subproc->Done()) {
     subprocs_.DoWork();
   }