diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index 041c6060bec534c2f34d4dff43b1c05824992341..a6babe5c5210efce96db8c4862f54625f2314fc9 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -531,6 +531,8 @@ set(SRCS
   cmFindProgramCommand.h
   cmForEachCommand.cxx
   cmForEachCommand.h
+  cmFunctionBlocker.cxx
+  cmFunctionBlocker.h
   cmFunctionCommand.cxx
   cmFunctionCommand.h
   cmGetCMakePropertyCommand.cxx
diff --git a/Source/cmForEachCommand.cxx b/Source/cmForEachCommand.cxx
index af32040970d61313a77e59f43677abc2afd8102b..53ae47916be463d4c5c0f14ce652310e15dc4d34 100644
--- a/Source/cmForEachCommand.cxx
+++ b/Source/cmForEachCommand.cxx
@@ -8,6 +8,8 @@
 #include <utility>
 
 #include "cm_memory.hxx"
+#include "cm_static_string_view.hxx"
+#include "cm_string_view.hxx"
 
 #include "cmExecutionStatus.h"
 #include "cmFunctionBlocker.h"
@@ -22,23 +24,22 @@ class cmForEachFunctionBlocker : public cmFunctionBlocker
 public:
   cmForEachFunctionBlocker(cmMakefile* mf);
   ~cmForEachFunctionBlocker() override;
-  bool IsFunctionBlocked(const cmListFileFunction& lff, cmMakefile& mf,
-                         cmExecutionStatus&) override;
+
+  cm::string_view StartCommandName() const override { return "foreach"_s; }
+  cm::string_view EndCommandName() const override { return "endforeach"_s; }
+
   bool ShouldRemove(const cmListFileFunction& lff, cmMakefile& mf) override;
   bool Replay(std::vector<cmListFileFunction> const& functions,
-              cmExecutionStatus& inStatus);
+              cmExecutionStatus& inStatus) override;
 
   std::vector<std::string> Args;
-  std::vector<cmListFileFunction> Functions;
 
 private:
   cmMakefile* Makefile;
-  int Depth;
 };
 
 cmForEachFunctionBlocker::cmForEachFunctionBlocker(cmMakefile* mf)
   : Makefile(mf)
-  , Depth(0)
 {
   this->Makefile->PushLoopBlock();
 }
@@ -48,36 +49,6 @@ cmForEachFunctionBlocker::~cmForEachFunctionBlocker()
   this->Makefile->PopLoopBlock();
 }
 
