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:
Weirdly, you can do what you want to already, just not with f-strings, you have to use environment variables:

```shell
>>> touch file-a file-b
>>> $prefix = "file"
>>> g`$prefix*`
['file-a', 'file-b']
```

I'm not saying Xonsh shouldn't let you use Python variables this way, just that there is already some magic replacement going on somewhere.

Resolving patch:
diff --git a/xonsh/parsers/base.py b/xonsh/parsers/base.py
--- 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 @@ def _set_error(self, msg, loc=None):
     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 @@ def p_atom_false(self, p):
 
     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 @@ def p_subproc_atom_redirect(self, p):
 
     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
--- a/xonsh/tokenize.py
+++ b/xonsh/tokenize.py
@@ -305,7 +305,7 @@ def maybe(*choices):
 )
 
 # 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
--- 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)