Subprocess Error Handling¶
Xonsh treats shell commands as first-class code. When a command fails,
you usually want your script to stop instead of silently marching
past the failure — the way a Python exception would — but you also want
the flexibility of &&/|| short-circuit logic that
the shell is built around.
This page walks through the rules xonsh uses to decide when a failing
subprocess raises a subprocess.CalledProcessError, how those rules
interact with pipes, logical operators, captured forms and per-command
decorators, and how the interactive prompt displays (or hides) the
resulting exception.
Chains¶
To describe where exceptions are raised we use the word chain for a grouping of subprocess commands that produces a single result:
single chain — one plain command:
ls filepipe chain — commands joined with
|:echo 1 | grep 1 | headlogical chain — commands joined with
&&/||:ls file1 || ls file2 || echo gone
Statements separated by ; or newlines are independent chains.
For example, echo 1 && echo 2 ; echo 3 ; echo 1 | grep 1 contains
three chains.
Xonsh decides whether to raise per chain, not per individual command,
which is what gives ||/&& their shell-like rescue semantics:
@ ls /no || echo rescued # one chain, rescued by ||
ls: cannot access '/no': No such file or directory
rescued # no exception
@ echo 1 && ls /no && echo never # one chain, ends on a failing ls
1
ls: cannot access '/no': No such file or directory
# CalledProcessError (the chain as a whole failed)
@ echo a ; ls /no ; echo b # three chains; second fails, third never runs
a
ls: cannot access '/no': No such file or directory
# CalledProcessError on the second chain
Environment variables¶
Xonsh exposes three knobs for error handling. The first two control whether a subprocess failure raises; the third controls display of the exception at the interactive prompt.
$XONSH_SUBPROC_RAISE_ERROR — default True¶
Raises subprocess.CalledProcessError when the final result of a
chain is a non-zero exit code. This is the rule that makes xonsh
scripts behave like Python: a failing command stops execution unless
you explicitly rescue it with || or @error_ignore.
ls /no→ raises (single chain, failed)ls /no | grep root→ raises (pipe chain, final stage failed)ls /no || echo ok→ no raise (logical chain rescued by||)echo 1 && ls /no→ raises (chain ends on failingls)(echo 1 && ls /etc) || echo fb→ no raise (outer||rescued by a successful inner chain)
$XONSH_SUBPROC_CMD_RAISE_ERROR — default False¶
Raises on every failing command regardless of chain context — any
non-zero exit is fatal. When True it short-circuits chain
semantics: ls /no || echo fb would raise on ls, the ||
fallback never runs.
Reserved for scripts that really want “fail fast on anything”. The old
name $RAISE_SUBPROC_ERROR is kept as a deprecated alias that syncs
to XONSH_SUBPROC_CMD_RAISE_ERROR.
$XONSH_PROMPT_SHOW_SUBPROC_ERROR — default False¶
This is a display flag — it does not change whether an exception
is raised, only whether the interactive prompt prints xonsh’s
subprocess.CalledProcessError: ... line after the command’s own
stderr. The command’s own stderr is always visible because
the subprocess writes it directly to the terminal.
False(default) — interactive prompt stays quiet after a failed command. You see the command’sstderr, the prompt returns, and$LAST_RETURN_CODEreflects the failure.True— restores the historical behavior of printingsubprocess.CalledProcessError: Command '...' returned non-zero exit status N.under the command’s own output.
Non-interactive scripts (./script.xsh, xonsh -c) are
unaffected — they always show the exception, because in script
mode you usually want to know which line blew up.
The per-command @error_raise decorator always shows the
exception regardless of this flag — it is the explicit per-command
opt-in.
Captured subprocess !() is the only exemption¶
Every subprocess form raises on a non-zero return code by default —
bare commands, ![...], $[...], $(...), @$(...). The
only exception is the full-capture form !(...): it returns a
CommandPipeline object and xonsh leaves error handling entirely
up to you.
@ ls nofile # exception
@ $(ls nofile) # exception
@ $[ls nofile] # exception
@ ![ls nofile] # exception
@ !(ls nofile) # no exception — returns a CommandPipeline
!(...) is designed to be tested directly, because
CommandPipeline is truthy when the command succeeded and falsy
when it failed:
@ if !(ls nofile):
print("found")
else:
print("absent")
absent
If you want a specific !(...) call to raise anyway, use the
@error_raise decorator inside it — the decorator wins over the
!() exemption:
@ if !(@error_raise ls nofile):
print("found")
# CalledProcessError — @error_raise overrides the !() exemption
Per-command decorators¶
Two decorator aliases give you an escape hatch that is scoped to a single command inside a larger chain.
@error_raise — always raise¶
Force subprocess.CalledProcessError on this command, regardless of
$XONSH_SUBPROC_RAISE_ERROR, $XONSH_SUBPROC_CMD_RAISE_ERROR, or
whether the command sits inside a normally-rescuing chain:
@ @error_raise ls /no || echo fb
# CalledProcessError — @error_raise wins over ||
@ !(@error_raise ls /no)
# CalledProcessError — @error_raise wins over !() exemption too
It also always shows the exception at the interactive prompt,
overriding $XONSH_PROMPT_SHOW_SUBPROC_ERROR = False.
@error_ignore — never raise¶
The inverse — the command never raises, no matter the environment settings or chain position:
@ @error_ignore ls /no
ls: cannot access '/no': No such file or directory
# no exception
@ echo 1 && @error_ignore ls /no && echo 2
echo 3
1
ls: cannot access '/no': No such file or directory
3
# no exception; the `@error_ignore` also makes the chain
# treat the failing ls as "don't raise on this final operand"
@error_ignore is especially handy when you want a command to
contribute to the chain’s return code but not its error behavior:
@ echo 1 | @error_ignore grep pattern | wc -l
0
# grep returns 1 (no match), @error_ignore keeps the pipe quiet,
# wc happily prints 0
Catching the exception¶
The raised exception is a plain subprocess.CalledProcessError, so
the usual Python idioms work:
@ import subprocess
@ try:
ls /no
except subprocess.CalledProcessError as e:
print("rc =", e.returncode)
print("cmd =", e.cmd)
ls: cannot access '/no': No such file or directory
rc = 2
cmd = ['ls', '/no']
For scoped overrides, env.swap is often cleaner than catching:
@ with @.env.swap(XONSH_SUBPROC_RAISE_ERROR=False):
ls /no
ls: cannot access '/no': No such file or directory
# no exception, even though the default is True
For captured iteration there is also CommandPipeline.itercheck()
which raises XonshCalledProcessError (a subclass of
subprocess.CalledProcessError that additionally carries
.completed_command and .stderr):
@ try:
for line in !(grep -R TODO src/).itercheck():
print(line.strip())
except XonshCalledProcessError as e:
print("grep failed with rc", e.returncode)
Interactive prompt behavior in detail¶
With the default $XONSH_PROMPT_SHOW_SUBPROC_ERROR = False:
@ ls /no
ls: cannot access '/no': No such file or directory
@ echo $LAST_RETURN_CODE
2
@ echo hi && ls /no && echo bye
hi
ls: cannot access '/no': No such file or directory
@
With $XONSH_PROMPT_SHOW_SUBPROC_ERROR = True:
@ $XONSH_PROMPT_SHOW_SUBPROC_ERROR = True
@ ls /no
ls: cannot access '/no': No such file or directory
subprocess.CalledProcessError: Command '['ls', '/no']' returned non-zero exit status 2.
@
@error_raise always shows:
@ @error_raise ls /no
ls: cannot access '/no': No such file or directory
subprocess.CalledProcessError: Command '['@error_raise', 'ls', '/no']' returned non-zero exit status 2.
See also¶
Tutorial — the scripting section discusses
$XONSH_SUBPROC_RAISE_ERRORin the context of writing robust xonsh scripts.Built-in Aliases — for
@error_raise/@error_ignoreand other decorator aliases.