Repository: tox-dev/tox

Issue:
Unescaped } breaks expansions that follow it
Based on the below test it looks like escaping only the opening or only the closing bracket is allowed. I.e. `\{posargs}` is valid syntax and expands to literal `{posargs}`.
https://github.com/tox-dev/tox/blob/651e1ae9203a095f422c48c9673e60696f8650c1/tests/config/loader/ini/replace/test_replace_posargs.py#L45-L58

I have found a case where this syntax breaks expansions that follow it:
```
\{posargs} {posargs}
```

# To reproduce
## tox.ini
```ini
[tox]
no_package = True

[testenv]
commands = python -c 'import sys; print(sys.argv)' \{posargs} {posargs}
```
## Run
```
tox4 -- foo
```
## Expected output:
```
['-c', '{posargs}', 'foo']
```
## Actual output:
```
['-c', '{posargs}', '{posargs}']
```
## tox4 -rvv -- foo
<details>
<summary> Collapsed for brevity </summary>

```
ROOT: 407 D setup logging to DEBUG on pid 15093 [tox/report.py:211]
ROOT: 417 W remove tox env folder /home/user/hello-tox/.tox/4/py [tox/tox_env/api.py:202]
ROOT: 533 I find interpreter for spec PythonSpec(implementation=CPython) [virtualenv/discovery/builtin.py:62]
ROOT: 533 D discover exe for PythonInfo(spec=CPython3.9.1.final.0-64, exe=/home/user/path/to/tox4-venv/bin/python, platform=linux, version='3.9.1 (default, Dec 11 2020, 14:32:07) \n[GCC 7.3.0]', encoding_fs_io=utf-8-utf-8) in /home/user/.conda/envs/py3 [virtualenv/discovery/py_info.py:372]
ROOT: 535 D filesystem is case-sensitive [virtualenv/info.py:28]
ROOT: 537 D Attempting to acquire lock 135013968838864 on /home/user/.local/share/virtualenv/py_info/1/32916b286f4a70af59e238bfd51ed56d548abf51ddd087759ce7be39e4509643.lock [filelock.py:270]
ROOT: 537 D Lock 135013968838864 acquired on /home/user/.local/share/virtualenv/py_info/1/32916b286f4a70af59e238bfd51ed56d548abf51ddd087759ce7be39e4509643.lock [filelock.py:274]
ROOT: 538 D got python info of /home/user/.conda/envs/py3/bin/python3.9 from /home/user/.local/share/virtualenv/py_info/1/32916b286f4a70af59e238bfd51ed56d548abf51ddd087759ce7be39e4509643.json [virtualenv/app_data/via_disk_folder.py:135]
ROOT: 540 D Attempting to release lock 135013968838864 on /home/user/.local/share/virtualenv/py_info/1/32916b286f4a70af59e238bfd51ed56d548abf51ddd087759ce7be39e4509643.lock [filelock.py:315]
ROOT: 540 D Lock 135013968838864 released on /home/user/.local/share/virtualenv/py_info/1/32916b286f4a70af59e238bfd51ed56d548abf51ddd087759ce7be39e4509643.lock [filelock.py:318]
ROOT: 540 I proposed PythonInfo(spec=CPython3.9.1.final.0-64, system=/home/user/.conda/envs/py3/bin/python3.9, exe=/home/user/path/to/tox4-venv/bin/python, platform=linux, version='3.9.1 (default, Dec 11 2020, 14:32:07) \n[GCC 7.3.0]', encoding_fs_io=utf-8-utf-8) [virtualenv/discovery/builtin.py:69]
ROOT: 541 D accepted PythonInfo(spec=CPython3.9.1.final.0-64, system=/home/user/.conda/envs/py3/bin/python3.9, exe=/home/user/path/to/tox4-venv/bin/python, platform=linux, version='3.9.1 (default, Dec 11 2020, 14:32:07) \n[GCC 7.3.0]', encoding_fs_io=utf-8-utf-8) [virtualenv/discovery/builtin.py:71]
ROOT: 628 I create virtual environment via CPython3Posix(dest=/home/user/hello-tox/.tox/4/py, clear=True, no_vcs_ignore=False, global=False) [virtualenv/run/session.py:52]
ROOT: 628 D delete /home/user/hello-tox/.tox/4/py [virtualenv/create/creator.py:169]
ROOT: 629 D create folder /home/user/hello-tox/.tox/4/py/bin [virtualenv/util/path/_sync.py:25]
ROOT: 629 D create folder /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages [virtualenv/util/path/_sync.py:25]
ROOT: 630 D write /home/user/hello-tox/.tox/4/py/pyvenv.cfg [virtualenv/create/pyenv_cfg.py:34]
ROOT: 630 D 	home = /home/user/.conda/envs/py3 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 630 D 	implementation = CPython [virtualenv/create/pyenv_cfg.py:38]
ROOT: 630 D 	version_info = 3.9.1.final.0 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 630 D 	virtualenv = 20.4.2 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 630 D 	include-system-site-packages = false [virtualenv/create/pyenv_cfg.py:38]
ROOT: 630 D 	base-prefix = /home/user/.conda/envs/py3 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 630 D 	base-exec-prefix = /home/user/.conda/envs/py3 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 631 D 	base-executable = /home/user/.conda/envs/py3/bin/python3.9 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 631 D symlink /home/user/.conda/envs/py3/bin/python3.9 to /home/user/hello-tox/.tox/4/py/bin/python [virtualenv/util/path/_sync.py:44]
ROOT: 633 D create virtualenv import hook file /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/_virtualenv.pth [virtualenv/create/via_global_ref/api.py:95]
ROOT: 633 D create /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/_virtualenv.py [virtualenv/create/via_global_ref/api.py:98]
ROOT: 634 D ============================== target debug ============================== [virtualenv/run/session.py:54]
ROOT: 635 D debug via /home/user/hello-tox/.tox/4/py/bin/python /home/user/path/to/tox4-venv/lib/python3.9/site-packages/virtualenv/create/debug.py [virtualenv/create/creator.py:224]
ROOT: 634 D {
  "sys": {
    "executable": "/home/user/hello-tox/.tox/4/py/bin/python",
    "_base_executable": "/home/user/hello-tox/.tox/4/py/bin/python",
    "prefix": "/home/user/hello-tox/.tox/4/py",
    "base_prefix": "/home/user/.conda/envs/py3",
    "real_prefix": null,
    "exec_prefix": "/home/user/hello-tox/.tox/4/py",
    "base_exec_prefix": "/home/user/.conda/envs/py3",
    "path": [
      "/home/user/.conda/envs/py3/lib/python39.zip",
      "/home/user/.conda/envs/py3/lib/python3.9",
      "/home/user/.conda/envs/py3/lib/python3.9/lib-dynload",
      "/home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages"
    ],
    "meta_path": [
      "<class '_virtualenv._Finder'>",
      "<class '_frozen_importlib.BuiltinImporter'>",
      "<class '_frozen_importlib.FrozenImporter'>",
      "<class '_frozen_importlib_external.PathFinder'>"
    ],
    "fs_encoding": "utf-8",
    "io_encoding": "utf-8"
  },
  "version": "3.9.1 (default, Dec 11 2020, 14:32:07) \n[GCC 7.3.0]",
  "makefile_filename": "/home/user/.conda/envs/py3/lib/python3.9/config-3.9-x86_64-linux-gnu/Makefile",
  "os": "<module 'os' from '/home/user/.conda/envs/py3/lib/python3.9/os.py'>",
  "site": "<module 'site' from '/home/user/.conda/envs/py3/lib/python3.9/site.py'>",
  "datetime": "<module 'datetime' from '/home/user/.conda/envs/py3/lib/python3.9/datetime.py'>",
  "math": "<module 'math' from '/home/user/.conda/envs/py3/lib/python3.9/lib-dynload/math.cpython-39-x86_64-linux-gnu.so'>",
  "json": "<module 'json' from '/home/user/.conda/envs/py3/lib/python3.9/json/__init__.py'>"
} [virtualenv/run/session.py:55]
ROOT: 708 I add seed packages via FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/home/user/.local/share/virtualenv) [virtualenv/run/session.py:59]
ROOT: 719 D got embed update of distribution wheel from /home/user/.local/share/virtualenv/wheel/3.9/embed/1/wheel.json [virtualenv/app_data/via_disk_folder.py:135]
ROOT: 721 D got embed update of distribution setuptools from /home/user/.local/share/virtualenv/wheel/3.9/embed/1/setuptools.json [virtualenv/app_data/via_disk_folder.py:135]
ROOT: 722 D got embed update of distribution pip from /home/user/.local/share/virtualenv/wheel/3.9/embed/1/pip.json [virtualenv/app_data/via_disk_folder.py:135]
ROOT: 728 D using periodically updated wheel /home/user/.local/share/virtualenv/wheel/house/setuptools-53.0.0-py3-none-any.whl [virtualenv/seed/wheels/periodic_update.py:53]
ROOT: 729 D install wheel from wheel /home/user/path/to/tox4-venv/lib/python3.9/site-packages/virtualenv/seed/wheels/embed/wheel-0.36.2-py2.py3-none-any.whl via CopyPipInstall [virtualenv/seed/embed/via_app_data/via_app_data.py:49]
ROOT: 729 D install pip from wheel /home/user/path/to/tox4-venv/lib/python3.9/site-packages/virtualenv/seed/wheels/embed/pip-21.0.1-py3-none-any.whl via CopyPipInstall [virtualenv/seed/embed/via_app_data/via_app_data.py:49]
ROOT: 730 D install setuptools from wheel /home/user/.local/share/virtualenv/wheel/house/setuptools-53.0.0-py3-none-any.whl via CopyPipInstall [virtualenv/seed/embed/via_app_data/via_app_data.py:49]
ROOT: 732 D Attempting to acquire lock 135013969084128 on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/pip-21.0.1-py3-none-any.lock [filelock.py:270]
ROOT: 733 D Attempting to acquire lock 135013941858064 on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any.lock [filelock.py:270]
ROOT: 734 D Lock 135013969084128 acquired on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/pip-21.0.1-py3-none-any.lock [filelock.py:274]
ROOT: 734 D Attempting to acquire lock 135013941855472 on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any.lock [filelock.py:270]
ROOT: 736 D Lock 135013941858064 acquired on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any.lock [filelock.py:274]
ROOT: 736 D Lock 135013941855472 acquired on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any.lock [filelock.py:274]
ROOT: 738 D Attempting to release lock 135013969084128 on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/pip-21.0.1-py3-none-any.lock [filelock.py:315]
ROOT: 738 D Lock 135013969084128 released on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/pip-21.0.1-py3-none-any.lock [filelock.py:318]
ROOT: 738 D Attempting to release lock 135013941855472 on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any.lock [filelock.py:315]
ROOT: 739 D Lock 135013941855472 released on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any.lock [filelock.py:318]
ROOT: 739 D Attempting to release lock 135013941858064 on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any.lock [filelock.py:315]
ROOT: 740 D Lock 135013941858064 released on /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any.lock [filelock.py:318]
ROOT: 741 D copy /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/pip-21.0.1-py3-none-any/pip-21.0.1.virtualenv to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/pip-21.0.1.virtualenv [virtualenv/util/path/_sync.py:52]
ROOT: 741 D copy directory /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any/wheel-0.36.2.dist-info to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/wheel-0.36.2.dist-info [virtualenv/util/path/_sync.py:52]
ROOT: 741 D copy directory /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/_distutils_hack to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/_distutils_hack [virtualenv/util/path/_sync.py:52]
ROOT: 743 D copy directory /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/pip-21.0.1-py3-none-any/pip to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/pip [virtualenv/util/path/_sync.py:52]
ROOT: 751 D copy directory /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/setuptools to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/setuptools [virtualenv/util/path/_sync.py:52]
ROOT: 768 D copy directory /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any/wheel to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/wheel [virtualenv/util/path/_sync.py:52]
ROOT: 850 D copy /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any/wheel-0.36.2.virtualenv to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/wheel-0.36.2.virtualenv [virtualenv/util/path/_sync.py:52]
ROOT: 855 D changing mode of /home/user/hello-tox/.tox/4/py/bin/wheel-3.9 to 775 [distlib/util.py:566]
ROOT: 857 D changing mode of /home/user/hello-tox/.tox/4/py/bin/wheel3 to 775 [distlib/util.py:566]
ROOT: 858 D changing mode of /home/user/hello-tox/.tox/4/py/bin/wheel to 775 [distlib/util.py:566]
ROOT: 861 D changing mode of /home/user/hello-tox/.tox/4/py/bin/wheel3.9 to 775 [distlib/util.py:566]
ROOT: 861 D generated console scripts wheel-3.9 wheel3 wheel3.9 wheel [virtualenv/seed/embed/via_app_data/pip_install/base.py:48]
ROOT: 1018 D copy directory /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/setuptools-53.0.0.dist-info to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/setuptools-53.0.0.dist-info [virtualenv/util/path/_sync.py:52]
ROOT: 1031 D copy directory /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/pkg_resources to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/pkg_resources [virtualenv/util/path/_sync.py:52]
ROOT: 1109 D copy /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/distutils-precedence.pth to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/distutils-precedence.pth [virtualenv/util/path/_sync.py:52]
ROOT: 1113 D copy /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/setuptools-53.0.0.virtualenv to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/setuptools-53.0.0.virtualenv [virtualenv/util/path/_sync.py:52]
ROOT: 1115 D generated console scripts  [virtualenv/seed/embed/via_app_data/pip_install/base.py:48]
ROOT: 1390 D copy directory /home/user/.local/share/virtualenv/wheel/3.9/image/1/CopyPipInstall/pip-21.0.1-py3-none-any/pip-21.0.1.dist-info to /home/user/hello-tox/.tox/4/py/lib/python3.9/site-packages/pip-21.0.1.dist-info [virtualenv/util/path/_sync.py:52]
ROOT: 1398 D changing mode of /home/user/hello-tox/.tox/4/py/bin/pip3.9 to 775 [distlib/util.py:566]
ROOT: 1398 D changing mode of /home/user/hello-tox/.tox/4/py/bin/pip-3.9 to 775 [distlib/util.py:566]
ROOT: 1399 D changing mode of /home/user/hello-tox/.tox/4/py/bin/pip to 775 [distlib/util.py:566]
ROOT: 1399 D changing mode of /home/user/hello-tox/.tox/4/py/bin/pip3 to 775 [distlib/util.py:566]
ROOT: 1399 D generated console scripts pip pip-3.9 pip3 pip3.9 [virtualenv/seed/embed/via_app_data/pip_install/base.py:48]
ROOT: 1400 I add activators for Bash, CShell, Fish, PowerShell, Python, Xonsh [virtualenv/run/session.py:64]
ROOT: 1408 D write /home/user/hello-tox/.tox/4/py/pyvenv.cfg [virtualenv/create/pyenv_cfg.py:34]
ROOT: 1408 D 	home = /home/user/.conda/envs/py3 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 1409 D 	implementation = CPython [virtualenv/create/pyenv_cfg.py:38]
ROOT: 1409 D 	version_info = 3.9.1.final.0 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 1409 D 	virtualenv = 20.4.2 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 1409 D 	include-system-site-packages = false [virtualenv/create/pyenv_cfg.py:38]
ROOT: 1409 D 	base-prefix = /home/user/.conda/envs/py3 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 1409 D 	base-exec-prefix = /home/user/.conda/envs/py3 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 1409 D 	base-executable = /home/user/.conda/envs/py3/bin/python3.9 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 1413 W commands[0]> python -c 'import sys; print(sys.argv)' '{posargs}' '{posargs}' [tox/tox_env/api.py:288]
['-c', '{posargs}', '{posargs}']
ROOT: 1474 I exit 0 (0.06 seconds) /home/user/hello-tox> python -c 'import sys; print(sys.argv)' '{posargs}' '{posargs}' pid=15117 [tox/execute/api.py:193]
  py: OK (1.06=setup[1.00]+cmd[0.06] seconds)
  congratulations :) (1.13 seconds)
```

