diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt
--- a/clang/tools/CMakeLists.txt
+++ b/clang/tools/CMakeLists.txt
@@ -51,3 +51,8 @@
 
 add_clang_subdirectory(amdgpu-arch)
 add_clang_subdirectory(nvptx-arch)
+
+install(PROGRAMS run-clang-format.py
+  DESTINATION "${CMAKE_INSTALL_BINDIR}"
+  COMPONENT clang-format
+  RENAME run-clang-format)
diff --git a/clang/tools/run-clang-format.py b/clang/tools/run-clang-format.py
new file mode 100644
--- /dev/null
+++ b/clang/tools/run-clang-format.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+#
+#===- run-clang-format.py - Parallel clang-format runner ----*- python -*--===#
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+#===-----------------------------------------------------------------------===#
+
+"""
+Parallel clang-format runner
+==========================
+
+Runs clang-format over all files in given directories. Requires clang-format in $PATH.
+
+Example invocations.
+- Run clang-format on all files in the current working directory.
+    run-clang-format.py
+
+- Run clang-format on all files in the chosen directories.
+    run-clang-format.py dir1 dir2 dir3
+"""
+
+
+from __future__ import print_function
+import argparse
+import fnmatch
+import os
+import multiprocessing
+import queue
+import subprocess
+import sys
+import threading
+
+
+def glob_files(args):
+    files = []
+
+    extensions = args.extensions.split(',')
+
+    for directory in args.directories:
+        for root, _, filenames in os.walk(directory):
+            for ext in extensions:
+                for filename in fnmatch.filter(filenames, '*.' + ext):
+                    files.append(os.path.join(root, filename))
+
+    return files
+
+
+def parse_args(argv=None):
+    if argv is None:
+        argv = sys.argv
+    parser = argparse.ArgumentParser(
+        description='Runs clang-format over all files in given directories.'
+        ' Requires clang-format in PATH.')
+    parser.add_argument('--clang-format-binary', metavar='PATH',
+                        default='clang-format',
+                        help='path to clang-format binary')
+    parser.add_argument('-e', '--extensions', dest='extensions',
+                        help='comma-delimited list of extensions used to glob source files',
+                        default="c,cc,cpp,cxx,c++,h,hh,hpp,hxx,h++")
+    parser.add_argument('-style',
+                        help='formatting style',
+                        default="file")
+    parser.add_argument('--no-inplace', dest='inplace', action='store_false',
+                        help='do not format files inplace, but write output to the console'
+                        ' (useful for debugging)',
+                        default=True)
+    parser.add_argument('-j', metavar='THREAD_COUNT', type=int, default=0,
+                        help='number of clang-format instances to be run in parallel')
+    parser.add_argument('-v', '--verbose', action='store_true',
+                        help='output verbose comments')
+    parser.add_argument(metavar='DIRPATH', dest='directories', nargs='*',
+                        help='path(s) used to glob source files')
+
+    args = parser.parse_args(argv[1:])
+
+    if not args.directories:
+        args.directories = [os.getcwd()]
+
+    check_clang_format_binary(args)
+
+    return args
+
+
+def _get_format_invocation(args, filename):
+    invocation = [args.clang_format_binary]
+    invocation.append('-style=' + args.style)
+    if args.inplace:
+        invocation.append('-i')
+
+    invocation.append(filename)
+    return invocation
+
+
+def check_clang_format_binary(args):
+    """Checks if invoking supplied clang-format binary works."""
+    try:
+        subprocess.check_output([args.clang_format_binary, '--version'])
+    except OSError:
+        print('Unable to run clang-format. Is clang-format '
+              'binary correctly specified?', file=sys.stderr)
+        raise
+
+
+def run_format(args, task_queue, formatted_files):
+    """Takes filenames out of queue and runs clang-format on them."""
+    while True:
+        filename = task_queue.get()
+        invocation = _get_format_invocation(args, filename)
+
+        if args.verbose:
+            print('Processing {}'.format(filename))
+        formatted = subprocess.check_output(invocation)
+        formatted_files[filename] = formatted
+
+        task_queue.task_done()
+
+
+def format_all(args, files):
+    max_task = args.j
+    if max_task == 0:
+        max_task = multiprocessing.cpu_count()
+
+    formatted_files = {}
+
+    try:
+        # Spin up a bunch of format-launching threads.
+        task_queue = queue.Queue(max_task)
+        for _ in range(max_task):
+            task_thread = threading.Thread(target=run_format,
+                                           args=(args, task_queue, formatted_files))
+            task_thread.daemon = True
+            task_thread.start()
+
+        # Fill the queue with files.
+        for name in files:
+            task_queue.put(name)
+
+        # Wait for all threads to be done.
+        task_queue.join()
+
+    except OSError:
+        print("Cannot find clang-format at '{}'.".format(args.clang_format_binary),
+              file=sys.stderr)
+        raise
+
+    except subprocess.CalledProcessError as ex:
+        print("Running clang-format failed with non-zero status.", file=sys.stderr)
+        print("Command    : {}".format(' '.join(ex.cmd)), file=sys.stderr)
+        print("Return code: {}".format(str(ex.returncode)), file=sys.stderr)
+        raise
+
+    return formatted_files
+
+
+def main():
+    args = parse_args()
+
+    files = glob_files(args)
+
+    format_all(args, files)
+
+
+if __name__ == '__main__':
+    main()  # pragma: no cover