Repository: tox-dev/tox

Issue:
tox 4 bracket posargs are missing
as example i cut down the apipkg tox.ini to just

```
[testenv]
deps=pytest
commands=pytest []
```
ran
```
$ tox4
py: install_deps> python -I -m pip install pytest
.pkg: get_requires_for_build_wheel> python /home/rpfannsc/.local/lib/python3.9/site-packages/tox/util/pep517/backend.py True setuptools.build_meta __legacy__
.pkg: prepare_metadata_for_build_wheel> python /home/rpfannsc/.local/lib/python3.9/site-packages/tox/util/pep517/backend.py True setuptools.build_meta __legacy__
.pkg: get_requires_for_build_sdist> python /home/rpfannsc/.local/lib/python3.9/site-packages/tox/util/pep517/backend.py True setuptools.build_meta __legacy__
.pkg: build_sdist> python /home/rpfannsc/.local/lib/python3.9/site-packages/tox/util/pep517/backend.py True setuptools.build_meta __legacy__
py: install_package> python -I -m pip install --no-deps --force-reinstall /home/rpfannsc/Projects/pytest-dev/apipkg/.tox/4/.pkg/dist/apipkg-1.6.dev16+ge54efbf.d20210227.tar.gz
py: commands[0]> pytest '[]'
ERROR: file or directory not found: []

========================================================================================= test session starts ==========================================================================================
platform linux -- Python 3.9.1, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
apipkg full install version=1.6.dev16+ge54efbf.d20210227
rootdir: /home/rpfannsc/Projects/pytest-dev/apipkg
collected 0 items

======================================================================================== no tests ran in 0.00s =========================================================================================
py: exit 4 (0.34 seconds) /home/rpfannsc/Projects/pytest-dev/apipkg> pytest '[]' pid=1183180
.pkg: _exit> python /home/rpfannsc/.local/lib/python3.9/site-packages/tox/util/pep517/backend.py True setuptools.build_meta __legacy__
  py: FAIL code 4 (9.90=setup[9.57]+cmd[0.34] seconds)
  evaluation failed :( (10.00 seconds)
```
and ran 
```
$ tox
GLOB sdist-make: /home/rpfannsc/Projects/pytest-dev/apipkg/setup.py
python create: /home/rpfannsc/Projects/pytest-dev/apipkg/.tox/python
python installdeps: pytest
python inst: /home/rpfannsc/Projects/pytest-dev/apipkg/.tox/.tmp/package/1/apipkg-1.6.dev16+ge54efbf.d20210227.zip
python installed: apipkg @ file:///home/rpfannsc/Projects/pytest-dev/apipkg/.tox/.tmp/package/1/apipkg-1.6.dev16%2Bge54efbf.d20210227.zip,attrs==20.3.0,iniconfig==1.1.1,packaging==20.9,pluggy==0.13.1,py==1.10.0,pyparsing==2.4.7,pytest==6.2.2,toml==0.10.2
python run-test-pre: PYTHONHASHSEED='1155200767'
python run-test: commands[0] | pytest
========================================================================================= test session starts ==========================================================================================
platform linux -- Python 3.9.1, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
cachedir: .tox/python/.pytest_cache
apipkg full install version=1.6.dev16+ge54efbf.d20210227
rootdir: /home/rpfannsc/Projects/pytest-dev/apipkg
collected 42 items                                                                                                                                                                                     

test_apipkg.py ..........................................                                                                                                                                        [100%]

========================================================================================== 42 passed in 0.63s ==========================================================================================
_______________________________________________________________________________________________ summary ________________________________________________________________________________________________
  python: commands succeeded
  congratulations :)
```
and then ran
```
$ tox -- -k aliasmod
GLOB sdist-make: /home/rpfannsc/Projects/pytest-dev/apipkg/setup.py
python inst-nodeps: /home/rpfannsc/Projects/pytest-dev/apipkg/.tox/.tmp/package/1/apipkg-1.6.dev16+ge54efbf.d20210227.zip
python installed: apipkg @ file:///home/rpfannsc/Projects/pytest-dev/apipkg/.tox/.tmp/package/1/apipkg-1.6.dev16%2Bge54efbf.d20210227.zip,attrs==20.3.0,iniconfig==1.1.1,packaging==20.9,pluggy==0.13.1,py==1.10.0,pyparsing==2.4.7,pytest==6.2.2,toml==0.10.2
python run-test-pre: PYTHONHASHSEED='1148858278'
python run-test: commands[0] | pytest -k aliasmod
========================================================================================= test session starts ==========================================================================================
platform linux -- Python 3.9.1, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
cachedir: .tox/python/.pytest_cache
apipkg full install version=1.6.dev16+ge54efbf.d20210227
rootdir: /home/rpfannsc/Projects/pytest-dev/apipkg
collected 42 items / 33 deselected / 9 selected                                                                                                                                                        

test_apipkg.py .........                                                                                                                                                                         [100%]

=================================================================================== 9 passed, 33 deselected in 0.19s ===================================================================================
_______________________________________________________________________________________________ summary ________________________________________________________________________________________________
  python: commands succeeded
  congratulations :)

```


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

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

