Repository: sphinx-doc/sphinx

Issue:
Don't add ANSI colour codes to Sphinx -w warnings file
### Describe the bug

If the terminal supports colour, Sphinx outputs in colour. This is good, it improves readability.

But if outputting to a file, it also adds ANSI colour codes, where is shouldn't.

### How to Reproduce

`index.rst` with an error, in this case, a typo in the role:
```rst
:peep:`8`
```

And an empty `conf.py` will do.

1. Build with warnings and output to a file: `sphinx-build -b html . _build -n -w sphinx-warnings.txt`

![image](https://github.com/sphinx-doc/sphinx/assets/1324225/448edc41-6d06-43f6-a7e9-aaefcd3acaae)

3. View the file in an editor:

Actual result:

```
[31m/private/tmp/bug-report/index.rst:1: ERROR: Unknown interpreted text role "peep".[39;49;00m
```

Because Sphinx is correctly detecting my terminal can do colour, it's correctly adding ANSI colour codes to the terminal output.

But it's also incorrectly adding them to the text file. 

Expected result:

Colour in the terminal but no ANSI colour codes in the text file:

```
/private/tmp/bug-report/index.rst:1: ERROR: Unknown interpreted text role "peep".
```


### Environment Information

```text
Platform:              darwin; (macOS-13.5-arm64-arm-64bit)
Python version:        3.11.4 (v3.11.4:d2340ef257, Jun  6 2023, 19:15:51) [Clang 13.0.0 (clang-1300.0.29.30)])
Python implementation: CPython
Sphinx version:        7.2.2
Docutils version:      0.20.1
Jinja2 version:        3.1.2
Pygments version:      2.16.1
```


### Sphinx extensions

```python
None.
```


### Additional context

_No response_

Discussion:
mgeier: Is the `--keep-colors` option really necessary?

I can't imagine anyone wanting those escape sequences in their warnings file.

More importantly, I think adding this option would cause confusion with the already existing `--color`  and `--no-color` options.
picnixz: Err.. I don't really know. Like sometimes you may want to keep the colors when you print them using some weird commands. Or for filtering. 

It was just to ensure that if there was someone out there doing it, then they don't need to change much (although I would recommend using `tee` instead then).
picnixz: On a second though, I think not having that option would be the best. People that want to keep colora should simply tee the standard output I think.
picnixz: So, I removed `--keep-colors` since it's indeed not a good idea to keep (and it simplified the flow).
picnixz: I'll make the changes today or tomorrow and add a CHANGES entry as well.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index b02c10934..f551c76ec 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,6 +20,9 @@ Bugs fixed
   Patch by Vinay Sajip.
 * #11622: Ensure that the order of keys in ``searchindex.js`` is deterministic.
   Patch by Pietro Albini.
+* #11617: ANSI control sequences are stripped from the output when writing to
+  a warnings file with :option:`-w <sphinx-build -w>`.
+  Patch by Bénédikt Tran.
 
 Testing
 -------
diff --git a/doc/man/sphinx-build.rst b/doc/man/sphinx-build.rst
index a1fb9be40..654a851ec 100644
--- a/doc/man/sphinx-build.rst
+++ b/doc/man/sphinx-build.rst
@@ -20,7 +20,7 @@ files, including ``conf.py``.
 format is selected by specifying the builder name on the command line; it
 defaults to HTML.  Builders can also perform other tasks related to
 documentation processing.  For a list of available builders, refer to
-:option:`sphinx-build -b`.
+:doc:`/usage/builders/index`.
 
 By default, everything that is outdated is built.  Output only for selected
 files can be built by specifying individual filenames.
@@ -30,87 +30,47 @@ Options
 
 .. program:: sphinx-build
 
-.. option:: -b buildername
-
-   The most important option: it selects a builder.  The most common builders
-   are:
-
-   **html**
-      Build HTML pages.  This is the default builder.
-
-   **dirhtml**
-      Build HTML pages, but with a single directory per document.  Makes for
-      prettier URLs (no ``.html``) if served from a webserver.
-
-   **singlehtml**
-      Build a single HTML with the whole content.
-
-   **htmlhelp**, **qthelp**, **devhelp**, **epub**
-      Build HTML files with additional information for building a documentation
-      collection in one of these formats.
-
-   **applehelp**
-      Build an Apple Help Book.  Requires :program:`hiutil` and
-      :program:`codesign`, which are not Open Source and presently only
-      available on Mac OS X 10.6 and higher.
-
-   **latex**
-      Build LaTeX sources that can be compiled to a PDF document using
-      :program:`pdflatex`.
-
-   **man**
-      Build manual pages in groff format for UNIX systems.
-
-   **texinfo**
-      Build Texinfo files that can be processed into Info files using
-      :program:`makeinfo`.
-
-   **text**
-      Build plain text files.
-
-   **gettext**
-      Build gettext-style message catalogs (``.pot`` files).
-
-   **doctest**
-      Run all doctests in the documentation, if the :mod:`~sphinx.ext.doctest`
-      extension is enabled.
-
-   **linkcheck**
-      Check the integrity of all external links.
-
-   **xml**
-     Build Docutils-native XML files.
-
-   **pseudoxml**
-     Build compact pretty-printed "pseudo-XML" files displaying the
-     internal structure of the intermediate document trees.
-
-   See :doc:`/usage/builders/index` for a list of all builders shipped with
-   Sphinx.  Extensions can add their own builders.
-
 .. _make_mode:
 
 .. option:: -M buildername
 
-   Alternative to :option:`-b`. Uses the Sphinx :program:`make_mode` module,
-   which provides the same build functionality as a default :ref:`Makefile or
-   Make.bat <makefile_options>`. In addition to all Sphinx
-   :doc:`/usage/builders/index`, the following build pipelines are available:
+   Select a builder, using the *make-mode*.
+   See :doc:`/usage/builders/index` for a list of all of Sphinx's built-in builders.
+   Extensions can add their own builders.
+
+   .. important::
+      Sphinx only recognizes the ``-M`` option if it is used first.
+
+   The *make-mode* provides the same build functionality as
+   a default :ref:`Makefile or Make.bat <makefile_options>`,
+   and provides the following additional build pipelines:
 
-   **latexpdf**
+   *latexpdf*
      Build LaTeX files and run them through :program:`pdflatex`, or as per
      :confval:`latex_engine` setting.
      If :confval:`language` is set to ``'ja'``, will use automatically
      the :program:`platex/dvipdfmx` latex to PDF pipeline.
 
-   **info**
+   *info*
      Build Texinfo files and run them through :program:`makeinfo`.
 
-   .. important::
-      Sphinx only recognizes the ``-M`` option if it is placed first.
+   .. note::
+
+      The default output directory locations when using *make-mode*
+      differ from the defaults when using :option:`-b`.
+
+      * doctrees are saved to ``<outputdir>/doctrees``
+      * output files are saved to ``<outputdir>/<builder name>``
 
    .. versionadded:: 1.2.1
 
+.. option:: -b buildername
+
+   Selects a builder.
+
+   See :doc:`/usage/builders/index` for a list of all of Sphinx's built-in builders.
+   Extensions can add their own builders.
+
 .. option:: -a
 
    If given, always write all output files. The default is to only write output
@@ -235,6 +195,10 @@ Options
 
    Write warnings (and errors) to the given file, in addition to standard error.
 
+   .. versionchanged:: 7.3
+
+      ANSI control sequences are stripped when writing to *file*.
+
 .. option:: -W
 
    Turn warnings into errors.  This means that the build stops at the first
diff --git a/doc/tutorial/getting-started.rst b/doc/tutorial/getting-started.rst
index 5cf0b38aa..678f08db3 100644
--- a/doc/tutorial/getting-started.rst
+++ b/doc/tutorial/getting-started.rst
@@ -104,7 +104,7 @@ the documentation as HTML for the first time.  To do that, run this command:
 
 .. code-block:: console
 
-   (.venv) $ sphinx-build -b html docs/source/ docs/build/html
+   (.venv) $ sphinx-build -M html docs/source/ docs/build/
 
 And finally, open ``docs/build/html/index.html`` in your browser.  You should see
 something like this:
diff --git a/doc/usage/advanced/intl.rst b/doc/usage/advanced/intl.rst
index ae6e7dc9d..e858c3c32 100644
--- a/doc/usage/advanced/intl.rst
+++ b/doc/usage/advanced/intl.rst
@@ -27,7 +27,7 @@ Sphinx uses these facilities to translate whole documents.
 
 Initially project maintainers have to collect all translatable strings (also
 referred to as *messages*) to make them known to translators.  Sphinx extracts
-these through invocation of ``sphinx-build -b gettext``.
+these through invocation of ``sphinx-build -M gettext``.
 
 Every single element in the doctree will end up in a single message which
 results in lists being equally split into different chunks while large
diff --git a/doc/usage/builders/index.rst b/doc/usage/builders/index.rst
index ce2c5dce0..92c88839d 100644
--- a/doc/usage/builders/index.rst
+++ b/doc/usage/builders/index.rst
@@ -10,9 +10,62 @@ Builders
 These are the built-in Sphinx builders.  More builders can be added by
 :doc:`extensions </usage/extensions/index>`.
 
-The builder's "name" must be given to the **-b** command-line option of
+The builder's "name" must be given to the **-M** or **-b** command-line options of
 :program:`sphinx-build` to select a builder.
 
+The most common builders are:
+
+**html**
+   Build HTML pages.  This is the default builder.
+
+**dirhtml**
+   Build HTML pages, but with a single directory per document.  Makes for
+   prettier URLs (no ``.html``) if served from a webserver.
+
+**singlehtml**
+   Build a single HTML with the whole content.
+
+**htmlhelp**, **qthelp**, **devhelp**, **epub**
+   Build HTML files with additional information for building a documentation
+   collection in one of these formats.
+
+**applehelp**
+   Build an Apple Help Book.  Requires :program:`hiutil` and
+   :program:`codesign`, which are not Open Source and presently only
+   available on Mac OS X 10.6 and higher.
+
+**latex**
+   Build LaTeX sources that can be compiled to a PDF document using
+   :program:`pdflatex`.
+
+**man**
+   Build manual pages in groff format for UNIX systems.
+
+**texinfo**
+   Build Texinfo files that can be processed into Info files using
+   :program:`makeinfo`.
+
+**text**
+   Build plain text files.
+
+**gettext**
+   Build gettext-style message catalogs (``.pot`` files).
+
+**doctest**
+   Run all doctests in the documentation, if the :mod:`~sphinx.ext.doctest`
+   extension is enabled.
+
+**linkcheck**
+   Check the integrity of all external links.
+
+**xml**
+  Build Docutils-native XML files.
+
+**pseudoxml**
+  Build compact pretty-printed "pseudo-XML" files displaying the
+  internal structure of the intermediate document trees.
+
+--------------
 
 .. module:: sphinx.builders.html
 .. class:: StandaloneHTMLBuilder
diff --git a/doc/usage/extensions/coverage.rst b/doc/usage/extensions/coverage.rst
index 1390ebf11..49a2334a1 100644
--- a/doc/usage/extensions/coverage.rst
+++ b/doc/usage/extensions/coverage.rst
@@ -9,7 +9,7 @@ This extension features one additional builder, the :class:`CoverageBuilder`.
 .. class:: CoverageBuilder
 
    To use this builder, activate the coverage extension in your configuration
-   file and give ``-b coverage`` on the command line.
+   file and give ``-M coverage`` on the command line.
 
 .. todo:: Write this section.
 
diff --git a/doc/usage/extensions/graphviz.rst b/doc/usage/extensions/graphviz.rst
index c134f6ddb..231bd3699 100644
--- a/doc/usage/extensions/graphviz.rst
+++ b/doc/usage/extensions/graphviz.rst
@@ -211,7 +211,7 @@ There are also these config values:
    :program:`sphinx-build` command line via the :option:`-D <sphinx-build -D>`
    option should be preferable, like this::
 
-      sphinx-build -b html -D graphviz_dot=C:\graphviz\bin\dot.exe . _build/html
+      sphinx-build -M html -D graphviz_dot=C:\graphviz\bin\dot.exe . _build
 
 .. confval:: graphviz_dot_args
 
diff --git a/doc/usage/extensions/math.rst b/doc/usage/extensions/math.rst
index df4fc6b84..a6f7449b9 100644
--- a/doc/usage/extensions/math.rst
+++ b/doc/usage/extensions/math.rst
@@ -75,7 +75,7 @@ are built:
    :program:`sphinx-build` command line via the :option:`-D <sphinx-build -D>`
    option should be preferable, like this::
 
-      sphinx-build -b html -D imgmath_latex=C:\tex\latex.exe . _build/html
+      sphinx-build -M html -D imgmath_latex=C:\tex\latex.exe . _build
 
    This value should only contain the path to the latex executable, not further
    arguments; use :confval:`imgmath_latex_args` for that purpose.
diff --git a/doc/usage/quickstart.rst b/doc/usage/quickstart.rst
index 0773f6030..da00bb792 100644
--- a/doc/usage/quickstart.rst
+++ b/doc/usage/quickstart.rst
@@ -130,11 +130,11 @@ docs.  A build is started with the :program:`sphinx-build` program:
 
 .. code-block:: console
 
-   $ sphinx-build -b html sourcedir builddir
+   $ sphinx-build -M html sourcedir outputdir
 
-where *sourcedir* is the :term:`source directory`, and *builddir* is the
+where *sourcedir* is the :term:`source directory`, and *outputdir* is the
 directory in which you want to place the built documentation.
-The :option:`-b <sphinx-build -b>` option selects a builder; in this example
+The :option:`-M <sphinx-build -M>` option selects a builder; in this example
 Sphinx will build HTML files.
 
 |more| Refer to the :doc:`sphinx-build man page </man/sphinx-build>` for all
diff --git a/sphinx/cmd/build.py b/sphinx/cmd/build.py
index 64452c2b1..9f31ffe72 100644
--- a/sphinx/cmd/build.py
+++ b/sphinx/cmd/build.py
@@ -21,7 +21,7 @@
 from sphinx.application import Sphinx
 from sphinx.errors import SphinxError, SphinxParallelError
 from sphinx.locale import __
-from sphinx.util import Tee
+from sphinx.util._io import TeeStripANSI
 from sphinx.util.console import (  # type: ignore[attr-defined]
     color_terminal,
     nocolor,
@@ -34,6 +34,11 @@
 
 if TYPE_CHECKING:
     from collections.abc import Sequence
+    from typing import Protocol
+
+    class SupportsWrite(Protocol):
+        def write(self, text: str, /) -> int | None:
+            ...
 
 
 def handle_exception(
@@ -266,11 +271,12 @@ def _parse_logging(
         try:
             warnfile = path.abspath(warnfile)
             ensuredir(path.dirname(warnfile))
+            # the caller is responsible for closing this file descriptor
             warnfp = open(warnfile, 'w', encoding="utf-8")  # NoQA: SIM115
         except Exception as exc:
             parser.error(__('cannot open warning file %r: %s') % (
                 warnfile, exc))
-        warning = Tee(warning, warnfp)  # type: ignore[assignment]
+        warning = TeeStripANSI(warning, warnfp)  # type: ignore[assignment]
         error = warning
 
     return status, warning, error, warnfp
@@ -334,6 +340,10 @@ def build_main(argv: Sequence[str]) -> int:
     except (Exception, KeyboardInterrupt) as exc:
         handle_exception(app, args, exc, args.error)
         return 2
+    finally:
+        if warnfp is not None:
+            # close the file descriptor for the warnings file opened by Sphinx
+            warnfp.close()
 
 
 def _bug_report_info() -> int:
diff --git a/sphinx/util/_io.py b/sphinx/util/_io.py
index 23d525685..1dc111c0c 100644
--- a/sphinx/util/_io.py
+++ b/sphinx/util/_io.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from typing import TYPE_CHECKING
 
 from sphinx.util.console import _strip_escape_sequences
@@ -6,7 +8,7 @@
     from typing import Protocol
 
     class SupportsWrite(Protocol):
-        def write(self, text: str, /) -> None:
+        def write(self, text: str, /) -> int | None:
             ...
 
 


Test patch:
diff --git a/tests/test_build.py b/tests/test_build.py
index ed4bc4389..1fb2f7585 100644
--- a/tests/test_build.py
+++ b/tests/test_build.py
@@ -2,11 +2,14 @@
 
 import os
 import shutil
+from contextlib import contextmanager
+from pathlib import Path
 from unittest import mock
 
 import pytest
 from docutils import nodes
 
+from sphinx.cmd.build import build_main
 from sphinx.errors import SphinxError
 
 
@@ -133,3 +136,29 @@ def test_image_glob(app, status, warning):
     assert doctree[0][3][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf',
                                               'image/svg+xml': 'subdir/svgimg.svg'}
     assert doctree[0][3][0]['uri'] == 'subdir/svgimg.*'
+
+
+@contextmanager
+def force_colors():
+    forcecolor = os.environ.get('FORCE_COLOR', None)
+
+    try:
+        os.environ['FORCE_COLOR'] = '1'
+        yield
+    finally:
+        if forcecolor is None:
+            os.environ.pop('FORCE_COLOR', None)
+        else:
+            os.environ['FORCE_COLOR'] = forcecolor
+
+
+def test_log_no_ansi_colors(tmp_path):
+    with force_colors():
+        wfile = tmp_path / 'warnings.txt'
+        srcdir = Path(__file__).parent / 'roots/test-nitpicky-warnings'
+        argv = list(map(str, ['-b', 'html', srcdir, tmp_path, '-n', '-w', wfile]))
+        retcode = build_main(argv)
+        assert retcode == 0
+
+        content = wfile.read_text(encoding='utf8')
+        assert '\x1b[91m' not in content