Repository: xonsh/xonsh

Issue:
Feat: add f-glob strings
In Xonsh we have augmented string literals with `p`-strings (that return a Path object) and glob strings g`...` (that return a list of results). It would seem that we want glob strings to behave similarly to normal strings, and so there is a case to be made for supporting `f`-strings in this context.

An example:
```xonsh
>>> ls
file-a  file-b
>>> prefix = "file"
>>> echo fg`{prefix}*`
file-a file-b
```

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**


Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/xonsh/xonsh/pull/4835?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh) Report
> Merging [#4835](https://codecov.io/gh/xonsh/xonsh/pull/4835?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh) (bfe1ae9) into [main](https://codecov.io/gh/xonsh/xonsh/commit/0ddc05e82e3c91130f61173618925619e44cda7e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh) (0ddc05e) will **decrease** coverage by `2.48%`.
> The diff coverage is `75.86%`.

```diff
@@            Coverage Diff             @@
##             main    #4835      +/-   ##
==========================================
- Coverage   66.58%   64.09%   -2.49%     
==========================================
  Files         134      134              
  Lines       23634    23645      +11     
  Branches     4687     4689       +2     
==========================================
- Hits        15736    15156     -580     
- Misses       6677     7243     +566     
- Partials     1221     1246      +25     
```

| Flag | Coverage Δ | |
|---|---|---|
| macOS-latest | `?` | |
| ubuntu-latest | `64.09% <75.86%> (-0.05%)` | :arrow_down: |
| windows-latest | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/xonsh/xonsh/pull/4835?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh) | Coverage Δ | |
|---|---|---|
| [xonsh/parsers/base.py](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvcGFyc2Vycy9iYXNlLnB5) | `84.71% <75.00%> (-0.47%)` | :arrow_down: |
| [xonsh/tokenize.py](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvdG9rZW5pemUucHk=) | `56.87% <100.00%> (ø)` | |
| [xonsh/macutils.py](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvbWFjdXRpbHMucHk=) | `0.00% <0.00%> (-80.00%)` | :arrow_down: |
| [xonsh/prompt/job.py](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvcHJvbXB0L2pvYi5weQ==) | `20.00% <0.00%> (-50.00%)` | :arrow_down: |
| [xonsh/winutils.py](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvd2ludXRpbHMucHk=) | `0.00% <0.00%> (-47.81%)` | :arrow_down: |
| [xonsh/platform.py](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvcGxhdGZvcm0ucHk=) | `46.62% <0.00%> (-17.60%)` | :arrow_down: |
| [xonsh/completers/path.py](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvY29tcGxldGVycy9wYXRoLnB5) | `56.41% <0.00%> (-15.39%)` | :arrow_down: |
| [xonsh/lazyimps.py](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvbGF6eWltcHMucHk=) | `57.37% <0.00%> (-14.76%)` | :arrow_down: |
| [xonsh/procs/readers.py](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvcHJvY3MvcmVhZGVycy5weQ==) | `47.00% <0.00%> (-11.06%)` | :arrow_down: |
| [xonsh/jobs.py](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvam9icy5weQ==) | `38.35% <0.00%> (-10.28%)` | :arrow_down: |
| ... and [33 more](https://codecov.io/gh/xonsh/xonsh/pull/4835/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh) | |

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh)


Patch:
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index 0cc3de3b..41945aef 100644
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -1054,6 +1054,23 @@ mode or subprocess mode) by using the ``g````:
     5
 
 
+Formatted Glob Literals
+-----------------------
+
+Using the ``f`` modifier with either regex or normal globbing makes
+the glob pattern behave like a formatted string literal. This can be used to
+substitute variables and other expressions into the glob pattern:
+
+.. code-block:: xonshcon
+
+    >>> touch a aa aaa aba abba aab aabb abcba
+    >>> mypattern = 'ab'
+    >>> print(f`{mypattern[0]}+`)
+    ['a', 'aa', 'aaa']
+    >>> print(gf`{mypattern}*`)
+    ['aba', 'abba', 'abcba']
+
+
 Custom Path Searches
 --------------------
 