```diff
@@             Coverage Diff              @@
##           rewrite     #1954      +/-   ##
============================================
+ Coverage    99.82%   100.00%   +0.17%     
============================================
  Files          143       142       -1     
  Lines         7839      7729     -110     
  Branches       789       764      -25     
============================================
- Hits          7825      7729      -96     
+ 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/1954?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [.../config/loader/ini/replace/test\_replace\_posargs.py](https://codecov.io/gh/tox-dev/tox/pull/1954/diff?src=pr&el=tree#diff-dGVzdHMvY29uZmlnL2xvYWRlci9pbmkvcmVwbGFjZS90ZXN0X3JlcGxhY2VfcG9zYXJncy5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/tox/pytest.py](https://codecov.io/gh/tox-dev/tox/pull/1954/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/1954/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L2FwaS5weQ==) | `100.00% <0.00%> (ø)` | |
| [src/tox/session/state.py](https://codecov.io/gh/tox-dev/tox/pull/1954/diff?src=pr&el=tree#diff-c3JjL3RveC9zZXNzaW9uL3N0YXRlLnB5) | `100.00% <0.00%> (ø)` | |
| [src/tox/execute/request.py](https://codecov.io/gh/tox-dev/tox/pull/1954/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/1954/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/1954/diff?src=pr&el=tree#diff-c3JjL3RveC9leGVjdXRlL3BlcDUxN19iYWNrZW5kLnB5) | `100.00% <0.00%> (ø)` | |
| [src/tox/session/cmd/run/common.py](https://codecov.io/gh/tox-dev/tox/pull/1954/diff?src=pr&el=tree#diff-c3JjL3RveC9zZXNzaW9uL2NtZC9ydW4vY29tbW9uLnB5) | `100.00% <0.00%> (ø)` | |
| [src/tox/tox\_env/python/req\_file.py](https://codecov.io/gh/tox-dev/tox/pull/1954/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L3B5dGhvbi9yZXFfZmlsZS5weQ==) | `100.00% <0.00%> (ø)` | |
| [src/tox/config/loader/ini/replace.py](https://codecov.io/gh/tox-dev/tox/pull/1954/diff?src=pr&el=tree#diff-c3JjL3RveC9jb25maWcvbG9hZGVyL2luaS9yZXBsYWNlLnB5) | `100.00% <0.00%> (ø)` | |
| ... and [6 more](https://codecov.io/gh/tox-dev/tox/pull/1954/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tox-dev/tox/pull/1954?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/1954?src=pr&el=footer). Last update [651e1ae...671a60f](https://codecov.io/gh/tox-dev/tox/pull/1954?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

gaborbernat: @hexagonrecursion in the future can you please give human-readable titles to the PRs, please 🙏🏻 

Patch:
diff --git a/docs/changelog/1928.feature.rst b/docs/changelog/1928.feature.rst
new file mode 100644
index 00000000..1f53b09b
--- /dev/null
+++ b/docs/changelog/1928.feature.rst
@@ -0,0 +1,2 @@
+Implemented ``[]`` substitution (alias for ``{posargs}``) - by
+:user:`hexagonrecursion`.
diff --git a/src/tox/config/loader/ini/replace.py b/src/tox/config/loader/ini/replace.py
index 91d3c032..505519ce 100644
--- a/src/tox/config/loader/ini/replace.py
+++ b/src/tox/config/loader/ini/replace.py
@@ -26,10 +26,9 @@ def replace(conf: "Config", name: Optional[str], loader: "IniLoader", value: str
     # perform all non-escaped replaces
     start, end = 0, 0
     while True:
-        start, end, match = find_replace_part(value, start, end)
-        if not match:
+        start, end, to_replace = find_replace_part(value, start, end)
+        if to_replace is None:
             break
-        to_replace = value[start + 1 : end]
         replaced = _replace_match(conf, name, loader, to_replace, chain.copy())
         if replaced is None:
             # if we cannot replace, keep what was there, and continue looking for additional replaces following
@@ -38,17 +37,39 @@ def replace(conf: "Config", name: Optional[str], loader: "IniLoader", value: str
             start = end = end + 1
             continue
         new_value = value[:start] + replaced + value[end + 1 :]
-        start, end = 0, 0  # if we performed a replace start over
+        start, end = 0, 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
     # remove escape sequences
     value = value.replace("\\{", "{")
     value = value.replace("\\}", "}")
+    value = value.replace("\\[", "[")
+    value = value.replace("\\]", "]")
     return value
 
 
-def find_replace_part(value: str, start: int, end: int) -> Tuple[int, int, bool]:
+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
+
+
+def find_braces(value: str, start: int, end: int) -> Tuple[int, int, bool]:
     match = False
     while end != -1:
         end = value.find("}", end)


Test patch:
diff --git a/tests/config/loader/ini/replace/test_replace_posargs.py b/tests/config/loader/ini/replace/test_replace_posargs.py
index 20f4091f..d2ebed4b 100644
--- a/tests/config/loader/ini/replace/test_replace_posargs.py
+++ b/tests/config/loader/ini/replace/test_replace_posargs.py
@@ -5,23 +5,27 @@ import pytest
 from tests.config.loader.ini.replace.conftest import ReplaceOne
 
 
-def test_replace_pos_args_none_sys_argv(replace_one: ReplaceOne) -> None:
-    result = replace_one("{posargs}", None)
+@pytest.mark.parametrize("syntax", ["{posargs}", "[]"])
+def test_replace_pos_args_none_sys_argv(syntax: str, replace_one: ReplaceOne) -> None:
+    result = replace_one(syntax, None)
     assert result == ""
 
 
-def test_replace_pos_args_empty_sys_argv(replace_one: ReplaceOne) -> None:
-    result = replace_one("{posargs}", [])
+@pytest.mark.parametrize("syntax", ["{posargs}", "[]"])
+def test_replace_pos_args_empty_sys_argv(syntax: str, replace_one: ReplaceOne) -> None:
+    result = replace_one(syntax, [])
     assert result == ""
 
 
-def test_replace_pos_args_extra_sys_argv(replace_one: ReplaceOne) -> None:
-    result = replace_one("{posargs}", [sys.executable, "magic"])
+@pytest.mark.parametrize("syntax", ["{posargs}", "[]"])
+def test_replace_pos_args_extra_sys_argv(syntax: str, replace_one: ReplaceOne) -> None:
+    result = replace_one(syntax, [sys.executable, "magic"])
     assert result == f"{sys.executable} magic"
 
 
-def test_replace_pos_args(replace_one: ReplaceOne) -> None:
-    result = replace_one("{posargs}", ["ok", "what", " yes "])
+@pytest.mark.parametrize("syntax", ["{posargs}", "[]"])
+def test_replace_pos_args(syntax: str, replace_one: ReplaceOne) -> None:
+    result = replace_one(syntax, ["ok", "what", " yes "])
     quote = '"' if sys.platform == "win32" else "'"
     assert result == f"ok what {quote} yes {quote}"
 
@@ -31,8 +35,8 @@ def test_replace_pos_args(replace_one: ReplaceOne) -> None:
     [
         ("magic", "magic"),
         ("magic:colon", "magic:colon"),
-        ("magic\n b:c", "magic\nb:c"),  # unescaped newline keeps the newline
-        ("magi\\\n c:d", "magic:d"),  # escaped newline merges the lines
+        ("magic\n b:c", "magic\nb:c"),  # an unescaped newline keeps the newline
+        ("magi\\\n c:d", "magic:d"),  # an escaped newline merges the lines
         ("\\{a\\}", "{a}"),  # escaped curly braces
     ],
 )
@@ -50,9 +54,24 @@ def test_replace_pos_args_default(replace_one: ReplaceOne, value: str, result: s
         "\\{posargs\\}",
         "{\\{posargs}",
         "{\\{posargs}{}",
+        "\\[]",
+        "[\\]",
+        "\\[\\]",
     ],
 )
 def test_replace_pos_args_escaped(replace_one: ReplaceOne, value: str) -> None:
     result = replace_one(value, None)
-    outcome = value.replace("\\{", "{").replace("\\}", "}")
+    outcome = value.replace("\\", "")
+    assert result == outcome
+
+
+@pytest.mark.parametrize(
+    ("value", "result"),
+    [
+        ("[]-{posargs}", "foo-foo"),
+        ("{posargs}-[]", "foo-foo"),
+    ],
+)
+def test_replace_mixed_brackets_and_braces(replace_one: ReplaceOne, value: str, result: str) -> None:
+    outcome = replace_one(value, ["foo"])
     assert result == outcome