Callable alias: fixed capturing stdout in case of redirect (#5527)

### Motivation

Closes #5512. 
The issue was introduced in #4445. It's needed to add checking the
redirect case.

### Before

```xsh
cd /tmp
@aliases.register('a')
def _a():
    print("1-hello")
    echo 2-hello
a > o.txt
# 2-hello
cat o.txt
# 1-hello
```

### After

```xsh
cd /tmp
@aliases.register('a')
def _a():
    print("1-hello")
    echo 2-hello
a > o.txt
cat o.txt
# 1-hello
# 2-hello
```

## For community
⬇️ **Please click the 👍 reaction instead of leaving a `+1` or 👍
comment**

---------

Co-authored-by: a <1@1.1>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Andy Kipp 2024-07-02 09:30:06 +02:00 committed by GitHub
parent 6b30c3aae3
commit 61a77ac3ce
Failed to generate hash of commit
3 changed files with 61 additions and 8 deletions

View file

@ -0,0 +1,23 @@
**Added:**
* <news item>
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* Callable alias: fixed capturing stdout in case of redirect e.g. `a > file` (#5527).
**Security:**
* <news item>

View file

@ -150,6 +150,25 @@ def test_capture_always(
assert exp in capfd.readouterr().out assert exp in capfd.readouterr().out
@skip_if_on_windows
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_callias_captured_redirect(xonsh_session, tmpdir):
@xonsh_session.aliases.register("a")
def _a(a, i, o, e):
print("print_stdout")
xonsh_session.subproc_captured_stdout(["echo", "cap_stdout"])
xonsh_session.subproc_captured_object(["echo", "cap_object"])
xonsh_session.subproc_captured_hiddenobject(["echo", "hiddenobject"])
xonsh_session.subproc_uncaptured(["echo", "uncaptured"])
print("print_error", file=e)
f = tmpdir / "capture.txt"
cmd = (["a", (">", str(f))],)
specs = cmds_to_specs(cmd, captured="hiddenobject")
_run_command_pipeline(specs, cmd).end()
assert f.read_text(encoding="utf-8") == "print_stdout\nhiddenobject\n"
@skip_if_on_windows @skip_if_on_windows
@pytest.mark.parametrize("captured", ["stdout", "object"]) @pytest.mark.parametrize("captured", ["stdout", "object"])
@pytest.mark.parametrize("interactive", [True, False]) @pytest.mark.parametrize("interactive", [True, False])

View file

@ -1021,19 +1021,30 @@ def cmds_to_specs(cmds, captured=False, envs=None):
# Apply boundary conditions # Apply boundary conditions
if not XSH.env.get("XONSH_CAPTURE_ALWAYS"): if not XSH.env.get("XONSH_CAPTURE_ALWAYS"):
# Make sure sub-specs are always captured. # Make sure sub-specs are always captured in case:
# I.e. ![some_alias | grep x] $(some_alias) # `![some_alias | grep x]`, `$(some_alias)`, `some_alias > file`.
specs_to_capture = specs if captured in STDOUT_CAPTURE_KINDS else specs[:-1] last = spec
for spec in specs_to_capture: is_redirected_stdout = bool(last.stdout)
if spec.env is None: specs_to_capture = (
spec.env = {"XONSH_CAPTURE_ALWAYS": True} specs
else: if captured in STDOUT_CAPTURE_KINDS or is_redirected_stdout
spec.env.setdefault("XONSH_CAPTURE_ALWAYS", True) else specs[:-1]
)
_set_specs_capture_always(specs_to_capture)
_update_last_spec(specs[-1]) _update_last_spec(specs[-1])
return specs return specs
def _set_specs_capture_always(specs_to_capture):
"""Set XONSH_CAPTURE_ALWAYS for all specs."""
for spec in specs_to_capture:
if spec.env is None:
spec.env = {"XONSH_CAPTURE_ALWAYS": True}
else:
spec.env.setdefault("XONSH_CAPTURE_ALWAYS", True)
def _shell_set_title(cmds): def _shell_set_title(cmds):
if XSH.env.get("XONSH_INTERACTIVE") and XSH.shell is not None: if XSH.env.get("XONSH_INTERACTIVE") and XSH.shell is not None:
# context manager updates the command information that gets # context manager updates the command information that gets