-bool cmForEachFunctionBlocker::IsFunctionBlocked(const cmListFileFunction& lff,
-                                                 cmMakefile& mf,
-                                                 cmExecutionStatus& inStatus)
-{
-  if (lff.Name.Lower == "foreach") {
-    // record the number of nested foreach commands
-    this->Depth++;
-  } else if (lff.Name.Lower == "endforeach") {
-    // if this is the endofreach for this statement
-    if (!this->Depth) {
-      // Remove the function blocker for this scope or bail.
-      std::unique_ptr<cmFunctionBlocker> fb(
-        mf.RemoveFunctionBlocker(this, lff));
-      if (!fb) {
-        return false;
-      }
-
-      return this->Replay(this->Functions, inStatus);
-    }
-    // close out a nested foreach
-    this->Depth--;
-  }
-
-  // record the command
-  this->Functions.push_back(lff);
-
-  // always return true
-  return true;
-}
-
 bool cmForEachFunctionBlocker::ShouldRemove(const cmListFileFunction& lff,
                                             cmMakefile& mf)
 {
diff --git a/Source/cmFunctionBlocker.cxx b/Source/cmFunctionBlocker.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..29996916a6fb8a537bd15125d82f5371f6189313
--- /dev/null
+++ b/Source/cmFunctionBlocker.cxx
@@ -0,0 +1,24 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmFunctionBlocker.h"
+
+#include "cmExecutionStatus.h"
+#include "cmMakefile.h"
+
+bool cmFunctionBlocker::IsFunctionBlocked(const cmListFileFunction& lff,
+                                          cmExecutionStatus& status)
+{
+  if (lff.Name.Lower == this->StartCommandName()) {
+    this->ScopeDepth++;
+  } else if (lff.Name.Lower == this->EndCommandName()) {
+    this->ScopeDepth--;
+    if (this->ScopeDepth == 0U) {
+      cmMakefile& mf = status.GetMakefile();
+      auto self = mf.RemoveFunctionBlocker(this, lff);
+      return this->Replay(this->Functions, status);
+    }
+  }
+
+  this->Functions.push_back(lff);
+  return true;
+}
diff --git a/Source/cmFunctionBlocker.h b/Source/cmFunctionBlocker.h
index cd6b05db47bef54c104370595f157ce0e27f4952..8fc2c1c66471bab5dc7bfa36b1643d84470ef200 100644
--- a/Source/cmFunctionBlocker.h
+++ b/Source/cmFunctionBlocker.h
@@ -3,6 +3,12 @@
 #ifndef cmFunctionBlocker_h
 #define cmFunctionBlocker_h
 
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <vector>
+
+#include "cm_string_view.hxx"
+
 #include "cmListFileCache.h"
 
 class cmExecutionStatus;
@@ -14,8 +20,8 @@ public:
   /**
    * should a function be blocked
    */
-  virtual bool IsFunctionBlocked(const cmListFileFunction& lff, cmMakefile& mf,
-                                 cmExecutionStatus& status) = 0;
+  bool IsFunctionBlocked(cmListFileFunction const& lff,
+                         cmExecutionStatus& status);
 
   /**
    * should this function blocker be removed, useful when one function adds a
@@ -38,8 +44,17 @@ public:
     return this->StartingContext;
   }
 
+private:
+  virtual cm::string_view StartCommandName() const = 0;
+  virtual cm::string_view EndCommandName() const = 0;
+
+  virtual bool Replay(std::vector<cmListFileFunction> const& functions,
+                      cmExecutionStatus& status) = 0;
+
 private:
   cmListFileContext StartingContext;
+  std::vector<cmListFileFunction> Functions;
+  unsigned int ScopeDepth = 1;
 };
 
 #endif
diff --git a/Source/cmFunctionCommand.cxx b/Source/cmFunctionCommand.cxx
index 51b6970180c39535fca7a5ca9f65f0a954945ff1..6a0419673aff686be4be074f686230dba7d6ce9a 100644
--- a/Source/cmFunctionCommand.cxx
+++ b/Source/cmFunctionCommand.cxx
@@ -5,6 +5,9 @@
 #include <sstream>
 #include <utility>
 
+#include "cm_static_string_view.hxx"
+#include "cm_string_view.hxx"
+
 #include "cmAlgorithms.h"
 #include "cmExecutionStatus.h"
 #include "cmFunctionBlocker.h"
@@ -107,40 +110,16 @@ bool cmFunctionHelperCommand::operator()(
 class cmFunctionFunctionBlocker : public cmFunctionBlocker
 {
 public:
-  bool IsFunctionBlocked(const cmListFileFunction&, cmMakefile& mf,
-                         cmExecutionStatus&) override;
+  cm::string_view StartCommandName() const override { return "function"_s; }
+  cm::string_view EndCommandName() const override { return "endfunction"_s; }
+
   bool ShouldRemove(const cmListFileFunction&, cmMakefile& mf) override;
   bool Replay(std::vector<cmListFileFunction> const& functions,
-              cmExecutionStatus& status);
+              cmExecutionStatus& status) override;
 
   std::vector<std::string> Args;
-  std::vector<cmListFileFunction> Functions;
-  int Depth = 0;
 };
 
-bool cmFunctionFunctionBlocker::IsFunctionBlocked(
-  const cmListFileFunction& lff, cmMakefile& mf, cmExecutionStatus& status)
-{
-  // record commands until we hit the ENDFUNCTION
-  // at the ENDFUNCTION call we shift gears and start looking for invocations
-  if (lff.Name.Lower == "function") {
-    this->Depth++;
-  } else if (lff.Name.Lower == "endfunction") {
-    // if this is the endfunction for this function then execute
-    if (!this->Depth) {
-      auto self = mf.RemoveFunctionBlocker(this, lff);
-      return this->Replay(this->Functions, status);
-    }
-    // decrement for each nested function that ends
-    this->Depth--;
-  }
-
-  // if it wasn't an endfunction and we are not executing then we must be
-  // recording
-  this->Functions.push_back(lff);
-  return true;
-}
-
 bool cmFunctionFunctionBlocker::ShouldRemove(const cmListFileFunction& lff,
                                              cmMakefile& mf)
 {
diff --git a/Source/cmIfCommand.cxx b/Source/cmIfCommand.cxx
index 95f8177a6b65831c1e6b6fd8bcdc41d449dfe664..63cdd203d030948ff0807254bed65b6aa78a73a1 100644
--- a/Source/cmIfCommand.cxx
+++ b/Source/cmIfCommand.cxx
@@ -3,6 +3,8 @@
 #include "cmIfCommand.h"
 
 #include "cm_memory.hxx"
+#include "cm_static_string_view.hxx"
+#include "cm_string_view.hxx"
 
 #include "cmConditionEvaluator.h"
 #include "cmExecutionStatus.h"
@@ -33,51 +35,19 @@ static std::string cmIfCommandError(
 class cmIfFunctionBlocker : public cmFunctionBlocker
 {
 public:
-  bool IsFunctionBlocked(const cmListFileFunction& lff, cmMakefile& mf,
-                         cmExecutionStatus&) override;
+  cm::string_view StartCommandName() const override { return "if"_s; }
+  cm::string_view EndCommandName() const override { return "endif"_s; }
+
   bool ShouldRemove(const cmListFileFunction& lff, cmMakefile& mf) override;
   bool Replay(std::vector<cmListFileFunction> const& functions,
-              cmExecutionStatus& inStatus);
+              cmExecutionStatus& inStatus) override;
 
   std::vector<cmListFileArgument> Args;
-  std::vector<cmListFileFunction> Functions;
   bool IsBlocking;
   bool HasRun = false;
   bool ElseSeen = false;
-  unsigned int ScopeDepth = 0;
 };
 
-//=========================================================================
-bool cmIfFunctionBlocker::IsFunctionBlocked(const cmListFileFunction& lff,
-                                            cmMakefile& mf,
-                                            cmExecutionStatus& inStatus)
-{
-  // we start by recording all the functions
-  if (lff.Name.Lower == "if") {
-    this->ScopeDepth++;
-  } else if (lff.Name.Lower == "endif") {
-    this->ScopeDepth--;
-    // if this is the endif for this if statement, then start executing
-    if (!this->ScopeDepth) {
-      // Remove the function blocker for this scope or bail.
-      std::unique_ptr<cmFunctionBlocker> fb(
-        mf.RemoveFunctionBlocker(this, lff));
-      if (!fb) {
-        return false;
-      }
-
-      return this->Replay(this->Functions, inStatus);
-    }
-  }
-
-  // record the command
-  this->Functions.push_back(lff);
-
-  // always return true
-  return true;
-}
-
-//=========================================================================
 bool cmIfFunctionBlocker::ShouldRemove(const cmListFileFunction& lff,
                                        cmMakefile&)
 {
@@ -235,7 +205,6 @@ bool cmIfCommand(std::vector<cmListFileArgument> const& args,
   {
     auto fb = cm::make_unique<cmIfFunctionBlocker>();
     // if is isn't true block the commands
-    fb->ScopeDepth = 1;
     fb->IsBlocking = !isTrue;
     if (isTrue) {
       fb->HasRun = true;
diff --git a/Source/cmMacroCommand.cxx b/Source/cmMacroCommand.cxx
index e82d6fa5d82b1321e2680d7298826b1232904c91..030bb664cf47ac919eb76e795d404af4ca8e766c 100644
--- a/Source/cmMacroCommand.cxx
+++ b/Source/cmMacroCommand.cxx
@@ -7,6 +7,8 @@
 #include <utility>
 
 #include "cm_memory.hxx"
+#include "cm_static_string_view.hxx"
+#include "cm_string_view.hxx"
 
 #include "cmAlgorithms.h"
 #include "cmExecutionStatus.h"
@@ -141,41 +143,16 @@ bool cmMacroHelperCommand::operator()(
 class cmMacroFunctionBlocker : public cmFunctionBlocker
 {
 public:
-  bool IsFunctionBlocked(const cmListFileFunction&, cmMakefile& mf,
-                         cmExecutionStatus&) override;
+  cm::string_view StartCommandName() const override { return "macro"_s; }
+  cm::string_view EndCommandName() const override { return "endmacro"_s; }
+
   bool ShouldRemove(const cmListFileFunction&, cmMakefile& mf) override;
   bool Replay(std::vector<cmListFileFunction> const& functions,
-              cmExecutionStatus& status);
+              cmExecutionStatus& status) override;
 
   std::vector<std::string> Args;
-  std::vector<cmListFileFunction> Functions;
-  int Depth = 0;
 };
 
-bool cmMacroFunctionBlocker::IsFunctionBlocked(const cmListFileFunction& lff,
-                                               cmMakefile& mf,
-                                               cmExecutionStatus& status)
-{
-  // record commands until we hit the ENDMACRO
-  // at the ENDMACRO call we shift gears and start looking for invocations
-  if (lff.Name.Lower == "macro") {
-    this->Depth++;
-  } else if (lff.Name.Lower == "endmacro") {
-    // if this is the endmacro for this macro then execute
-    if (!this->Depth) {
-      auto self = mf.RemoveFunctionBlocker(this, lff);
-      return this->Replay(this->Functions, status);
-    }
-    // decrement for each nested macro that ends
-    this->Depth--;
-  }
-
-  // if it wasn't an endmacro and we are not executing then we must be
-  // recording
-  this->Functions.push_back(lff);
-  return true;
-}
-
 bool cmMacroFunctionBlocker::ShouldRemove(const cmListFileFunction& lff,
                                           cmMakefile& mf)
 {
diff --git a/Source/cmMakefile.cxx b/Source/cmMakefile.cxx
index f297fdf43b0c04364fc6d7d58110ad548698f242..8c1178698de170977b8051311b14334f05ed9f48 100644
--- a/Source/cmMakefile.cxx
+++ b/Source/cmMakefile.cxx
@@ -3070,7 +3070,7 @@ bool cmMakefile::IsFunctionBlocked(const cmListFileFunction& lff,
     return false;
   }
 
-  return this->FunctionBlockers.top()->IsFunctionBlocked(lff, *this, status);
+  return this->FunctionBlockers.top()->IsFunctionBlocked(lff, status);
 }
 
 void cmMakefile::PushFunctionBlockerBarrier()
diff --git a/Source/cmWhileCommand.cxx b/Source/cmWhileCommand.cxx
index 025d14ac17e319940225504030a335858774c669..bacee58ce5db1d86e08b40796b6c06489c576f9d 100644
--- a/Source/cmWhileCommand.cxx
+++ b/Source/cmWhileCommand.cxx
@@ -3,6 +3,8 @@
 #include "cmWhileCommand.h"
 
 #include "cm_memory.hxx"
+#include "cm_static_string_view.hxx"
+#include "cm_string_view.hxx"
 
 #include "cmConditionEvaluator.h"
 #include "cmExecutionStatus.h"
@@ -21,23 +23,22 @@ class cmWhileFunctionBlocker : public cmFunctionBlocker
 public:
   cmWhileFunctionBlocker(cmMakefile* mf);
   ~cmWhileFunctionBlocker() override;
-  bool IsFunctionBlocked(const cmListFileFunction& lff, cmMakefile& mf,
-                         cmExecutionStatus&) override;
+
+  cm::string_view StartCommandName() const override { return "while"_s; }
+  cm::string_view EndCommandName() const override { return "endwhile"_s; }
+
   bool ShouldRemove(const cmListFileFunction& lff, cmMakefile& mf) override;
   bool Replay(std::vector<cmListFileFunction> const& functions,
-              cmExecutionStatus& inStatus);
+              cmExecutionStatus& inStatus) override;
 
   std::vector<cmListFileArgument> Args;
-  std::vector<cmListFileFunction> Functions;
 
 private:
   cmMakefile* Makefile;
-  int Depth;
 };
 
 cmWhileFunctionBlocker::cmWhileFunctionBlocker(cmMakefile* mf)
   : Makefile(mf)
-  , Depth(0)
 {
   this->Makefile->PushLoopBlock();
 }
@@ -47,36 +48,6 @@ cmWhileFunctionBlocker::~cmWhileFunctionBlocker()
   this->Makefile->PopLoopBlock();
 }
 
-bool cmWhileFunctionBlocker::IsFunctionBlocked(const cmListFileFunction& lff,
-                                               cmMakefile& mf,
-                                               cmExecutionStatus& inStatus)
-{
-  // at end of for each execute recorded commands
-  if (lff.Name.Lower == "while") {
-    // record the number of while commands past this one
-    this->Depth++;
-  } else if (lff.Name.Lower == "endwhile") {
-    // if this is the endwhile for this while loop then execute
-    if (!this->Depth) {
-      // Remove the function blocker for this scope or bail.
-      std::unique_ptr<cmFunctionBlocker> fb(
-        mf.RemoveFunctionBlocker(this, lff));
-      if (!fb) {
-        return false;
-      }
-      return this->Replay(this->Functions, inStatus);
-    }
-    // decrement for each nested while that ends
-    this->Depth--;
-  }
-
-  // record the command
-  this->Functions.push_back(lff);
-
-  // always return true
-  return true;
-}
-
 bool cmWhileFunctionBlocker::ShouldRemove(const cmListFileFunction& lff,
                                           cmMakefile&)
 {
diff --git a/bootstrap b/bootstrap
index 7d9631c6162d048e8d100f224053b22024bcd157..5742d536d19928352dd86c63ea44f72277d18d72 100755
--- a/bootstrap
+++ b/bootstrap
@@ -326,6 +326,7 @@ CMAKE_CXX_SOURCES="\
   cmFindPathCommand \
   cmFindProgramCommand \
   cmForEachCommand \
+  cmFunctionBlocker \
   cmFunctionCommand \
   cmFSPermissions \
   cmGeneratedFileStream \