diff --git a/llvm/docs/CommandGuide/lit.rst b/llvm/docs/CommandGuide/lit.rst
--- a/llvm/docs/CommandGuide/lit.rst
+++ b/llvm/docs/CommandGuide/lit.rst
@@ -556,6 +556,7 @@
                          Otherwise, %t but with a single leading ``/`` removed.
  %:T                     On Windows, %/T but a ``:`` is removed if its the second character.
                          Otherwise, %T but with a single leading ``/`` removed.
+ %{for-each-file <path>} Invokes RUN command for each file in ``<path>``. Accepts wildcards.
  ======================= ==============
 
 Other substitutions are provided that are variations on this base set and
diff --git a/llvm/docs/ReleaseNotes.rst b/llvm/docs/ReleaseNotes.rst
--- a/llvm/docs/ReleaseNotes.rst
+++ b/llvm/docs/ReleaseNotes.rst
@@ -260,6 +260,8 @@
 * Made significant changes to JSON output format of `llvm-readobj`/`llvm-readelf`
   to improve correctness and clarity.
 
+* llvm-lit now accepts %{for-each-file} in RUN commands.
+
 Changes to LLDB
 ---------------------------------
 
diff --git a/llvm/utils/lit/lit/TestRunner.py b/llvm/utils/lit/lit/TestRunner.py
--- a/llvm/utils/lit/lit/TestRunner.py
+++ b/llvm/utils/lit/lit/TestRunner.py
@@ -1529,9 +1529,15 @@
 
     # We use #_MARKER_# to hide %% while we do the other substitutions.
     def escapePercents(ln):
+        if isinstance(ln, list):
+            return [_caching_re_compile("%%").sub("#_MARKER_#", line)
+                    for line in ln]
         return _caching_re_compile("%%").sub("#_MARKER_#", ln)
 
     def unescapePercents(ln):
+        if isinstance(ln, list):
+            return [_caching_re_compile("#_MARKER_#").sub("%", line)
+                    for line in ln]
         return _caching_re_compile("#_MARKER_#").sub("%", ln)
 
     def substituteIfElse(ln):
@@ -1623,6 +1629,47 @@
         assert len(ln) == 0
         return result
 
+    def substituteForEachFile(ln):
+        from glob import iglob
+        from pathlib import Path
+
+        if "%{for-each-file" not in ln:
+            return ln
+
+        lines = []
+        pattern = re.compile(r'%{for-each-file ([^}]+)}')
+        match_iter = pattern.finditer(ln)
+        try:
+            match = next(match_iter)
+        except StopIteration:
+            raise ValueError(
+                "%{for-each-file} expects a path, but it is not specified."
+            )
+        try:
+            # FIXME: here we make sure there is only one %{for-each-file}
+            # to expand, but we can support multiple via cartesian product.
+            # It doesn't seem immediately useful, so we are keeping this
+            # function simple. 
+            next(match_iter)
+            raise ValueError(
+                "Multiple %{for-each-file} per directive are not supported"
+                " yet."
+            )
+        except StopIteration:
+            pass
+        path = Path(match[1]).resolve()
+        if not path.is_absolute():
+            raise ValueError(
+                "%{for-each-file} expects an absulute path,"
+                " but '{}' is not absolute."
+                .format(str(path))
+            )
+        if path.is_dir() and '*' not in str(path):
+            path = path / '*'
+        for file in iglob(str(path), recursive=True):
+            lines.append(pattern.sub(file, ln))
+        return lines
+
     def processLine(ln):
         # Apply substitutions
         ln = substituteIfElse(escapePercents(ln))
@@ -1639,7 +1686,10 @@
             ln = _caching_re_compile(a).sub(str(b), escapePercents(ln))
 
         # Strip the trailing newline and any extra whitespace.
-        return ln.strip()
+        ln = ln.strip()
+
+        ln = substituteForEachFile(escapePercents(ln))
+        return ln
 
     def processLineToFixedPoint(ln):
         assert isinstance(recursion_limit, int) and recursion_limit >= 0
@@ -1671,7 +1721,11 @@
                 # Can come from preamble_commands.
                 assert isinstance(directive, str)
                 line = directive
-            output.append(unescapePercents(process(line)))
+            line = unescapePercents(process(line))
+            if isinstance(line, str):
+                output.append(line)
+            elif isinstance(line, list):
+                output.extend(line)
 
     return output