diff --git a/news/feat-f-glob-strings.rst b/news/feat-f-glob-strings.rst
new file mode 100644
index 00000000..7c1d02a3
--- /dev/null
+++ b/news/feat-f-glob-strings.rst
@@ -0,0 +1,23 @@
+**Added:**
+
+* Support for f-glob strings (e.g. ``fg`{prefix}*```)
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* <news item>
+
+**Security:**
+
+* <news item>
diff --git a/xonsh/parsers/base.py b/xonsh/parsers/base.py
index ab20d244..7208ddf6 100644
--- a/xonsh/parsers/base.py
+++ b/xonsh/parsers/base.py
@@ -124,31 +124,6 @@ def xonsh_superhelp(x, lineno=None, col=None):
     return xonsh_call("__xonsh__.superhelp", [x], lineno=lineno, col=col)
 
 
-def xonsh_pathsearch(pattern, pymode=False, lineno=None, col=None):
-    """Creates the AST node for calling the __xonsh__.pathsearch() function.
-    The pymode argument indicate if it is called from subproc or python mode"""
-    pymode = ast.NameConstant(value=pymode, lineno=lineno, col_offset=col)
-    searchfunc, pattern = RE_SEARCHPATH.match(pattern).groups()
-    pattern = ast.Str(s=pattern, lineno=lineno, col_offset=col)
-    pathobj = False
-    if searchfunc.startswith("@"):
-        func = searchfunc[1:]
-    elif "g" in searchfunc:
-        func = "__xonsh__.globsearch"
-        pathobj = "p" in searchfunc
-    else:
-        func = "__xonsh__.regexsearch"
-        pathobj = "p" in searchfunc
-    func = load_attribute_chain(func, lineno=lineno, col=col)
-    pathobj = ast.NameConstant(value=pathobj, lineno=lineno, col_offset=col)
-    return xonsh_call(
-        "__xonsh__.pathsearch",
-        args=[func, pattern, pymode, pathobj],
-        lineno=lineno,
-        col=col,
-    )
-
-
 def load_ctx(x):
     """Recursively sets ctx to ast.Load()"""
     if not hasattr(x, "ctx"):
@@ -658,6 +633,44 @@ class BaseParser:
     def _parse_error(self, msg, loc):
         raise_parse_error(msg, loc, self._source, self.lines)
 
+    def xonsh_pathsearch(self, pattern, pymode=False, lineno=None, col=None):
+        """Creates the AST node for calling the __xonsh__.pathsearch() function.
+        The pymode argument indicate if it is called from subproc or python mode"""
+        pymode = ast.NameConstant(value=pymode, lineno=lineno, col_offset=col)
+        searchfunc, pattern = RE_SEARCHPATH.match(pattern).groups()
+        if not searchfunc.startswith("@") and "f" in searchfunc:
+            pattern_as_str = f"f'''{pattern}'''"
+            try:
+                pattern = pyparse(pattern_as_str).body[0].value
+            except SyntaxError:
+                pattern = None
+            if pattern is None:
+                try:
+                    pattern = FStringAdaptor(
+                        pattern_as_str, "f", filename=self.lexer.fname
+                    ).run()
+                except SyntaxError as e:
+                    self._set_error(str(e), self.currloc(lineno=lineno, column=col))
+        else:
+            pattern = ast.Str(s=pattern, lineno=lineno, col_offset=col)
+        pathobj = False
+        if searchfunc.startswith("@"):
+            func = searchfunc[1:]
+        elif "g" in searchfunc:
+            func = "__xonsh__.globsearch"
+            pathobj = "p" in searchfunc
+        else:
+            func = "__xonsh__.regexsearch"
+            pathobj = "p" in searchfunc
+        func = load_attribute_chain(func, lineno=lineno, col=col)
+        pathobj = ast.NameConstant(value=pathobj, lineno=lineno, col_offset=col)
+        return xonsh_call(
+            "__xonsh__.pathsearch",
+            args=[func, pattern, pymode, pathobj],
+            lineno=lineno,
+            col=col,
+        )
+
     #
     # Precedence of operators
     #
