Pattern,
     Sequence,
     Set,
+    Sized,
     Tuple,
     Type,
     TypeVar,
     ),
     show_default=True,
 )
+@click.option(
+    "--force-exclude",
+    type=str,
+    help=(
+        "Like --exclude, but files and directories matching this regex will be "
+        "excluded even when they are passed explicitly as arguments"
+    ),
+)
 @click.option(
     "-q",
     "--quiet",
     verbose: bool,
     include: str,
     exclude: str,
+    force_exclude: Optional[str],
     src: Tuple[str, ...],
     config: Optional[str],
 ) -> None:
     if code is not None:
         print(format_str(code, mode=mode))
         ctx.exit(0)
+    report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
+    sources = get_sources(
+        ctx=ctx,
+        src=src,
+        quiet=quiet,
+        verbose=verbose,
+        include=include,
+        exclude=exclude,
+        force_exclude=force_exclude,
+        report=report,
+    )
+
+    path_empty(
+        sources,
+        "No Python files are present to be formatted. Nothing to do 😴",
+        quiet,
+        verbose,
+        ctx,
+    )
+
+    if len(sources) == 1:
+        reformat_one(
+            src=sources.pop(),
+            fast=fast,
+            write_back=write_back,
+            mode=mode,
+            report=report,
+        )
+    else:
+        reformat_many(
+            sources=sources, fast=fast, write_back=write_back, mode=mode, report=report
+        )
+
+    if verbose or not quiet:
+        out("Oh no! 💥 💔 💥" if report.return_code else "All done! ✨ 🍰 ✨")
+        click.secho(str(report), err=True)
+    ctx.exit(report.return_code)
+
+
+def get_sources(
+    *,
+    ctx: click.Context,
+    src: Tuple[str, ...],
+    quiet: bool,
+    verbose: bool,
+    include: str,
+    exclude: str,
+    force_exclude: Optional[str],
+    report: "Report",
+) -> Set[Path]:
+    """Compute the set of files to be formatted."""
     try:
         include_regex = re_compile_maybe_verbose(include)
     except re.error:
     except re.error:
         err(f"Invalid regular expression for exclude given: {exclude!r}")
         ctx.exit(2)
-    report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
+    try:
+        force_exclude_regex = (
+            re_compile_maybe_verbose(force_exclude) if force_exclude else None
+        )
+    except re.error:
+        err(f"Invalid regular expression for force_exclude given: {force_exclude!r}")
+        ctx.exit(2)
+
     root = find_project_root(src)
     sources: Set[Path] = set()
