Repository: pallets/click
Pull request #3001 (2025-07-16 06:22:39)

Merge stable into main


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 1b8af9a..fbb3254 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,14 @@
 .. currentmodule:: click
 
+Version 8.2.2
+-------------
+
+Unreleased
+
+-   Fix reconciliation of `default`, `flag_value` and `type` parameters for
+    flag options, as well as parsing and normalization of environment variables.
+    :issue:`2952` :pr:`2956`
+
 Version 8.2.1
 -------------
 
diff --git a/docs/contrib.md b/docs/contrib.md
new file mode 100644
index 0000000..1171696
--- /dev/null
+++ b/docs/contrib.md
@@ -0,0 +1,45 @@
+(contrib)=
+
+# click-contrib
+
+As the user number of Click grows, more and more major feature requests are
+made. To users, it may seem reasonable to include those features with Click;
+however, many of them are experimental or aren't practical to support
+generically. Maintainers have to choose what is reasonable to maintain in Click
+core.
+
+The [click-contrib](https://github.com/click-contrib/) GitHub organization exists as a place to collect third-party
+packages that extend Click's features. It is also meant to ease the effort of
+searching for such extensions.
+
+Please note that the quality and stability of those packages may be different
+from Click itself. While published under a common organization, they are still
+separate from Click and the Pallets maintainers.
+
+## Third-party projects
+
+Other projects that extend Click's features are available outside the
+[click-contrib](https://github.com/click-contrib/) organization.
+
+Some of the most popular and actively maintained are listed below:
+
+| Project                                                 | Description                                                                          | Popularity                                                                                             | Activity                                                                                                    |
+|---------------------------------------------------------|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
+| [Typer](https://github.com/fastapi/typer)               | Use Python type hints to create CLI apps.                                            | ![GitHub stars](https://img.shields.io/github/stars/fastapi/typer?label=%20&style=flat-square)         | ![Last commit](https://img.shields.io/github/last-commit/fastapi/typer?label=%20&style=flat-square)         |
+| [rich-click](https://github.com/ewels/rich-click)       | Format help output with Rich.                                                        | ![GitHub stars](https://img.shields.io/github/stars/ewels/rich-click?label=%20&style=flat-square)      | ![Last commit](https://img.shields.io/github/last-commit/ewels/rich-click?label=%20&style=flat-square)      |
+| [click-app](https://github.com/simonw/click-app)        | Cookiecutter template for creating new CLIs.                                         | ![GitHub stars](https://img.shields.io/github/stars/simonw/click-app?label=%20&style=flat-square)      | ![Last commit](https://img.shields.io/github/last-commit/simonw/click-app?label=%20&style=flat-square)      |
+| [Cloup](https://github.com/janluke/cloup)               | Adds option groups, constraints, command aliases, help themes, suggestions and more. | ![GitHub stars](https://img.shields.io/github/stars/janluke/cloup?label=%20&style=flat-square)         | ![Last commit](https://img.shields.io/github/last-commit/janluke/cloup?label=%20&style=flat-square)         |
+| [Click Extra](https://github.com/kdeldycke/click-extra) | Cloup + colorful `--help`, `--config`, `--show-params`, `--verbosity` options, etc.  | ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/click-extra?label=%20&style=flat-square) | ![Last commit](https://img.shields.io/github/last-commit/kdeldycke/click-extra?label=%20&style=flat-square) |
+
+```{note}
+To make it into the list above, a project:
+
+- must be actively maintained (at least one commit in the last year)
+- must have a reasonable number of stars (at least 20)
+
+If you have a project that meets these criteria, please open a pull request
+to add it to the list.
+
+If a project is no longer maintained or does not meet the criteria above,
+please open a pull request to remove it from the list.
+```
diff --git a/docs/contrib.rst b/docs/contrib.rst
deleted file mode 100644
index 628edd7..0000000
--- a/docs/contrib.rst
+++ /dev/null
@@ -1,58 +0,0 @@
-.. _contrib:
-
-=============
-click-contrib
-=============
-
-As the number of users of Click grows, more and more major feature requests are
-made. To users it may seem reasonable to include those features with Click;
-however, many of them are experimental or aren't practical to support
-generically. Maintainers have to choose what is reasonable to maintain in Click
-core.
-
-The click-contrib_ GitHub organization exists as a place to collect third-party
-packages that extend Click's features. It is also meant to ease the effort of
-searching for such extensions.
-
-Please note that the quality and stability of those packages may be different
-than Click itself. While published under a common organization, they are still
-separate from Click and the Pallets maintainers.
-
-
-Third-party projects
---------------------
-
-Other projects that extend Click's features are available outside of the
-click-contrib_ organization.
-
-Some of the most popular and actively maintained are listed below:
-
-==========================================================  ===========================================================================================  =================================================================================================  ======================================================================================================
-Project                                                     Description                                                                                  Popularity                                                                                         Activity
-==========================================================  ===========================================================================================  =================================================================================================  ======================================================================================================
-`Typer <https://github.com/fastapi/typer>`_                 Use Python type hints to create CLI apps.                                                    .. image:: https://img.shields.io/github/stars/fastapi/typer?label=%20&style=flat-square           .. image:: https://img.shields.io/github/last-commit/fastapi/typer?label=%20&style=flat-square
-                                                                                                                                                            :alt: GitHub stars                                                                                 :alt: Last commit
-`rich-click <https://github.com/ewels/rich-click>`_         Format help outputwith Rich.                                                                 .. image:: https://img.shields.io/github/stars/ewels/rich-click?label=%20&style=flat-square        .. image:: https://img.shields.io/github/last-commit/ewels/rich-click?label=%20&style=flat-square
-                                                                                                                                                            :alt: GitHub stars                                                                                 :alt: Last commit
-`click-app <https://github.com/simonw/click-app>`_          Cookiecutter template for creating new CLIs.                                                 .. image:: https://img.shields.io/github/stars/simonw/click-app?label=%20&style=flat-square        .. image:: https://img.shields.io/github/last-commit/simonw/click-app?label=%20&style=flat-square
-                                                                                                                                                            :alt: GitHub stars                                                                                 :alt: Last commit
-`Cloup <https://github.com/janluke/cloup>`_                 Adds option groups, constraints, command aliases, help themes, suggestions and more.         .. image:: https://img.shields.io/github/stars/janluke/cloup?label=%20&style=flat-square           .. image:: https://img.shields.io/github/last-commit/janluke/cloup?label=%20&style=flat-square
-                                                                                                                                                            :alt: GitHub stars                                                                                 :alt: Last commit
-`Click Extra <https://github.com/kdeldycke/click-extra>`_   Cloup + colorful ``--help``, ``--config``, ``--show-params``, ``--verbosity`` options, etc.  .. image:: https://img.shields.io/github/stars/kdeldycke/click-extra?label=%20&style=flat-square   .. image:: https://img.shields.io/github/last-commit/kdeldycke/click-extra?label=%20&style=flat-square
-                                                                                                                                                            :alt: GitHub stars                                                                                 :alt: Last commit
-==========================================================  ===========================================================================================  =================================================================================================  ======================================================================================================
-
-.. note::
-
-    To make it into the list above, a project:
-
-    - must be actively maintained (at least one commit in the last year)
-    - must have a reasonable number of stars (at least 20)
-
-    If you have a project that meets these criteria, please open a pull request
-    to add it to the list.
-
-    If a project is no longer maintained or does not meet the criteria above,
-    please open a pull request to remove it from the list.
-
-.. _click-contrib: https://github.com/click-contrib/
diff --git a/docs/documentation.rst b/docs/documentation.md
similarity index 67%
rename from docs/documentation.rst
rename to docs/documentation.md
index b60e5b3..199b85b 100644
--- a/docs/documentation.rst
+++ b/docs/documentation.md
@@ -1,17 +1,17 @@
-Help Pages
-===================
+# Help Pages
 
-.. currentmodule:: click
+```{currentmodule} click
+```
 
 Click makes it very easy to document your command line tools. For most things Click automatically generates help pages for you. By design the text is customizable, but the layout is not.
 
-Help Texts
---------------
+## Help Texts
 
 Commands and options accept help arguments. For commands, the docstring of the function is automatically used if provided.
 
 Simple example:
 
+```{eval-rst}
 .. click:example::
 
     @click.command()
@@ -26,17 +26,18 @@ Simple example:
                 click.echo("Hello!")
 
 .. click:run::
-
     invoke(hello, args=['--help'])
+```
 
+## Command Short Help
 
-Command Short Help
-------------------
-
-For subcommands, a short help snippet is generated. By default, it's the first sentence of the docstring. If too long, then it will ellipsize what cannot be fit on a single line with ``...``.  The short help snippet can also be overridden with ``short_help``:
+For subcommands, a short help snippet is generated. By default, it's the first sentence of the docstring. If too long, then it will ellipsize what cannot be fit on a single line with `...`. The short help snippet can also be overridden with `short_help`:
 
+```{eval-rst}
 .. click:example::
 
+    import click
+
     @click.group()
     def cli():
         """A simple command line tool."""
@@ -45,39 +46,39 @@ For subcommands, a short help snippet is generated. By default, it's the first s
     def init():
         """Initializes the repository."""
 
-
 .. click:run::
-
     invoke(cli, args=['--help'])
+```
 
-Command Epilog Help
--------------------
+## Command Epilog Help
 
 The help epilog is printed at the end of the help and is useful for showing example command usages or referencing additional help resources.
 
+```{eval-rst}
 .. click:example::
 
+    import click
+
     @click.command(
-    	epilog='See https://example.com for more details',
-  	)
+        epilog='See https://example.com for more details',
+        )
     def init():
         """Initializes the repository."""
 
-
 .. click:run::
-
     invoke(init, args=['--help'])
+```
 
-.. _documenting-arguments:
+(documenting-arguments)=
 
-Documenting Arguments
-----------------------
+## Documenting Arguments
 
-:class:`click.argument` does not take a ``help`` parameter. This follows the Unix Command Line Tools convention of using arguments only for necessary things and documenting them in the command help text
+{class}`click.argument` does not take a `help` parameter. This follows the Unix Command Line Tools convention of using arguments only for necessary things and documenting them in the command help text
 by name. This should then be done via the docstring.
 
 A brief example:
 
+```{eval-rst}
 .. click:example::
 
     @click.command()
@@ -86,13 +87,13 @@ A brief example:
         """Print FILENAME."""
         click.echo(filename)
 
-
 .. click:run::
-
     invoke(touch, args=['--help'])
+```
 
 Or more explicitly:
 
+```{eval-rst}
 .. click:example::
 
     @click.command()
@@ -105,14 +106,14 @@ Or more explicitly:
         click.echo(filename)
 
 .. click:run::
-
     invoke(touch, args=['--help'])
+```
 
+## Showing Defaults
 
-Showing Defaults
----------------------------
-To control the appearance of defaults pass ``show_default``.
+To control the appearance of defaults pass `show_default`.
 
+```{eval-rst}
 .. click:example::
 
     @click.command()
@@ -121,11 +122,12 @@ To control the appearance of defaults pass ``show_default``.
         click.echo('.' * n)
 
 .. click:run::
-
     invoke(dots, args=['--help'])
+```
 
-For single option boolean flags, the default remains hidden if the default value is False even if show default is true.
+For single option boolean flags, the default remains hidden if the default value is False, even if show default is set to true.
 
+```{eval-rst}
 .. click:example::
 
     @click.command()
@@ -140,16 +142,18 @@ For single option boolean flags, the default remains hidden if the default value
             click.echo('-' * n)
 
 .. click:run::
-
    invoke(dots, args=['--help'])
+```
 
+## Click's Wrapping Behavior
 
-Click's Wrapping Behavior
-----------------------------
 Click's default wrapping ignores single new lines and rewraps the text based on the width of the terminal, to a maximum of 80 characters. In the example notice how the second grouping of three lines is rewrapped into a single paragraph.
 
+```{eval-rst}
 .. click:example::
 
+    import click
+
     @click.command()
     def cli():
         """
@@ -164,17 +168,20 @@ Click's default wrapping ignores single new lines and rewraps the text based on
         """
 
 .. click:run::
-
     invoke(cli, args=['--help'])
+```
+
+## Escaping Click's Wrapping
 
-Escaping Click's Wrapping
----------------------------
-Sometimes Click's wrapping can be a problem, such as when showing code examples where newlines are significant. This behavior can be escaped on a per-paragraph basis by adding a line with only ``\b`` . The ``\b`` is removed from the rendered help text.
+Sometimes Click's wrapping can be a problem, such as when showing code examples where new lines are significant. This behavior can be escaped on a per-paragraph basis by adding a line with only `\b` . The `\b` is removed from the rendered help text.
 
 Example:
 
+```{eval-rst}
 .. click:example::
 
+    import click
+
     @click.command()
     def cli():
         """First paragraph.
@@ -188,28 +195,27 @@ Example:
         that will be rewrapped again.
         """
 
-
 .. click:run::
-
     invoke(cli, args=['--help'])
+```
 
-To change the rendering maximum width, pass ``max_content_width`` when calling the command.
-
-.. code-block:: python
+To change the rendering maximum width, pass `max_content_width` when calling the command.
 
-    cli(max_content_width=120)
+```bash
+cli(max_content_width=120)
+```
 
+## Truncating Help Texts
 
-
-Truncating Help Texts
----------------------
-
-Click gets :class:`Command` help text from the docstring. If you do not want to include part of the docstring, add the ``\f`` escape marker to have Click truncate the help text after the marker.
+Click gets {class}`Command` help text from the docstring. If you do not want to include part of the docstring, add the `\f` escape marker to have Click truncate the help text after the marker.
 
 Example:
 
+```{eval-rst}
 .. click:example::
 
+    import click
+
     @click.command()
     def cli():
         """First paragraph.
@@ -219,16 +225,16 @@ Example:
         """
 
 .. click:run::
-
     invoke(cli, args=['--help'])
+```
 
-.. _doc-meta-variables:
+(doc-meta-variables)=
 
-Placeholder / Meta Variable
------------------------------
+## Placeholder / Meta Variable
 
-The default placeholder variable (`meta variable <https://en.wikipedia.org/wiki/Metasyntactic_variable#IETF_Requests_for_Comments>`_) in the help pages is the parameter name in uppercase with underscores. This can be changed for Commands and Parameters with the ``options_metavar`` and  ``metavar`` kwargs.
+The default placeholder variable ([meta variable](https://en.wikipedia.org/wiki/Metasyntactic_variable#IETF_Requests_for_Comments)) in the help pages is the parameter name in uppercase with underscores. This can be changed for Commands and Parameters with the `options_metavar` and `metavar` kwargs.
 
+```{eval-rst}
 .. click:example::
 
     # This controls entry on the usage line.
@@ -241,28 +247,31 @@ The default placeholder variable (`meta variable <https://en.wikipedia.org/wiki/
         for x in range(count):
             click.echo(f"Hello {name}!")
 
-Example:
+# Example usage:
 
 .. click:run::
-
     invoke(hello, args=['--help'])
 
-Help Parameter Customization
-----------------------------
-Help parameters are automatically added by Click for any command. The default is ``--help`` but can be override by the context setting :attr:`~Context.help_option_names`. Click also performs automatic conflict resolution on the default help parameter so if a command itself implements a parameter named ``help`` then the default help will not be run.
+```
+
+## Help Parameter Customization
+
+Help parameters are automatically added by Click for any command. The default is `--help` but can be overridden by the context setting {attr}`~Context.help_option_names`. Click also performs automatic conflict resolution on the default help parameter, so if a command itself implements a parameter named `help` then the default help will not be run.
 
-This example changes the default parameters to ``-h`` and ``--help``
-instead of just ``--help``:
+This example changes the default parameters to `-h` and `--help`
+instead of just `--help`:
 
+```{eval-rst}
 .. click:example::
 
+    import click
+
     CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
 
     @click.command(context_settings=CONTEXT_SETTINGS)
     def cli():
         pass
 
-
 .. click:run::
-
     invoke(cli, ['-h'])
+```
diff --git a/docs/entry-points.rst b/docs/entry-points.rst
index d5fce61..6c70f97 100644
--- a/docs/entry-points.rst
+++ b/docs/entry-points.rst
@@ -2,7 +2,7 @@ Packaging Entry Points
 ======================
 
 It's recommended to write command line utilities as installable packages with
-entry points instead telling users to run ``python hello.py``.
+entry points instead of telling users to run ``python hello.py``.
 
 A distribution package is a ``.whl`` file you install with pip or another Python
 installer. You use a ``pyproject.toml`` file to describe the project and how it
diff --git a/docs/options.md b/docs/options.md
index c2dd2b6..48c35c8 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -368,6 +368,31 @@ If a list is passed to `envvar`, the first environment variable found is picked.
 
 ```
 
+Variable names are:
+ - [Case-insensitive on Windows but not on other platforms](https://github.com/python/cpython/blob/aa9eb5f757ceff461e6e996f12c89e5d9b583b01/Lib/os.py#L777-L789).
+ - Not stripped of whitespaces and should match the exact name provided to the `envvar` argument.
+
+For flag options, there is two concepts to consider: the activation of the flag driven by the environment variable, and the value of the flag if it is activated.
+
+The environment variable need to be interpreted, because values read from them are always strings. We need to transform these strings into boolean values that will determine if the flag is activated or not.
+
+Here are the rules used to parse environment variable values for flag options:
+   - `true`, `1`, `yes`, `on`, `t`, `y` are interpreted as activating the flag
+   - `false`, `0`, `no`, `off`, `f`, `n` are interpreted as deactivating the flag
+   - The presence of the environment variable without value is interpreted as deactivating the flag
+   - Empty strings are interpreted as deactivating the flag
+   - Values are case-insensitive, so the `True`, `TRUE`, `tRuE` strings are all activating the flag
+   - Values are stripped of leading and trailing whitespaces before being interpreted, so the `" True "` string is transformed to `"true"` and so activates the flag
+   - If the flag option has a `flag_value` argument, passing that value in the environment variable will activate the flag, in addition to all the cases described above
+   - Any other value is interpreted as deactivating the flag
+
+.. caution::
+    For boolean flags with a pair of values, the only recognized environment variable is the one provided to the `envvar` argument.
+
+    So an option defined as `--flag\--no-flag`, with a `envvar="FLAG"` parameter, there is no magical `NO_FLAG=<anything>` variable that is recognized. Only the `FLAG=<anything>` environment variable is recognized.
+
+Once the status of the flag has been determine to be activated or not, the `flag_value` is used as the value of the flag if it is activated. If the flag is not activated, the value of the flag is set to `None` by default.
+
 ## Multiple Options from Environment Values
 
 As options can accept multiple values, pulling in such values from
diff --git a/docs/unicode-support.md b/docs/unicode-support.md
new file mode 100644
index 0000000..ad9ef09
--- /dev/null
+++ b/docs/unicode-support.md
@@ -0,0 +1,100 @@
+# Unicode Support
+
+```{currentmodule} click
+```
+
+Click has to take extra care to support Unicode text in different environments.
+
+- The command line in Unix is traditionally bytes, not Unicode. While there are encoding hints, there are some
+  situations where this can break. The most common one is SSH connections to machines with different locales.
+
+  Misconfigured environments can cause a wide range of Unicode problems due to the lack of support for roundtripping
+  surrogate escapes. This will not be fixed in Click itself!
+
+- Standard input and output is opened in text mode by default. Click has to reopen the stream in binary mode in certain
+  situations. Because there is no standard way to do this, it might not always work. Primarily this can become a problem
+  when testing command-line applications.
+
+  This is not supported:
+
+  ```python
+  sys.stdin = io.StringIO('Input here')
+  sys.stdout = io.StringIO()
+  ```
+
+  Instead you need to do this:
+
+  ```python
+  input = 'Input here'
+  in_stream = io.BytesIO(input.encode('utf-8'))
+  sys.stdin = io.TextIOWrapper(in_stream, encoding='utf-8')
+  out_stream = io.BytesIO()
+  sys.stdout = io.TextIOWrapper(out_stream, encoding='utf-8')
+  ```
+
+  Remember in that case, you need to use `out_stream.getvalue()` and not `sys.stdout.getvalue()` if you want to access
+  the buffer contents as the wrapper will not forward that method.
+
+- `sys.stdin`, `sys.stdout` and `sys.stderr` are by default text-based. When Click needs a binary stream, it attempts to
+  discover the underlying binary stream.
+
+- `sys.argv` is always text. This means that the native type for input values to the types in Click is Unicode, not
+  bytes.
+
+  This causes problems if the terminal is incorrectly set and Python does not figure out the encoding. In that case, the
+  Unicode string will contain error bytes encoded as surrogate escapes.
+
+- When dealing with files, Click will always use the Unicode file system API by using the operating system's reported or
+  guessed filesystem encoding. Surrogates are supported for filenames, so it should be possible to open files through
+  the {func}`File` type even if the environment is misconfigured.
+
+## Surrogate Handling
+
+Click does all the Unicode handling in the standard library and is subject to its behavior. Unicode requires extra care.
+The reason for this is that the encoding detection is done in the interpreter, and on Linux and certain other operating
+systems, its encoding handling is problematic.
+
+The biggest source of frustration is that Click scripts invoked by init systems, deployment tools, or cron jobs will
+refuse to work unless a Unicode locale is exported.
+
+If Click encounters such an environment it will prevent further execution to force you to set a locale. This is done
+because Click cannot know about the state of the system once it's invoked and restore the values before Python's Unicode
+handling kicked in.
+
+If you see something like this error:
+
+```console
+Traceback (most recent call last):
+  ...
+RuntimeError: Click will abort further execution because Python was
+  configured to use ASCII as encoding for the environment. Consult
+  https://click.palletsprojects.com/unicode-support/ for mitigation
+  steps.
+```
+
+You are dealing with an environment where Python thinks you are restricted to ASCII data. The solution to these problems
+is different depending on which locale your computer is running in.
+
+For instance, if you have a German Linux machine, you can fix the problem by exporting the locale to `de_DE.utf-8`:
+
+```console
+export LC_ALL=de_DE.utf-8
+export LANG=de_DE.utf-8
+```
+
+If you are on a US machine, `en_US.utf-8` is the encoding of choice. On some newer Linux systems, you could also try
+`C.UTF-8` as the locale:
+
+```console
+export LC_ALL=C.UTF-8
+export LANG=C.UTF-8
+```
+
+On some systems it was reported that `UTF-8` has to be written as `UTF8` and vice versa. To see which locales are
+supported you can invoke `locale -a`.
+
+You need to export the values before you invoke your Python script.
+
+In Python 3.7 and later you will no longer get a `RuntimeError` in many cases thanks to {pep}`538` and {pep}`540`, which
+changed the default assumption in unconfigured environments. This doesn't change the general issue that your locale may
+be misconfigured.
diff --git a/docs/unicode-support.rst b/docs/unicode-support.rst
deleted file mode 100644
index 680e739..0000000
--- a/docs/unicode-support.rst
+++ /dev/null
@@ -1,112 +0,0 @@
-Unicode Support
-===============
-
-.. currentmodule:: click
-
-Click has to take extra care to support Unicode text in different
-environments.
-
-*   The command line in Unix is traditionally bytes, not Unicode. While
-    there are encoding hints, there are some situations where this can
-    break. The most common one is SSH connections to machines with
-    different locales.
-
-    Misconfigured environments can cause a wide range of Unicode
-    problems due to the lack of support for roundtripping surrogate
-    escapes. This will not be fixed in Click itself!
-
-*   Standard input and output is opened in text mode by default. Click
-    has to reopen the stream in binary mode in certain situations.
-    Because there is no standard way to do this, it might not always
-    work. Primarily this can become a problem when testing command-line
-    applications.
-
-    This is not supported::
-
-        sys.stdin = io.StringIO('Input here')
-        sys.stdout = io.StringIO()
-
-    Instead you need to do this::
-
-        input = 'Input here'
-        in_stream = io.BytesIO(input.encode('utf-8'))
-        sys.stdin = io.TextIOWrapper(in_stream, encoding='utf-8')
-        out_stream = io.BytesIO()
-        sys.stdout = io.TextIOWrapper(out_stream, encoding='utf-8')
-
-    Remember in that case, you need to use ``out_stream.getvalue()``
-    and not ``sys.stdout.getvalue()`` if you want to access the buffer
-    contents as the wrapper will not forward that method.
-
-*   ``sys.stdin``, ``sys.stdout`` and ``sys.stderr`` are by default
-    text-based. When Click needs a binary stream, it attempts to
-    discover the underlying binary stream.
-
-*   ``sys.argv`` is always text. This means that the native type for
-    input values to the types in Click is Unicode, not bytes.
-
-    This causes problems if the terminal is incorrectly set and Python
-    does not figure out the encoding. In that case, the Unicode string
-    will contain error bytes encoded as surrogate escapes.
-
-*   When dealing with files, Click will always use the Unicode file
-    system API by using the operating system's reported or guessed
-    filesystem encoding. Surrogates are supported for filenames, so it
-    should be possible to open files through the :class:`File` type even
-    if the environment is misconfigured.
-
-
-Surrogate Handling
-------------------
-
-Click does all the Unicode handling in the standard library and is
-subject to its behavior. Unicode requires extra care. The reason for
-this is that the encoding detection is done in the interpreter, and on
-Linux and certain other operating systems, its encoding handling is
-problematic.
-
-The biggest source of frustration is that Click scripts invoked by init
-systems, deployment tools, or cron jobs will refuse to work unless a
-Unicode locale is exported.
-
-If Click encounters such an environment it will prevent further
-execution to force you to set a locale. This is done because Click
-cannot know about the state of the system once it's invoked and restore
-the values before Python's Unicode handling kicked in.
-
-If you see something like this error::
-
-    Traceback (most recent call last):
-      ...
-    RuntimeError: Click will abort further execution because Python was
-      configured to use ASCII as encoding for the environment. Consult
-      https://click.palletsprojects.com/unicode-support/ for mitigation
-      steps.
-
-You are dealing with an environment where Python thinks you are
-restricted to ASCII data. The solution to these problems is different
-depending on which locale your computer is running in.
-
-For instance, if you have a German Linux machine, you can fix the
-problem by exporting the locale to ``de_DE.utf-8``::
-
-    export LC_ALL=de_DE.utf-8
-    export LANG=de_DE.utf-8
-
-If you are on a US machine, ``en_US.utf-8`` is the encoding of choice.
-On some newer Linux systems, you could also try ``C.UTF-8`` as the
-locale::
-
-    export LC_ALL=C.UTF-8
-    export LANG=C.UTF-8
-
-On some systems it was reported that ``UTF-8`` has to be written as
-``UTF8`` and vice versa. To see which locales are supported you can
-invoke ``locale -a``.
-
-You need to export the values before you invoke your Python script.
-
-In Python 3.7 and later you will no longer get a ``RuntimeError`` in
-many cases thanks to :pep:`538` and :pep:`540`, which changed the
-default assumption in unconfigured environments. This doesn't change the
-general issue that your locale may be misconfigured.
diff --git a/docs/utils.md b/docs/utils.md
new file mode 100644
index 0000000..c4a553b
--- /dev/null
+++ b/docs/utils.md
@@ -0,0 +1,390 @@
+# Utilities
+
+```{currentmodule} click
+```
+
+Besides the functionality that Click provides to interface with argument parsing and handling, it also provides a bunch
+of addon functionality that is useful for writing command line utilities.
+
+## Printing to Stdout
+
+The most obvious helper is the {func}`echo` function, which in many ways works like the Python `print` statement or
+function. The main difference is that it works the same in many different terminal environments.
+
+Example:
+
+```python
+import click
+
+click.echo('Hello World!')
+```
+
+It can output both text and binary data. It will emit a trailing newline by default, which needs to be suppressed by
+passing `nl=False`:
+
+```python
+click.echo(b'\xe2\x98\x83', nl=False)
+```
+
+Last but not least {func}`echo` uses click's intelligent internal output streams to stdout and stderr which support
+unicode output on the Windows console. This means for as long as you are using `click.echo` you can output unicode
+characters (there are some limitations on the default font with regards to which characters can be displayed).
+
+```{versionadded} 6.0
+```
+
+Click emulates output streams on Windows to support unicode to the Windows console through separate APIs. For more
+information see {doc}`wincmd`.
+
+```{versionadded} 3.0
+```
+
+You can also easily print to standard error by passing `err=True`:
+
+```python
+click.echo('Hello World!', err=True)
+```
+
+(ansi-colors)=
+
+## ANSI Colors
+
+```{versionadded} 2.0
+```
+
+The {func}`echo` function supports ANSI colors and styles. On Windows this uses [colorama](https://pypi.org/project/colorama/).
+
+Primarily this means that:
+
+- Click's {func}`echo` function will automatically strip ANSI color codes if the stream is not connected to a terminal.
+- the {func}`echo` function will transparently connect to the terminal on Windows and translate ANSI codes to terminal
+  API calls. This means that colors will work on Windows the same way they do on other operating systems.
+
+On Windows, Click uses colorama without calling `colorama.init()`. You can still call that in your code, but it's not
+required for Click.
+
+For styling a string, the {func}`style` function can be used:
+
+```python
+import click
+
+click.echo(click.style('Hello World!', fg='green'))
+click.echo(click.style('Some more text', bg='blue', fg='white'))
+click.echo(click.style('ATTENTION', blink=True, bold=True))
+```
+
+The combination of {func}`echo` and {func}`style` is also available in a single function called {func}`secho`:
+
+```python
+click.secho('Hello World!', fg='green')
+click.secho('Some more text', bg='blue', fg='white')
+click.secho('ATTENTION', blink=True, bold=True)
+```
+
+## Pager Support
+
+In some situations, you might want to show long texts on the terminal and let a user scroll through it. This can be
+achieved by using the {func}`echo_via_pager` function which works similarly to the {func}`echo` function, but always
+writes to stdout and, if possible, through a pager.
+
+Example:
+
+```python
+@click.command()
+def less():
+    click.echo_via_pager("\n".join(f"Line {idx}" for idx in range(200)))
+```
+
+If you want to use the pager for a lot of text, especially if generating everything in advance would take a lot of time,
+you can pass a generator (or generator function) instead of a string:
+
+```python
+def _generate_output():
+    for idx in range(50000):
+        yield f"Line {idx}\n"
+
+@click.command()
+def less():
+    click.echo_via_pager(_generate_output())
+```
+
+## Screen Clearing
+
+```{versionadded} 2.0
+```
+
+To clear the terminal screen, you can use the {func}`clear` function that is provided starting with Click 2.0. It does
+what the name suggests: it clears the entire visible screen in a platform-agnostic way:
+
+```python
+import click
+click.clear()
+```
+
+## Getting Characters from Terminal
+
+```{versionadded} 2.0
+```
+
+Normally, when reading input from the terminal, you would read from standard input. However, this is buffered input and
+will not show up until the line has been terminated. In certain circumstances, you might not want to do that and instead
+read individual characters as they are being written.
+
+For this, Click provides the {func}`getchar` function which reads a single character from the terminal buffer and
+returns it as a Unicode character.
+
+Note that this function will always read from the terminal, even if stdin is instead a pipe.
+
+Example:
+
+```python
+import click
+
+click.echo('Continue? [yn] ', nl=False)
+c = click.getchar()
+click.echo()
+if c == 'y':
+    click.echo('We will go on')
+elif c == 'n':
+    click.echo('Abort!')
+else:
+    click.echo('Invalid input :(')
+```
+
+Note that this reads raw input, which means that things like arrow keys will show up in the platform's native escape
+format. The only characters translated are `^C` and `^D` which are converted into keyboard interrupts and end of file
+exceptions respectively. This is done because otherwise, it's too easy to forget about that and to create scripts that
+cannot be properly exited.
+
+## Waiting for Key Press
+
+```{versionadded} 2.0
+```
+
+Sometimes, it's useful to pause until the user presses any key on the keyboard. This is especially useful on Windows
+where `cmd.exe` will close the window at the end of the command execution by default, instead of waiting.
+
+In click, this can be accomplished with the {func}`pause` function. This function will print a quick message to the
+terminal (which can be customized) and wait for the user to press a key. In addition to that, it will also become a NOP
+(no operation instruction) if the script is not run interactively.
+
+Example:
+
+```python
+import click
+click.pause()
+```
+
+## Launching Editors
+
+```{versionadded} 2.0
+```
+
+Click supports launching editors automatically through {func}`edit`. This is very useful for asking users for multi-line
+input. It will automatically open the user's defined editor or fall back to a sensible default. If the user closes the
+editor without saving, the return value will be `None`, otherwise the entered text.
+
+Example usage:
+
+```python
+import click
+
+def get_commit_message():
+    MARKER = '# Everything below is ignored\n'
+    message = click.edit('\n\n' + MARKER)
+    if message is not None:
+        return message.split(MARKER, 1)[0].rstrip('\n')
+```
+
+Alternatively, the function can also be used to launch editors for files by a specific filename. In this case, the
+return value is always `None`.
+
+Example usage:
+
+```python
+import click
+click.edit(filename='/etc/passwd')
+```
+
+## Launching Applications
+
+```{versionadded} 2.0
+```
+
+Click supports launching applications through {func}`launch`. This can be used to open the default application
+associated with a URL or filetype. This can be used to launch web browsers or picture viewers, for instance. In addition
+to this, it can also launch the file manager and automatically select the provided file.
+
+Example usage:
+
+```python
+click.launch("https://click.palletsprojects.com/")
+click.launch("/my/downloaded/file.txt", locate=True)
+```
+
+## Printing Filenames
+
+Because filenames might not be Unicode, formatting them can be a bit tricky.
+
+The way this works with click is through the {func}`format_filename` function. It does a best-effort conversion of the
+filename to Unicode and will never fail. This makes it possible to use these filenames in the context of a full Unicode
+string.
+
+Example:
+
+```python
+click.echo(f"Path: {click.format_filename(b'foo.txt')}")
+```
+
+## Standard Streams
+
+For command line utilities, it's very important to get access to input and output streams reliably. Python generally
+provides access to these streams through `sys.stdout` and friends, but unfortunately, there are API differences between
+2.x and 3.x, especially with regards to how these streams respond to Unicode and binary data.
+
+Because of this, click provides the {func}`get_binary_stream` and {func}`get_text_stream` functions, which produce
+consistent results with different Python versions and for a wide variety of terminal configurations.
+
+The end result is that these functions will always return a functional stream object (except in very odd cases; see
+{doc}`/unicode-support`).
+
+Example:
+
+```python
+import click
+
+stdin_text = click.get_text_stream('stdin')
+stdout_binary = click.get_binary_stream('stdout')
+```
+
+```{versionadded} 6.0
+```
+
+Click now emulates output streams on Windows to support unicode to the Windows console through separate APIs. For more
+information see {doc}`wincmd`.
+
+## Intelligent File Opening
+
+```{versionadded} 3.0
+```
+
+Starting with Click 3.0 the logic for opening files from the {func}`File` type is exposed through the {func}`open_file`
+function. It can intelligently open stdin/stdout as well as any other file.
+
+Example:
+
+```python
+import click
+
+stdout = click.open_file('-', 'w')
+test_file = click.open_file('test.txt', 'w')
+```
+
+If stdin or stdout are returned, the return value is wrapped in a special file where the context manager will prevent
+the closing of the file. This makes the handling of standard streams transparent and you can always use it like this:
+
+```python
+with click.open_file(filename, 'w') as f:
+    f.write('Hello World!\n')
+```
+
+## Finding Application Folders
+
+```{versionadded} 2.0
+```
+
+Very often, you want to open a configuration file that belongs to your application. However, different operating systems
+store these configuration files in different locations depending on their standards. Click provides a
+{func}`get_app_dir` function which returns the most appropriate location for per-user config files for your application
+depending on the OS.
+
+Example usage:
+
+```python
+import os
+import click
+import ConfigParser
+
+APP_NAME = 'My Application'
+def read_config():
+cfg = os.path.join(click.get_app_dir(APP_NAME), 'config.ini')
+parser = ConfigParser.RawConfigParser()
+parser.read([cfg])
+rv = {}
+for section in parser.sections():
+    for key, value in parser.items(section):
+        rv[f"{section}.{key}"] = value
+return rv
+```
+
+## Showing Progress Bars
+
+Sometimes, you have command line scripts that need to process a lot of data, but you want to quickly show the user some
+progress about how long that will take. Click supports simple progress bar rendering for that through the
+{func}`progressbar` function.
+
+```{note} If you find that you have requirements beyond what Click's progress bar supports, try using [tqdm](https://tqdm.github.io/).
+```
+
+The basic usage is very simple: the idea is that you have an iterable that you want to operate on. For each item in the
+iterable it might take some time to do processing. So say you have a loop like this:
+
+```python
+for user in all_the_users_to_process:
+    modify_the_user(user)
+```
+
+To hook this up with an automatically updating progress bar, all you need to do is to change the code to this:
+
+```python
+import click
+
+with click.progressbar(all_the_users_to_process) as bar:
+    for user in bar:
+        modify_the_user(user)
+```
+
+Click will then automatically print a progress bar to the terminal and calculate the remaining time for you. The
+calculation of remaining time requires that the iterable has a length. If it does not have a length but you know the
+length, you can explicitly provide it:
+
+```python
+with click.progressbar(all_the_users_to_process,
+                       length=number_of_users) as bar:
+    for user in bar:
+        modify_the_user(user)
+```
+
+Note that {func}`progressbar` updates the bar *after* each iteration of the loop. So code like this will render
+correctly:
+
+```python
+import time
+
+with click.progressbar([1, 2, 3]) as bar:
+    for x in bar:
+        print(f"sleep({x})...")
+        time.sleep(x)
+```
+
+Another useful feature is to associate a label with the progress bar which will be shown preceding the progress bar:
+
+```python
+with click.progressbar(all_the_users_to_process,
+                       label='Modifying user accounts',
+                       length=number_of_users) as bar:
+    for user in bar:
+        modify_the_user(user)
+```
+
+Sometimes, one may need to iterate over an external iterator, and advance the progress bar irregularly. To do so, you
+need to specify the length (and no iterable), and use the update method on the context return value instead of iterating
+directly over it:
+
+```python
+with click.progressbar(length=total_size,
+                       label='Unzipping archive') as bar:
+    for archive in zip_file:
+        archive.extract()
+        bar.update(archive.size)
+```
diff --git a/docs/utils.rst b/docs/utils.rst
deleted file mode 100644
index 995c171..0000000
--- a/docs/utils.rst
+++ /dev/null
@@ -1,408 +0,0 @@
-Utilities
-=========
-
-.. currentmodule:: click
-
-Besides the functionality that Click provides to interface with argument
-parsing and handling, it also provides a bunch of addon functionality that
-is useful for writing command line utilities.
-
-
-Printing to Stdout
-------------------
-
-The most obvious helper is the :func:`echo` function, which in many ways
-works like the Python ``print`` statement or function.  The main difference is
-that it works the same in many different terminal environments.
-
-Example::
-
-    import click
-
-    click.echo('Hello World!')
-
-It can output both text and binary data. It will emit a trailing newline
-by default, which needs to be suppressed by passing ``nl=False``::
-
-    click.echo(b'\xe2\x98\x83', nl=False)
-
-Last but not least :func:`echo` uses click's intelligent internal output
-streams to stdout and stderr which support unicode output on the Windows
-console.  This means for as long as you are using `click.echo` you can
-output unicode characters (there are some limitations on the default font
-with regards to which characters can be displayed).
-
-.. versionadded:: 6.0
-
-Click emulates output streams on Windows to support unicode to the
-Windows console through separate APIs.  For more information see
-:doc:`wincmd`.
-
-.. versionadded:: 3.0
-
-You can also easily print to standard error by passing ``err=True``::
-
-    click.echo('Hello World!', err=True)
-
-
-.. _ansi-colors:
-
-ANSI Colors
------------
-
-.. versionadded:: 2.0
-
-The :func:`echo` function supports ANSI colors and styles. On Windows
-this uses `colorama`_.
-
-Primarily this means that:
-
--   Click's :func:`echo` function will automatically strip ANSI color codes
-    if the stream is not connected to a terminal.
--   the :func:`echo` function will transparently connect to the terminal on
-    Windows and translate ANSI codes to terminal API calls.  This means
-    that colors will work on Windows the same way they do on other
-    operating systems.
-
-On Windows, Click uses colorama without calling ``colorama.init()``. You
-can still call that in your code, but it's not required for Click.
-
-For styling a string, the :func:`style` function can be used::
-
-    import click
-
-    click.echo(click.style('Hello World!', fg='green'))
-    click.echo(click.style('Some more text', bg='blue', fg='white'))
-    click.echo(click.style('ATTENTION', blink=True, bold=True))
-
-The combination of :func:`echo` and :func:`style` is also available in
-a single function called :func:`secho`::
-
-    click.secho('Hello World!', fg='green')
-    click.secho('Some more text', bg='blue', fg='white')
-    click.secho('ATTENTION', blink=True, bold=True)
-
-
-.. _colorama: https://pypi.org/project/colorama/
-
-Pager Support
--------------
-
-In some situations, you might want to show long texts on the terminal and
-let a user scroll through it.  This can be achieved by using the
-:func:`echo_via_pager` function which works similarly to the :func:`echo`
-function, but always writes to stdout and, if possible, through a pager.
-
-Example:
-
-.. click:example::
-
-    @click.command()
-    def less():
-        click.echo_via_pager("\n".join(f"Line {idx}" for idx in range(200)))
-
-If you want to use the pager for a lot of text, especially if generating everything in advance would take a lot of time, you can pass a generator (or generator function) instead of a string:
-
-.. click:example::
-    def _generate_output():
-        for idx in range(50000):
-            yield f"Line {idx}\n"
-
-    @click.command()
-    def less():
-        click.echo_via_pager(_generate_output())
-
-
-Screen Clearing
----------------
-
-.. versionadded:: 2.0
-
-To clear the terminal screen, you can use the :func:`clear` function that
-is provided starting with Click 2.0.  It does what the name suggests: it
-clears the entire visible screen in a platform-agnostic way:
-
-::
-
-    import click
-    click.clear()
-
-
-Getting Characters from Terminal
---------------------------------
-
-.. versionadded:: 2.0
-
-Normally, when reading input from the terminal, you would read from
-standard input.  However, this is buffered input and will not show up until
-the line has been terminated.  In certain circumstances, you might not want
-to do that and instead read individual characters as they are being written.
-
-For this, Click provides the :func:`getchar` function which reads a single
-character from the terminal buffer and returns it as a Unicode character.
-
-Note that this function will always read from the terminal, even if stdin
-is instead a pipe.
-
-Example::
-
-    import click
-
-    click.echo('Continue? [yn] ', nl=False)
-    c = click.getchar()
-    click.echo()
-    if c == 'y':
-        click.echo('We will go on')
-    elif c == 'n':
-        click.echo('Abort!')
-    else:
-        click.echo('Invalid input :(')
-
-Note that this reads raw input, which means that things like arrow keys
-will show up in the platform's native escape format.  The only characters
-translated are ``^C`` and ``^D`` which are converted into keyboard
-interrupts and end of file exceptions respectively.  This is done because
-otherwise, it's too easy to forget about that and to create scripts that
-cannot be properly exited.
-
-
-Waiting for Key Press
----------------------
-
-.. versionadded:: 2.0
-
-Sometimes, it's useful to pause until the user presses any key on the
-keyboard.  This is especially useful on Windows where ``cmd.exe`` will
-close the window at the end of the command execution by default, instead
-of waiting.
-
-In click, this can be accomplished with the :func:`pause` function.  This
-function will print a quick message to the terminal (which can be
-customized) and wait for the user to press a key.  In addition to that,
-it will also become a NOP (no operation instruction) if the script is not
-run interactively.
-
-Example::
-
-    import click
-    click.pause()
-
-
-Launching Editors
------------------
-
-.. versionadded:: 2.0
-
-Click supports launching editors automatically through :func:`edit`.  This
-is very useful for asking users for multi-line input.  It will
-automatically open the user's defined editor or fall back to a sensible
-default.  If the user closes the editor without saving, the return value
-will be ``None``, otherwise the entered text.
-
-Example usage::
-
-    import click
-
-    def get_commit_message():
-        MARKER = '# Everything below is ignored\n'
-        message = click.edit('\n\n' + MARKER)
-        if message is not None:
-            return message.split(MARKER, 1)[0].rstrip('\n')
-
-Alternatively, the function can also be used to launch editors for files by
-a specific filename.  In this case, the return value is always `None`.
-
-Example usage::
-
-    import click
-    click.edit(filename='/etc/passwd')
-
-
-Launching Applications
-----------------------
-
-.. versionadded:: 2.0
-
-Click supports launching applications through :func:`launch`.  This can be
-used to open the default application associated with a URL or filetype.
-This can be used to launch web browsers or picture viewers, for instance.
-In addition to this, it can also launch the file manager and automatically
-select the provided file.
-
-Example usage::
-
-    click.launch("https://click.palletsprojects.com/")
-    click.launch("/my/downloaded/file.txt", locate=True)
-
-
-Printing Filenames
-------------------
-
-Because filenames might not be Unicode, formatting them can be a bit
-tricky.
-
-The way this works with click is through the :func:`format_filename`
-function.  It does a best-effort conversion of the filename to Unicode and
-will never fail.  This makes it possible to use these filenames in the
-context of a full Unicode string.
-
-Example::
-
-    click.echo(f"Path: {click.format_filename(b'foo.txt')}")
-
-
-Standard Streams
-----------------
-
-For command line utilities, it's very important to get access to input and
-output streams reliably.  Python generally provides access to these
-streams through ``sys.stdout`` and friends, but unfortunately, there are
-API differences between 2.x and 3.x, especially with regards to how these
-streams respond to Unicode and binary data.
-
-Because of this, click provides the :func:`get_binary_stream` and
-:func:`get_text_stream` functions, which produce consistent results with
-different Python versions and for a wide variety of terminal configurations.
-
-The end result is that these functions will always return a functional
-stream object (except in very odd cases; see :doc:`/unicode-support`).
-
-Example::
-
-    import click
-
-    stdin_text = click.get_text_stream('stdin')
-    stdout_binary = click.get_binary_stream('stdout')
-
-.. versionadded:: 6.0
-
-Click now emulates output streams on Windows to support unicode to the
-Windows console through separate APIs.  For more information see
-:doc:`wincmd`.
-
-
-Intelligent File Opening
-------------------------
-
-.. versionadded:: 3.0
-
-Starting with Click 3.0 the logic for opening files from the :class:`File`
-type is exposed through the :func:`open_file` function.  It can
-intelligently open stdin/stdout as well as any other file.
-
-Example::
-
-    import click
-
-    stdout = click.open_file('-', 'w')
-    test_file = click.open_file('test.txt', 'w')
-
-If stdin or stdout are returned, the return value is wrapped in a special
-file where the context manager will prevent the closing of the file.  This
-makes the handling of standard streams transparent and you can always use
-it like this::
-
-    with click.open_file(filename, 'w') as f:
-        f.write('Hello World!\n')
-
-
-Finding Application Folders
----------------------------
-
-.. versionadded:: 2.0
-
-Very often, you want to open a configuration file that belongs to your
-application.  However, different operating systems store these configuration
-files in different locations depending on their standards.  Click provides
-a :func:`get_app_dir` function which returns the most appropriate location
-for per-user config files for your application depending on the OS.
-
-Example usage::
-
-    import os
-    import click
-    import ConfigParser
-
-    APP_NAME = 'My Application'
-
-    def read_config():
-        cfg = os.path.join(click.get_app_dir(APP_NAME), 'config.ini')
-        parser = ConfigParser.RawConfigParser()
-        parser.read([cfg])
-        rv = {}
-        for section in parser.sections():
-            for key, value in parser.items(section):
-                rv[f"{section}.{key}"] = value
-        return rv
-
-
-Showing Progress Bars
----------------------
-
-Sometimes, you have command line scripts that need to process a lot of data,
-but you want to quickly show the user some progress about how long that
-will take.  Click supports simple progress bar rendering for that through
-the :func:`progressbar` function.
-
-.. note::
-
-    If you find that you have requirements beyond what Click's progress
-    bar supports, try using `tqdm`_.
-
-    .. _tqdm: https://tqdm.github.io/
-
-The basic usage is very simple: the idea is that you have an iterable that
-you want to operate on.  For each item in the iterable it might take some
-time to do processing.  So say you have a loop like this::
-
-    for user in all_the_users_to_process:
-        modify_the_user(user)
-
-To hook this up with an automatically updating progress bar, all you need
-to do is to change the code to this::
-
-    import click
-
-    with click.progressbar(all_the_users_to_process) as bar:
-        for user in bar:
-            modify_the_user(user)
-
-Click will then automatically print a progress bar to the terminal and
-calculate the remaining time for you.  The calculation of remaining time
-requires that the iterable has a length.  If it does not have a length
-but you know the length, you can explicitly provide it::
-
-    with click.progressbar(all_the_users_to_process,
-                           length=number_of_users) as bar:
-        for user in bar:
-            modify_the_user(user)
-
-Note that :func:`progressbar` updates the bar *after* each iteration of the
-loop. So code like this will render correctly::
-
-    import time
-
-    with click.progressbar([1, 2, 3]) as bar:
-        for x in bar:
-            print(f"sleep({x})...")
-            time.sleep(x)
-
-Another useful feature is to associate a label with the progress bar which
-will be shown preceding the progress bar::
-
-    with click.progressbar(all_the_users_to_process,
-                           label='Modifying user accounts',
-                           length=number_of_users) as bar:
-        for user in bar:
-            modify_the_user(user)
-
-Sometimes, one may need to iterate over an external iterator, and advance the
-progress bar irregularly. To do so, you need to specify the length (and no
-iterable), and use the update method on the context return value instead of
-iterating directly over it::
-
-    with click.progressbar(length=total_size,
-                           label='Unzipping archive') as bar:
-        for archive in zip_file:
-            archive.extract()
-            bar.update(archive.size)
diff --git a/src/click/core.py b/src/click/core.py
index f57ada6..445460b 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -2554,7 +2554,6 @@ class Option(Parameter):
         if help:
             help = inspect.cleandoc(help)
 
-        default_is_missing = "default" not in attrs
         super().__init__(
             param_decls, type=type, multiple=multiple, deprecated=deprecated, **attrs
         )
@@ -2588,15 +2587,15 @@ class Option(Parameter):
         self._flag_needs_value = self.prompt is not None and not self.prompt_required
 
         if is_flag is None:
+            # Implicitly a flag because flag_value was set.
             if flag_value is not None:
-                # Implicitly a flag because flag_value was set.
                 is_flag = True
+            # Not a flag, but when used as a flag it shows a prompt.
             elif self._flag_needs_value:
-                # Not a flag, but when used as a flag it shows a prompt.
                 is_flag = False
-            else:
-                # Implicitly a flag because flag options were given.
-                is_flag = bool(self.secondary_opts)
+            # Implicitly a flag because flag options were given.
+            elif self.secondary_opts:
+                is_flag = True
         elif is_flag is False and not self._flag_needs_value:
             # Not a flag, and prompt is not enabled, can be used as a
             # flag if flag_value is set.
@@ -2604,14 +2603,19 @@ class Option(Parameter):
 
         self.default: t.Any | t.Callable[[], t.Any]
 
-        if is_flag and default_is_missing and not self.required:
-            if multiple:
-                self.default = ()
-            else:
-                self.default = False
+        if is_flag:
+            # Set missing default for flags if not explicitly required or prompted.
+            if self.default is None and not self.required and not self.prompt:
+                if multiple:
+                    self.default = ()
+                else:
+                    self.default = False
 
-        if is_flag and flag_value is None:
-            flag_value = not self.default
+            if flag_value is None:
+                # A boolean flag presence in the command line is enough to set
+                # the value: to the default if it is not blank, or to True
+                # otherwise.
+                flag_value = self.default if self.default else True
 
         self.type: types.ParamType
         if is_flag and type is None:
@@ -2619,8 +2623,10 @@ class Option(Parameter):
             # default.
             self.type = types.convert_type(None, flag_value)
 
-        self.is_flag: bool = is_flag
-        self.is_bool_flag: bool = is_flag and isinstance(self.type, types.BoolParamType)
+        self.is_flag: bool = bool(is_flag)
+        self.is_bool_flag: bool = bool(
+            is_flag and isinstance(self.type, types.BoolParamType)
+        )
         self.flag_value: t.Any = flag_value
 
         # Counting
@@ -2628,7 +2634,7 @@ class Option(Parameter):
         if count:
             if type is None:
                 self.type = types.IntRange(min=0)
-            if default_is_missing:
+            if self.default is None:
                 self.default = 0
 
         self.allow_from_autoenv = allow_from_autoenv
@@ -2918,8 +2924,8 @@ class Option(Parameter):
         # value as default.
         if self.is_flag and not self.is_bool_flag:
             for param in ctx.command.params:
-                if param.name == self.name and param.default:
-                    return t.cast(Option, param).flag_value
+                if param.name == self.name and param.default is not None:
+                    return t.cast(Option, param).default
 
             return None
 
@@ -2959,11 +2965,21 @@ class Option(Parameter):
         )
 
     def resolve_envvar_value(self, ctx: Context) -> str | None:
+        """Find which environment variable to read for this option and return
+        its value.
+
+        Returns the value of the environment variable if it exists, or ``None``
+        if it does not.
+
+        .. caution::
+
+            The raw value extracted from the environment is not normalized and
+            is returned as-is. Any normalization or reconciation with the
+            option's type should happen later.
+        """
         rv = super().resolve_envvar_value(ctx)
 
         if rv is not None:
-            if self.is_flag and self.flag_value:
-                return str(self.flag_value)
             return rv
 
         if (
@@ -2980,18 +2996,32 @@ class Option(Parameter):
         return None
 
     def value_from_envvar(self, ctx: Context) -> t.Any | None:
-        rv: t.Any | None = self.resolve_envvar_value(ctx)
+        """Normalize the value from the environment variable, if it exists."""
+        rv: str | None = self.resolve_envvar_value(ctx)
 
         if rv is None:
             return None
 
+        # Non-boolean flags are more liberal in what they accept. But a flag being a
+        # flag, its envvar value still needs to analyzed to determine if the flag is
+        # activated or not.
+        if self.is_flag and not self.is_bool_flag:
+            # If the flag_value is set and match the envvar value, return it
+            # directly.
+            if self.flag_value is not None and rv == self.flag_value:
+                return self.flag_value
+            # Analyze the envvar value as a boolean to know if the flag is
+            # activated or not.
+            return types.BoolParamType.str_to_bool(rv)
+
+        # Split the envvar value if it is allowed to be repeated.
         value_depth = (self.nargs != 1) + bool(self.multiple)
-
         if value_depth > 0:
-            rv = self.type.split_envvar_value(rv)
-
+            multi_rv = self.type.split_envvar_value(rv)
             if self.multiple and self.nargs != 1:
-                rv = batch(rv, self.nargs)
+                multi_rv = batch(multi_rv, self.nargs)  # type: ignore[assignment]
+
+            return multi_rv
 
         return rv
 
@@ -3011,6 +3041,17 @@ class Option(Parameter):
                 value = self.flag_value
                 source = ParameterSource.COMMANDLINE
 
+        # A flag which is activated and has a flag_value set, should returns
+        # the latter, unless the value comes from the explicitly sets default.
+        elif (
+            self.is_flag
+            and value is True
+            and not self.is_bool_flag
+            and self.flag_value is not None
+            and source is not ParameterSource.DEFAULT
+        ):
+            value = self.flag_value
+
         elif (
             self.multiple
             and value is not None
diff --git a/src/click/types.py b/src/click/types.py
index 684cb3b..e71c1c2 100644
--- a/src/click/types.py
+++ b/src/click/types.py
@@ -661,23 +661,67 @@ class FloatRange(_NumberRangeBase, FloatParamType):
 class BoolParamType(ParamType):
     name = "boolean"
 
-    def convert(
-        self, value: t.Any, param: Parameter | None, ctx: Context | None
-    ) -> t.Any:
-        if value in {False, True}:
-            return bool(value)
+    bool_states: dict[str, bool] = {
+        "1": True,
+        "0": False,
+        "yes": True,
+        "no": False,
+        "true": True,
+        "false": False,
+        "on": True,
+        "off": False,
+        "t": True,
+        "f": False,
+        "y": True,
+        "n": False,
+        # Absence of value is considered False.
+        "": False,
+    }
+    """A mapping of string values to boolean states.
+
+    Mapping is inspired by :py:attr:`configparser.ConfigParser.BOOLEAN_STATES`
+    and extends it.
+
+    .. caution::
+        String values are lower-cased, as the ``str_to_bool`` comparison function
+        below is case-insensitive.
+
+    .. warning::
+        The mapping is not exhaustive, and does not cover all possible boolean strings
+        representations. It will remains as it is to avoid endless bikeshedding.
+
+        Future work my be considered to make this mapping user-configurable from public
+        API.
+    """
 
-        norm = value.strip().lower()
+    @staticmethod
+    def str_to_bool(value: str | bool) -> bool | None:
+        """Convert a string to a boolean value.
 
-        if norm in {"1", "true", "t", "yes", "y", "on"}:
-            return True
+        If the value is already a boolean, it is returned as-is. If the value is a
+        string, it is stripped of whitespaces and lower-cased, then checked against
+        the known boolean states pre-defined in the `BoolParamType.bool_states` mapping
+        above.
 
-        if norm in {"0", "false", "f", "no", "n", "off"}:
-            return False
+        Returns `None` if the value does not match any known boolean state.
+        """
+        if isinstance(value, bool):
+            return value
+        return BoolParamType.bool_states.get(value.strip().lower())
 
-        self.fail(
-            _("{value!r} is not a valid boolean.").format(value=value), param, ctx
-        )
+    def convert(
+        self, value: t.Any, param: Parameter | None, ctx: Context | None
+    ) -> bool:
+        normalized = self.str_to_bool(value)
+        if normalized is None:
+            self.fail(
+                _(
+                    "{value!r} is not a valid boolean. Recognized values: {states}"
+                ).format(value=value, states=", ".join(sorted(self.bool_states))),
+                param,
+                ctx,
+            )
+        return normalized
 
     def __repr__(self) -> str:
         return "BOOL"


Test patch:
diff --git a/tests/test_arguments.py b/tests/test_arguments.py
index 50db8e6..53ab8d1 100644
--- a/tests/test_arguments.py
+++ b/tests/test_arguments.py
@@ -198,19 +198,6 @@ def test_nargs_envvar(runner, nargs, value, expect):
         assert result.return_value == expect
 
 
-def test_envvar_flag_value(runner):
-    @click.command()
-    # is_flag is implicitly true
-    @click.option("--upper", flag_value="upper", envvar="UPPER")
-    def cmd(upper):
-        click.echo(upper)
-        return upper
-
-    # For whatever value of the `env` variable, if it exists, the flag should be `upper`
-    result = runner.invoke(cmd, env={"UPPER": "whatever"})
-    assert result.output.strip() == "upper"
-
-
 def test_nargs_envvar_only_if_values_empty(runner):
     @click.command()
     @click.argument("arg", envvar="X", nargs=-1)
diff --git a/tests/test_basic.py b/tests/test_basic.py
index b84ae73..d773c37 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -222,17 +222,21 @@ def test_boolean_switch(runner, default, args, expect):
     assert result.return_value is expect
 
 
-@pytest.mark.parametrize("default", [True, False])
-@pytest.mark.parametrize(("args", "expect"), [(["--f"], True), ([], False)])
+@pytest.mark.parametrize(
+    ("default", "args", "expect"),
+    (
+        (True, ["--f"], True),
+        (True, [], True),
+        (False, ["--f"], True),
+        (False, [], False),
+    ),
+)
 def test_boolean_flag(runner, default, args, expect):
     @click.command()
     @click.option("--f", is_flag=True, default=default)
     def cli(f):
         return f
 
-    if default:
-        expect = not expect
-
     result = runner.invoke(cli, args, standalone_mode=False)
     assert result.return_value is expect
 
diff --git a/tests/test_imports.py b/tests/test_imports.py
index e5e5119..6b44c39 100644
--- a/tests/test_imports.py
+++ b/tests/test_imports.py
@@ -27,29 +27,30 @@ click.echo(json.dumps(rv))
 
 ALLOWED_IMPORTS = {
     "__future__",
-    "weakref",
-    "os",
-    "struct",
+    "codecs",
     "collections",
     "collections.abc",
-    "sys",
+    "configparser",
     "contextlib",
+    "datetime",
+    "enum",
+    "errno",
+    "fcntl",
     "functools",
-    "stat",
-    "re",
-    "codecs",
+    "gettext",
     "inspect",
-    "itertools",
     "io",
+    "itertools",
+    "os",
+    "re",
+    "shutil",
+    "stat",
+    "struct",
+    "sys",
     "threading",
-    "errno",
-    "fcntl",
-    "datetime",
-    "enum",
-    "typing",
     "types",
-    "gettext",
-    "shutil",
+    "typing",
+    "weakref",
 }
 
 if WIN:
diff --git a/tests/test_options.py b/tests/test_options.py
index 5c30418..f501738 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -247,15 +247,95 @@ def test_multiple_envvar(runner):
     assert result.output == "foo|bar\n"
 
 
-def test_trailing_blanks_boolean_envvar(runner):
+@pytest.mark.parametrize(
+    ("name", "value", "expected"),
+    (
+        # Lower-cased variations of value.
+        ("SHOUT", "true", True),
+        ("SHOUT", "false", False),
+        # Title-cased variations of value.
+        ("SHOUT", "True", True),
+        ("SHOUT", "False", False),
+        # Upper-cased variations of value.
+        ("SHOUT", "TRUE", True),
+        ("SHOUT", "FALSE", False),
+        # Random-cased variations of value.
+        ("SHOUT", "TruE", True),
+        ("SHOUT", "truE", True),
+        ("SHOUT", "FaLsE", False),
+        ("SHOUT", "falsE", False),
+        # Extra spaces around the value.
+        ("SHOUT", "true ", True),
+        ("SHOUT", " true", True),
+        ("SHOUT", " true ", True),
+        ("SHOUT", "false ", False),
+        ("SHOUT", " false", False),
+        ("SHOUT", " false ", False),
+        # Integer variations.
+        ("SHOUT", "1", True),
+        ("SHOUT", "0", False),
+        # Alternative states.
+        ("SHOUT", "t", True),
+        ("SHOUT", "f", False),
+        ("SHOUT", "y", True),
+        ("SHOUT", "n", False),
+        ("SHOUT", "yes", True),
+        ("SHOUT", "no", False),
+        ("SHOUT", "on", True),
+        ("SHOUT", "off", False),
+        # Blank value variations.
+        ("SHOUT", None, False),
+        ("SHOUT", "", False),
+        ("SHOUT", " ", False),
+        ("SHOUT", "       ", False),
+        # Variable names are not stripped of spaces and so don't match the
+        # flag, which then naturraly takes its default value.
+        ("SHOUT  ", "True", False),
+        ("SHOUT  ", "False", False),
+        ("  SHOUT  ", "True", False),
+        ("  SHOUT  ", "False", False),
+        # Same for random and reverse environment variable names: they are not
+        # recognized by the option.
+        ("RANDOM", "True", False),
+        ("NO_SHOUT", "True", False),
+        ("NO_SHOUT", "False", False),
+    ),
+)
+def test_boolean_envvar_normalization(runner, name, value, expected):
     @click.command()
     @click.option("--shout/--no-shout", envvar="SHOUT")
     def cli(shout):
         click.echo(f"shout: {shout!r}")
 
-    result = runner.invoke(cli, [], env={"SHOUT": " true "})
+    result = runner.invoke(cli, [], env={name: value})
     assert result.exit_code == 0
-    assert result.output == "shout: True\n"
+    assert result.output == f"shout: {expected}\n"
+
+
+@pytest.mark.parametrize(
+    ("name", "value"),
+    (
+        ("SHOUT", "tr ue"),
+        ("SHOUT", "10"),
+        ("SHOUT", "01"),
+        ("SHOUT", "00"),
+        ("SHOUT", "11"),
+        ("SHOUT", "randomstring"),
+        ("SHOUT", "None"),
+    ),
+)
+def test_boolean_envvar_bad_values(runner, name, value):
+    @click.command()
+    @click.option("--shout/--no-shout", envvar="SHOUT")
+    def cli(shout):
+        click.echo(f"shout: {shout!r}")
+
+    result = runner.invoke(cli, [], env={name: value})
+    assert result.exit_code == 2
+    assert (
+        f"Invalid value for '--shout': {value!r} is not a valid boolean."
+        in result.output
+    )
 
 
 def test_multiple_default_help(runner):
@@ -1008,28 +1088,169 @@ def test_type_from_flag_value():
 
 
 @pytest.mark.parametrize(
-    ("opts", "pass_flag", "expected"),
+    ("opt_params", "pass_flag", "expected"),
     [
-        pytest.param({"type": bool}, False, "False"),
-        pytest.param({"type": bool}, True, "True"),
-        pytest.param({"type": bool, "default": True}, False, "True"),
-        pytest.param({"type": bool, "default": True}, True, "False"),
-        pytest.param({"type": click.BOOL}, False, "False"),
-        pytest.param({"type": click.BOOL}, True, "True"),
-        pytest.param({"type": click.BOOL, "default": True}, False, "True"),
-        pytest.param({"type": click.BOOL, "default": True}, True, "False"),
-        pytest.param({"type": str}, False, ""),
-        pytest.param({"type": str}, True, "True"),
+        # Effect of the presence/absence of flag depending on type
+        ({"type": bool}, False, False),
+        ({"type": bool}, True, True),
+        ({"type": click.BOOL}, False, False),
+        ({"type": click.BOOL}, True, True),
+        ({"type": str}, False, "False"),
+        ({"type": str}, True, "True"),
+        # Effects of default value
+        ({"type": bool, "default": True}, False, True),
+        ({"type": bool, "default": True}, True, True),
+        ({"type": bool, "default": False}, False, False),
+        ({"type": bool, "default": False}, True, True),
+        ({"type": bool, "default": None}, False, False),
+        ({"type": bool, "default": None}, True, True),
+        ({"type": click.BOOL, "default": True}, False, True),
+        ({"type": click.BOOL, "default": True}, True, True),
+        ({"type": click.BOOL, "default": False}, False, False),
+        ({"type": click.BOOL, "default": False}, True, True),
+        ({"type": click.BOOL, "default": None}, False, False),
+        ({"type": click.BOOL, "default": None}, True, True),
+        ({"type": str, "default": True}, False, "True"),
+        ({"type": str, "default": True}, True, "True"),
+        ({"type": str, "default": False}, False, "False"),
+        ({"type": str, "default": False}, True, "True"),
+        ({"type": str, "default": "foo"}, False, "foo"),
+        ({"type": str, "default": "foo"}, True, "foo"),
+        ({"type": str, "default": None}, False, "False"),
+        ({"type": str, "default": None}, True, "True"),
+        # Effects of flag_value
+        ({"type": bool, "flag_value": True}, False, False),
+        ({"type": bool, "flag_value": True}, True, True),
+        ({"type": bool, "flag_value": False}, False, False),
+        ({"type": bool, "flag_value": False}, True, False),
+        ({"type": bool, "flag_value": None}, False, False),
+        ({"type": bool, "flag_value": None}, True, True),
+        ({"type": click.BOOL, "flag_value": True}, False, False),
+        ({"type": click.BOOL, "flag_value": True}, True, True),
+        ({"type": click.BOOL, "flag_value": False}, False, False),
+        ({"type": click.BOOL, "flag_value": False}, True, False),
+        ({"type": click.BOOL, "flag_value": None}, False, False),
+        ({"type": click.BOOL, "flag_value": None}, True, True),
+        ({"type": str, "flag_value": True}, False, "False"),
+        ({"type": str, "flag_value": True}, True, "True"),
+        ({"type": str, "flag_value": False}, False, "False"),
+        ({"type": str, "flag_value": False}, True, "False"),
+        ({"type": str, "flag_value": "foo"}, False, "False"),
+        ({"type": str, "flag_value": "foo"}, True, "foo"),
+        ({"type": str, "flag_value": None}, False, "False"),
+        ({"type": str, "flag_value": None}, True, "True"),
+        # Not passing --foo returns the default value as-is
+        ({"type": bool, "default": True, "flag_value": True}, False, True),
+        ({"type": bool, "default": True, "flag_value": False}, False, True),
+        ({"type": bool, "default": False, "flag_value": True}, False, False),
+        ({"type": bool, "default": False, "flag_value": False}, False, False),
+        ({"type": bool, "default": None, "flag_value": True}, False, False),
+        ({"type": bool, "default": None, "flag_value": False}, False, False),
+        ({"type": str, "default": True, "flag_value": True}, False, "True"),
+        ({"type": str, "default": True, "flag_value": False}, False, "True"),
+        ({"type": str, "default": False, "flag_value": True}, False, "False"),
+        ({"type": str, "default": False, "flag_value": False}, False, "False"),
+        ({"type": str, "default": "foo", "flag_value": True}, False, "foo"),
+        ({"type": str, "default": "foo", "flag_value": False}, False, "foo"),
+        ({"type": str, "default": "foo", "flag_value": "bar"}, False, "foo"),
+        ({"type": str, "default": "foo", "flag_value": None}, False, "foo"),
+        ({"type": str, "default": None, "flag_value": True}, False, "False"),
+        ({"type": str, "default": None, "flag_value": False}, False, "False"),
+        # Passing --foo returns the explicitly set flag_value
+        ({"type": bool, "default": True, "flag_value": True}, True, True),
+        ({"type": bool, "default": True, "flag_value": False}, True, False),
+        ({"type": bool, "default": False, "flag_value": True}, True, True),
+        ({"type": bool, "default": False, "flag_value": False}, True, False),
+        ({"type": bool, "default": None, "flag_value": True}, True, True),
+        ({"type": bool, "default": None, "flag_value": False}, True, False),
+        ({"type": str, "default": True, "flag_value": True}, True, "True"),
+        ({"type": str, "default": True, "flag_value": False}, True, "False"),
+        ({"type": str, "default": False, "flag_value": True}, True, "True"),
+        ({"type": str, "default": False, "flag_value": False}, True, "False"),
+        ({"type": str, "default": "foo", "flag_value": True}, True, "True"),
+        ({"type": str, "default": "foo", "flag_value": False}, True, "False"),
+        ({"type": str, "default": "foo", "flag_value": "bar"}, True, "bar"),
+        ({"type": str, "default": "foo", "flag_value": None}, True, "foo"),
+        ({"type": str, "default": None, "flag_value": True}, True, "True"),
+        ({"type": str, "default": None, "flag_value": False}, True, "False"),
     ],
 )
-def test_flag_value_is_correctly_set(runner, opts, pass_flag, expected):
+def test_flag_value_and_default(runner, opt_params, pass_flag, expected):
+    @click.command()
+    @click.option("--foo", is_flag=True, **opt_params)
+    def cmd(foo):
+        click.echo(repr(foo))
+
+    result = runner.invoke(cmd, ["--foo"] if pass_flag else [])
+    assert result.output == f"{expected!r}\n"
+
+
+@pytest.mark.parametrize(
+    "opts",
+    [
+        {"type": bool, "default": "foo"},
+        {"type": bool, "flag_value": "foo"},
+        {"type": click.BOOL, "default": "foo"},
+        {"type": click.BOOL, "flag_value": "foo"},
+    ],
+)
+def test_invalid_flag_definition(runner, opts):
     @click.command()
     @click.option("--foo", is_flag=True, **opts)
     def cmd(foo):
         click.echo(foo)
 
-    result = runner.invoke(cmd, ["--foo"] if pass_flag else [])
-    assert result.output == f"{expected}\n"
+    result = runner.invoke(cmd, ["--foo"])
+    assert (
+        "Error: Invalid value for '--foo': 'foo' is not a valid boolean"
+        in result.output
+    )
+
+
+@pytest.mark.parametrize(
+    ("flag_value", "envvar_value", "expected"),
+    [
+        # Envvar is set to false, so the flag default value is returned.
+        ("bar", "False", "False"),
+        # Same as above with alternative states and blank values.
+        ("bar", "0", "False"),
+        ("bar", "f", "False"),
+        ("bar", "", "False"),
+        # Envvar is set to true, so the flag_value is returned.
+        ("bar", "True", "bar"),
+        ("bar", "1", "bar"),
+        ("bar", "t", "bar"),
+        # So far we have the same cases as the test_flag_value_and_default
+        # test case. Now instead of passing a boolean-like value, let's use
+        # the flag_value as the envvar value.
+        ("bar", "bar", "bar"),
+        # flag_value is expected to be the exact same in the envvar for the flag to be
+        # activated.
+        ("bar", " bar ", "False"),
+        ("bar", "BAR", "False"),
+        ("bar", "random", "False"),
+        ("bar", "bar random", "False"),
+        ("bar", "random bar", "False"),
+        ("BAR", "BAR", "BAR"),
+        ("BAR", "bar", "False"),
+        (" bar ", " bar ", " bar "),
+        (" bar ", "bar", "False"),
+        (" bar ", "BAR", "False"),
+    ],
+)
+def test_envvar_flag_value(runner, flag_value, envvar_value, expected):
+    """Ensure that flag_value is recognized by the envvar."""
+
+    @click.command()
+    @click.option(
+        "--upper", type=str, is_flag=True, flag_value=flag_value, envvar="UPPER"
+    )
+    def cmd(upper):
+        click.echo(repr(upper))
+
+    result = runner.invoke(cmd, env={"UPPER": envvar_value})
+    assert result.exit_code == 0
+    assert result.output == f"{expected!r}\n"
 
 
 @pytest.mark.parametrize(
@@ -1047,7 +1268,7 @@ def test_flag_value_is_correctly_set(runner, opts, pass_flag, expected):
         pytest.param(Option(["-a"], flag_value=True), True, id="bool flag_value"),
     ],
 )
-def test_is_bool_flag_is_correctly_set(option, expected):
+def test_bool_flag_auto_detection(option, expected):
     assert option.is_bool_flag is expected
 
 
diff --git a/tests/test_types.py b/tests/test_types.py
index 75434f1..c287e37 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -226,12 +226,7 @@ def test_path_surrogates(tmp_path, monkeypatch):
 def test_file_surrogates(type, tmp_path):
     path = tmp_path / "\udcff"
 
-    # - common case: �': No such file or directory
-    # - special case: Illegal byte sequence
-    # The spacial case is seen with rootless Podman. The root cause is most
-    # likely that the path is handled by a user-space program (FUSE).
-    match = r"(�': No such file or directory|Illegal byte sequence)"
-    with pytest.raises(click.BadParameter, match=match):
+    with pytest.raises(click.BadParameter, match="�': No such file or directory"):
         type.convert(path, None, None)