@@ -2413,7 +2426,9 @@ class BaseParser:
 
     def p_atom_pathsearch(self, p):
         """atom : SEARCHPATH"""
-        p[0] = xonsh_pathsearch(p[1], pymode=True, lineno=self.lineno, col=self.col)
+        p[0] = self.xonsh_pathsearch(
+            p[1], pymode=True, lineno=self.lineno, col=self.col
+        )
 
     # introduce seemingly superfluous symbol 'atom_dname' to enable reuse it in other places
     def p_atom_dname_indirection(self, p):
@@ -3352,7 +3367,7 @@ class BaseParser:
 
     def p_subproc_atom_re(self, p):
         """subproc_atom : SEARCHPATH"""
-        p0 = xonsh_pathsearch(p[1], pymode=False, lineno=self.lineno, col=self.col)
+        p0 = self.xonsh_pathsearch(p[1], pymode=False, lineno=self.lineno, col=self.col)
         p0._cliarg_action = "extend"
         p[0] = p0
 
diff --git a/xonsh/tokenize.py b/xonsh/tokenize.py
index 609bbb5b..045b47e7 100644
--- a/xonsh/tokenize.py
+++ b/xonsh/tokenize.py
@@ -305,7 +305,7 @@ String = group(
 )
 
 # Xonsh-specific Syntax
-SearchPath = r"((?:[rgp]+|@\w*)?)`([^\n`\\]*(?:\\.[^\n`\\]*)*)`"
+SearchPath = r"((?:[rgpf]+|@\w*)?)`([^\n`\\]*(?:\\.[^\n`\\]*)*)`"
 
 # Because of leftmost-then-longest match semantics, be sure to put the
 # longest operators first (e.g., if = came before ==, == would get


Test patch:
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 400b6c96..05ce324f 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -2376,8 +2376,11 @@ def test_ls_regex(check_xonsh_ast):
     check_xonsh_ast({}, "$(ls `[Ff]+i*LE` -l)", False)
 
 
-def test_backtick(check_xonsh_ast):
-    check_xonsh_ast({}, "print(`.*`)", False)
+@pytest.mark.parametrize("p", ["", "p"])
+@pytest.mark.parametrize("f", ["", "f"])
+@pytest.mark.parametrize("glob_type", ["", "r", "g"])
+def test_backtick(p, f, glob_type, check_xonsh_ast):
+    check_xonsh_ast({}, f"print({p}{f}{glob_type}`.*`)", False)
 
 
 def test_ls_regex_octothorpe(check_xonsh_ast):
@@ -2388,10 +2391,6 @@ def test_ls_explicitregex(check_xonsh_ast):
     check_xonsh_ast({}, "$(ls r`[Ff]+i*LE` -l)", False)
 
 
-def test_rbacktick(check_xonsh_ast):
-    check_xonsh_ast({}, "print(r`.*`)", False)
-
-
 def test_ls_explicitregex_octothorpe(check_xonsh_ast):
     check_xonsh_ast({}, "$(ls r`#[Ff]+i*LE` -l)", False)
 
@@ -2400,22 +2399,6 @@ def test_ls_glob(check_xonsh_ast):
     check_xonsh_ast({}, "$(ls g`[Ff]+i*LE` -l)", False)
 
 
-def test_gbacktick(check_xonsh_ast):
-    check_xonsh_ast({}, "print(g`.*`)", False)
-
-
-def test_pbacktrick(check_xonsh_ast):
-    check_xonsh_ast({}, "print(p`.*`)", False)
-
-
-def test_pgbacktick(check_xonsh_ast):
-    check_xonsh_ast({}, "print(pg`.*`)", False)
-
-
-def test_prbacktick(check_xonsh_ast):
-    check_xonsh_ast({}, "print(pr`.*`)", False)
-
-
 def test_ls_glob_octothorpe(check_xonsh_ast):
     check_xonsh_ast({}, "$(ls g`#[Ff]+i*LE` -l)", False)