-    path_empty(src, quiet, verbose, ctx)
+    path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx)
+    exclude_regexes = [exclude_regex]
+    if force_exclude_regex is not None:
+        exclude_regexes.append(force_exclude_regex)
+
     for s in src:
         p = Path(s)
         if p.is_dir():
             sources.update(
-                gen_python_files_in_dir(
-                    p, root, include_regex, exclude_regex, report, get_gitignore(root)
+                gen_python_files(
+                    p.iterdir(),
+                    root,
+                    include_regex,
+                    exclude_regexes,
+                    report,
+                    get_gitignore(root),
                 )
             )
-        elif p.is_file() or s == "-":
-            # if a file was explicitly given, we don't care about its extension
+        elif s == "-":
             sources.add(p)
+        elif p.is_file():
+            sources.update(
+                gen_python_files(
+                    [p], root, None, exclude_regexes, report, get_gitignore(root)
+                )
+            )
         else:
             err(f"invalid path: {s}")
-    if len(sources) == 0:
-        if verbose or not quiet:
-            out("No Python files are present to be formatted. Nothing to do 😴")
-        ctx.exit(0)
-
-    if len(sources) == 1:
-        reformat_one(
-            src=sources.pop(),
-            fast=fast,
-            write_back=write_back,
-            mode=mode,
-            report=report,
-        )
-    else:
-        reformat_many(
-            sources=sources, fast=fast, write_back=write_back, mode=mode, report=report
-        )
-
-    if verbose or not quiet:
-        out("Oh no! 💥 💔 💥" if report.return_code else "All done! ✨ 🍰 ✨")
-        click.secho(str(report), err=True)
-    ctx.exit(report.return_code)
+    return sources
 
 
 def path_empty(
-    src: Tuple[str, ...], quiet: bool, verbose: bool, ctx: click.Context
+    src: Sized, msg: str, quiet: bool, verbose: bool, ctx: click.Context
 ) -> None:
     """
     Exit if there is no `src` provided for formatting
     """
-    if not src:
+    if len(src) == 0:
         if verbose or not quiet:
-            out("No Path provided. Nothing to do 😴")
+            out(msg)
             ctx.exit(0)
 
 
     return PathSpec.from_lines("gitwildmatch", lines)
 
 
-def gen_python_files_in_dir(
-    path: Path,
+def gen_python_files(
+    paths: Iterable[Path],
     root: Path,
-    include: Pattern[str],
-    exclude: Pattern[str],
+    include: Optional[Pattern[str]],
+    exclude_regexes: Iterable[Pattern[str]],
     report: "Report",
     gitignore: PathSpec,
 ) -> Iterator[Path]:
     `report` is where output about exclusions goes.
     """
     assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
-    for child in path.iterdir():
-        # First ignore files matching .gitignore
-        if gitignore.match_file(child.as_posix()):
-            report.path_ignored(child, "matches the .gitignore file content")
-            continue
-
+    for child in paths:
         # Then ignore with `exclude` option.
         try:
-            normalized_path = "/" + child.resolve().relative_to(root).as_posix()
+            normalized_path = child.resolve().relative_to(root).as_posix()
         except OSError as e:
             report.path_ignored(child, f"cannot be read because {e}")
             continue
-
         except ValueError:
             if child.is_symlink():
                 report.path_ignored(
 
             raise
 
+        # First ignore files matching .gitignore
+        if gitignore.match_file(normalized_path):
+            report.path_ignored(child, "matches the .gitignore file content")
+            continue
+
+        normalized_path = "/" + normalized_path
         if child.is_dir():
             normalized_path += "/"
 
-        exclude_match = exclude.search(normalized_path)
-        if exclude_match and exclude_match.group(0):
-            report.path_ignored(child, "matches the --exclude regular expression")
+        is_excluded = False
+        for exclude in exclude_regexes:
+            exclude_match = exclude.search(normalized_path) if exclude else None
+            if exclude_match and exclude_match.group(0):
+                report.path_ignored(child, "matches the --exclude regular expression")
+                is_excluded = True
+                break
+        if is_excluded:
             continue
 
         if child.is_dir():
-            yield from gen_python_files_in_dir(
-                child, root, include, exclude, report, gitignore
+            yield from gen_python_files(
+                child.iterdir(), root, include, exclude_regexes, report, gitignore
             )
 
         elif child.is_file():
-            include_match = include.search(normalized_path)
+            include_match = include.search(normalized_path) if include else True
             if include_match:
                 yield child
 
 
     ) -> None:
         runner = BlackRunner()
         if ignore_config:
-            args = ["--config", str(THIS_DIR / "empty.toml"), *args]
+            args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
         result = runner.invoke(black.main, args)
-        self.assertEqual(result.exit_code, exit_code, msg=runner.stderr_bytes.decode())
+        self.assertEqual(
+            result.exit_code,
+            exit_code,
+            msg=f"Failed with args: {args}. Stderr: {runner.stderr_bytes.decode()!r}",
+        )
 
     @patch("black.dump_to_file", dump_to_stderr)
     def checkSourceFile(self, name: str) -> None:
         ]
         this_abs = THIS_DIR.resolve()
         sources.extend(
-            black.gen_python_files_in_dir(
-                path, this_abs, include, exclude, report, gitignore
+            black.gen_python_files(
+                path.iterdir(), this_abs, include, [exclude], report, gitignore
             )
         )
         self.assertEqual(sorted(expected), sorted(sources))
         ]
         this_abs = THIS_DIR.resolve()
         sources.extend(
-            black.gen_python_files_in_dir(
-                path, this_abs, include, exclude, report, gitignore
+            black.gen_python_files(
+                path.iterdir(), this_abs, include, [exclude], report, gitignore
             )
         )
         self.assertEqual(sorted(expected), sorted(sources))
         ]
         this_abs = THIS_DIR.resolve()
         sources.extend(
-            black.gen_python_files_in_dir(
-                path,
+            black.gen_python_files(
+                path.iterdir(),
                 this_abs,
                 empty,
-                re.compile(black.DEFAULT_EXCLUDES),
+                [re.compile(black.DEFAULT_EXCLUDES)],
                 report,
                 gitignore,
             )
         ]
         this_abs = THIS_DIR.resolve()
         sources.extend(
-            black.gen_python_files_in_dir(
-                path,
+            black.gen_python_files(
+                path.iterdir(),
                 this_abs,
                 re.compile(black.DEFAULT_INCLUDES),
-                empty,
+                [empty],
                 report,
                 gitignore,
             )
         child.is_symlink.return_value = True
         try:
             list(
-                black.gen_python_files_in_dir(
-                    path, root, include, exclude, report, gitignore
+                black.gen_python_files(
+                    path.iterdir(), root, include, exclude, report, gitignore
                 )
             )
         except ValueError as ve:
         child.is_symlink.return_value = False
         with self.assertRaises(ValueError):
             list(
-                black.gen_python_files_in_dir(
-                    path, root, include, exclude, report, gitignore
+                black.gen_python_files(
+                    path.iterdir(), root, include, exclude, report, gitignore
                 )
             )
         path.iterdir.assert_called()