diff --git a/news/fix_redir.rst b/news/fix_redir.rst new file mode 100644 index 000000000..17633337b --- /dev/null +++ b/news/fix_redir.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Fixed redirect with python substitution e.g. ``echo 1 > @('/tmp/file')`` is working now. + +**Security:** + +* diff --git a/tests/procs/test_specs.py b/tests/procs/test_specs.py index edb9e912e..312579de7 100644 --- a/tests/procs/test_specs.py +++ b/tests/procs/test_specs.py @@ -353,6 +353,14 @@ def test_on_command_not_found_doesnt_fire_in_non_interactive_mode(xession): assert not fired +def test_redirect_to_substitution(xession): + s = SubprocSpec.build( + # `echo hello > @('file')` + ["echo", "hello", (">", ["file"])] + ) + assert s.stdout.name == "file" + + def test_partial_args_from_classmethod(xession): class Class: @classmethod diff --git a/tests/test_integrations.py b/tests/test_integrations.py index b9a772a8c..38fb0835a 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -156,6 +156,34 @@ f e>o "The Truth is Out There\n", 0, ), + # test redirecting to a python substitution + ( + """ +def _f(): + print('Wow Mom!') +aliases['f'] = _f +f > @('tttt') +with open('tttt') as tttt: + s = tttt.read().strip() +print('REDIRECTED OUTPUT: ' + s) +""", + "REDIRECTED OUTPUT: Wow Mom!\n", + 0, + ), + # test redirecting to a python substitution with p-string + ( + """ +def _f(): + print('Wow Mom!') +aliases['f'] = _f +f > @(p'tttt') +with open('tttt') as tttt: + s = tttt.read().strip() +print('REDIRECTED OUTPUT: ' + s) +""", + "REDIRECTED OUTPUT: Wow Mom!\n", + 0, + ), # test system exit in function alias ( """ diff --git a/tests/test_pipelines.py b/tests/test_pipelines.py index 1cf35d965..24f646f02 100644 --- a/tests/test_pipelines.py +++ b/tests/test_pipelines.py @@ -74,7 +74,6 @@ def patched_events(monkeypatch, xonsh_events, xonsh_session): ("!(echo hi | grep x)", "", "", ""), ), ) -@pytest.mark.flaky(reruns=3, reruns_delay=2) def test_command_pipeline_capture(cmdline, stdout, stderr, raw_stdout, xonsh_execer): pipeline: CommandPipeline = xonsh_execer.eval(cmdline) assert pipeline.out == stdout diff --git a/xonsh/procs/specs.py b/xonsh/procs/specs.py index 267a6c6e7..6a9df4d96 100644 --- a/xonsh/procs/specs.py +++ b/xonsh/procs/specs.py @@ -223,6 +223,9 @@ def _parse_redirects(r, loc=None): def _redirect_streams(r, loc=None): """Returns stdin, stdout, stderr tuple of redirections.""" + if isinstance(loc, list): + raise Exception(f"Unsupported redirect: {r!r} {loc!r}") + stdin = stdout = stderr = None no_ampersand = r.replace("&", "") # special case of redirecting stderr to stdout @@ -671,7 +674,21 @@ class SubprocSpec: """Weave a list of arguments into a command.""" resolved_cmd = [] for c in self.cmd: - resolved_cmd += c if isinstance(c, list) else [c] + if ( + isinstance(c, tuple) + and len(c) == 2 + and isinstance(c[1], list) + and len(c[1]) == 1 + ): + # Redirect case e.g. `> file` + resolved_cmd.append( + ( + c[0], + c[1][0], + ) + ) + else: + resolved_cmd += c if isinstance(c, list) else [c] self.cmd = resolved_cmd def resolve_redirects(self):