</details>

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/tox-dev/tox/pull/1961?src=pr&el=h1) Report
> Merging [#1961](https://codecov.io/gh/tox-dev/tox/pull/1961?src=pr&el=desc) (b9ee3ce) into [rewrite](https://codecov.io/gh/tox-dev/tox/commit/e0df6c16d90ff3dae3e08c42237bdda4feb065e3?el=desc) (e0df6c1) will **increase** coverage by `0.17%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tox-dev/tox/pull/1961/graphs/tree.svg?width=650&height=150&src=pr&token=DYodAwDCZ5)](https://codecov.io/gh/tox-dev/tox/pull/1961?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##           rewrite     #1961      +/-   ##
============================================
+ Coverage    99.82%   100.00%   +0.17%     
============================================
  Files          143       142       -1     
  Lines         7881      7693     -188     
  Branches       795       762      -33     
============================================
- Hits          7867      7693     -174     
+ Misses           3         0       -3     
+ Partials        11         0      -11     
```

| Flag | Coverage Δ | |
|---|---|---|
| tests | `100.00% <100.00%> (+0.17%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/tox-dev/tox/pull/1961?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/tox/config/loader/ini/replace.py](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree#diff-c3JjL3RveC9jb25maWcvbG9hZGVyL2luaS9yZXBsYWNlLnB5) | `100.00% <100.00%> (ø)` | |
| [src/tox/config/set\_env.py](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree#diff-c3JjL3RveC9jb25maWcvc2V0X2Vudi5weQ==) | `100.00% <100.00%> (ø)` | |
| [...onfig/loader/ini/replace/test\_find\_replace\_part.py](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree#diff-dGVzdHMvY29uZmlnL2xvYWRlci9pbmkvcmVwbGFjZS90ZXN0X2ZpbmRfcmVwbGFjZV9wYXJ0LnB5) | `100.00% <100.00%> (ø)` | |
| [.../config/loader/ini/replace/test\_replace\_posargs.py](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree#diff-dGVzdHMvY29uZmlnL2xvYWRlci9pbmkvcmVwbGFjZS90ZXN0X3JlcGxhY2VfcG9zYXJncy5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/tox/pytest.py](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree#diff-c3JjL3RveC9weXRlc3QucHk=) | `100.00% <0.00%> (ø)` | |
| [src/tox/tox\_env/api.py](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L2FwaS5weQ==) | `100.00% <0.00%> (ø)` | |
| [src/tox/session/state.py](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree#diff-c3JjL3RveC9zZXNzaW9uL3N0YXRlLnB5) | `100.00% <0.00%> (ø)` | |
| [src/tox/execute/request.py](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree#diff-c3JjL3RveC9leGVjdXRlL3JlcXVlc3QucHk=) | `100.00% <0.00%> (ø)` | |
| [src/tox/util/pep517/backend.py](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree#diff-c3JjL3RveC91dGlsL3BlcDUxNy9iYWNrZW5kLnB5) | `100.00% <0.00%> (ø)` | |
| [src/tox/execute/pep517\_backend.py](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree#diff-c3JjL3RveC9leGVjdXRlL3BlcDUxN19iYWNrZW5kLnB5) | `100.00% <0.00%> (ø)` | |
| ... and [10 more](https://codecov.io/gh/tox-dev/tox/pull/1961/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tox-dev/tox/pull/1961?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tox-dev/tox/pull/1961?src=pr&el=footer). Last update [e0df6c1...b9ee3ce](https://codecov.io/gh/tox-dev/tox/pull/1961?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/changelog/1956.bugfix.rst b/docs/changelog/1956.bugfix.rst
new file mode 100644
index 00000000..bd86986b
--- /dev/null
+++ b/docs/changelog/1956.bugfix.rst
@@ -0,0 +1,3 @@
+Due to a bug ``\{posargs} {posargs}`` used to expand to literal ``{posargs} {posargs}``.
+Now the second ``{posargs}`` is expanded.
+``\{posargs} {posargs}`` expands to ``{posargs} positional arguments here`` - by :user:`hexagonrecursion`.
diff --git a/src/tox/config/loader/ini/replace.py b/src/tox/config/loader/ini/replace.py
index 505519ce..c5074575 100644
--- a/src/tox/config/loader/ini/replace.py
+++ b/src/tox/config/loader/ini/replace.py
@@ -24,9 +24,9 @@ ARGS_GROUP = re.compile(r"(?<!\\\\|:[A-Z]):")
 
 def replace(conf: "Config", name: Optional[str], loader: "IniLoader", value: str, chain: List[str]) -> str:
     # perform all non-escaped replaces
-    start, end = 0, 0
+    end = 0
     while True:
-        start, end, to_replace = find_replace_part(value, start, end)
+        start, end, to_replace = find_replace_part(value, end)
         if to_replace is None:
             break
         replaced = _replace_match(conf, name, loader, to_replace, chain.copy())
@@ -34,10 +34,10 @@ def replace(conf: "Config", name: Optional[str], loader: "IniLoader", value: str
             # if we cannot replace, keep what was there, and continue looking for additional replaces following
             # note, here we cannot raise because the content may be a factorial expression, and in those case we don't
             # want to enforce escaping curly braces, e.g. it should work to write: env_list = {py39,py38}-{,dep}
-            start = end = end + 1
+            end = end + 1
             continue
         new_value = value[:start] + replaced + value[end + 1 :]
-        start, end = 0, 0  # if we performed a replacement start over
+        end = 0  # if we performed a replacement start over
         if new_value == value:  # if we're not making progress stop (circular reference?)
             break
         value = new_value
@@ -49,45 +49,25 @@ def replace(conf: "Config", name: Optional[str], loader: "IniLoader", value: str
     return value
 
 
-def find_replace_part(value: str, start: int, end: int) -> Tuple[int, int, Optional[str]]:
-    bracket_at = find_brackets(value, end)
-    if bracket_at != -1:
-        return bracket_at, bracket_at + 1, "posargs"  # brackets is an alias for positional arguments
-    start, end, match = find_braces(value, start, end)
-    return start, end, (value[start + 1 : end] if match else None)
-
-
-def find_brackets(value: str, end: int) -> int:
-    while True:
-        pos = value.find("[]", end)
-        if pos == -1:
-            break
-        if pos >= 1 and value[pos - 1] == "\\":  # the opened bracket is escaped
-            end = pos + 1
-            continue
-        break
-    return pos
+REPLACE_PART = re.compile(
+    r"""
+        (?<! \\) \{  # Unescaped {
+            ( [^{}] | \\ \{ | \\ \} )*  # Anything except an unescaped { or }
+        (?<! \\) \}  # Unescaped }
+    |
+        (?<! \\) \[ \]  # Unescaped []
+    """,
+    re.VERBOSE,
+)  # simplified - not verbose version (?<!\\)([^{}]|\\\{|\\\})*(?<!\\)\}|(?<!\\)\[\]
 
 
-def find_braces(value: str, start: int, end: int) -> Tuple[int, int, bool]:
-    match = False
-    while end != -1:
-        end = value.find("}", end)
-        if end == -1:
-            continue
-        if end >= 1 and value[end - 1] == "\\":  # ignore escaped
-            end += 1
-            continue
-        before = end
-        while True:
-            start = value.rfind("{", 0, before)
-            if start >= 1 and value[start - 1] == "\\":  # ignore escaped
-                before = start - 1
-                continue
-            match = start != -1
-            break
-        break  # pragma: no cover # for some odd reason this line is not reported by coverage, though is needed
-    return start, end, match
+def find_replace_part(value: str, end: int) -> Tuple[int, int, Optional[str]]:
+    match = REPLACE_PART.search(value, end)
+    if match is None:
+        return -1, -1, None
+    if match.group() == "[]":
+        return match.start(), match.end() - 1, "posargs"  # brackets is an alias for positional arguments
+    return match.start(), match.end() - 1, match.group()[1:-1]
 
 
 def _replace_match(
diff --git a/src/tox/config/set_env.py b/src/tox/config/set_env.py
index 8e5b5a49..8a4d794a 100644
--- a/src/tox/config/set_env.py
+++ b/src/tox/config/set_env.py
@@ -18,7 +18,7 @@ class SetEnv:
                     if "{" in key:
                         raise ValueError(f"invalid line {line!r} in set_env")
                 except ValueError:
-                    _, __, match = find_replace_part(line, 0, 0)
+                    _, __, match = find_replace_part(line, 0)
                     if match:
                         self._later.append(line)
                     else:


Test patch:
diff --git a/tests/config/loader/ini/replace/test_replace.py b/tests/config/loader/ini/replace/test_replace.py
new file mode 100644
index 00000000..99de5bcd
--- /dev/null
+++ b/tests/config/loader/ini/replace/test_replace.py
@@ -0,0 +1,27 @@
+from typing import Tuple
+
+import pytest
+
+from tox.config.loader.ini.replace import find_replace_part
+
+
+@pytest.mark.parametrize(
+    ("value", "result"),
+    [
+        ("[]", (0, 1, "posargs")),
+        ("123[]", (3, 4, "posargs")),
+        ("[]123", (0, 1, "posargs")),
+        (r"\[\] []", (5, 6, "posargs")),
+        (r"[\] []", (4, 5, "posargs")),
+        (r"\[] []", (4, 5, "posargs")),
+        ("{foo}", (0, 4, "foo")),
+        (r"\{foo} {bar}", (7, 11, "bar")),
+        ("{foo} {bar}", (0, 4, "foo")),
+        (r"{foo\} {bar}", (7, 11, "bar")),
+        (r"{foo:{bar}}", (5, 9, "bar")),
+        (r"{\{}", (0, 3, r"\{")),
+        (r"{\}}", (0, 3, r"\}")),
+    ],
+)
+def test_match(value: str, result: Tuple[int, int, str]) -> None:
+    assert find_replace_part(value, 0) == result
diff --git a/tests/config/loader/ini/replace/test_replace_posargs.py b/tests/config/loader/ini/replace/test_replace_posargs.py
index d2ebed4b..45b26ff8 100644
--- a/tests/config/loader/ini/replace/test_replace_posargs.py
+++ b/tests/config/loader/ini/replace/test_replace_posargs.py
@@ -75,3 +75,9 @@ def test_replace_pos_args_escaped(replace_one: ReplaceOne, value: str) -> None:
 def test_replace_mixed_brackets_and_braces(replace_one: ReplaceOne, value: str, result: str) -> None:
     outcome = replace_one(value, ["foo"])
     assert result == outcome
+
+
+def test_half_escaped_braces(replace_one: ReplaceOne) -> None:
+    """See https://github.com/tox-dev/tox/issues/1956"""
+    outcome = replace_one(r"\{posargs} {posargs}", ["foo"])
+    assert "{posargs} foo" == outcome