Subprocess

Xonsh provides several operators for launching subprocesses, each with different capturing and blocking behavior. Choosing the right one depends on whether you need the output, whether the process is interactive, and what return type you expect.

$(cmd) – captured stdout

Runs cmd, captures stdout and returns it as a string. Nothing is printed to the screen:

$(whoami)
# 'user'

id $(whoami)          # use captured output as an argument
# uid=501(user) gid=20(staff)

output = $(echo -e '1\n2\r3 4\r\n5')
output
# '1\n2\n3 4\n5\n'

Use command decorators to change the return format:

$(@lines ls /)
# ['/bin', '/etc', '/home']

$(@json curl https://example.com/data.json)
# {'key': 'value'}

See Command Decorators for the full list (@lines, @json, @jsonl, etc.).

!(cmd) – captured object

In fact every subprocess command in xonsh is executed through a CommandPipeline – the central object that manages process execution, piping, stdout/stderr capturing, and return codes.

!(cmd) operator captures stdout and stderr and returns a CommandPipeline. The object is truthy when the return code is 0, and iterates over lines of stdout.

Important

This operator is non-blocking – it does not wait for the process to finish. To get the output, access .out, .rtn, call .end(), or convert to str, which forces the process to complete.

r = !(ls /)
r.output            # '' -- process may not have finished yet
r.end()             # block until done
r.output            # 'bin\netc\n...'

r = !(ls /)
r.out               # .out forces ending
# 'bin\netc\n...'

Non-blocking pattern with a worker:

worker = !(sleep 3)                   # returns immediately
echo 'doing other work...'
if worker.rtn == 0:                   # .rtn blocks until done
    echo 'worker finished successfully'

Note

Because the terminal is detached, this operator can only be used for non-interactive tools. Running !(ls | fzf) or !(python -c "input()") will cause the process to be suspended by the OS. Use $(cmd), $[cmd], or ![cmd] for interactive tools.

![cmd] – uncaptured hidden object

Streams stdout and stderr to the screen and returns a HiddenCommandPipeline. This is the operator used under the hood when you type a plain command at the interactive prompt (cmd is the same as ![cmd]).

r = ![echo hello]
# hello                   <- streamed to terminal
r.returncode
# 0

The .out attribute is empty by default. Set $XONSH_CAPTURE_ALWAYS = True to capture output even in this mode:

with @.env.swap(XONSH_CAPTURE_ALWAYS=True):
    r = ![echo hello]
    # hello               <- still streamed
    r.out
    # 'hello\n'           <- also captured

Checking return status with the walrus operator:

if r := ![ls NO]:
    print(f'OK, code: {r.returncode}')
else:
    print(f'FAIL, code: {r.returncode}')
# ls: cannot access 'NO': No such file or directory
# FAIL, code: 2

$[cmd] – uncaptured

Streams stdout and stderr directly to the terminal and returns None. The output always goes to the real terminal, even when $[cmd] is called from inside a callable alias or other captured context – the subprocess inherits the raw OS file descriptors, bypassing any Python-level redirection.

Use this for interactive or uncapturable processes (e.g. editors):

ret = $[echo 123]
# 123    # output directly
repr(ret)
# 'None'

@aliases.register
def _configure():
    me = $(whoami)
    echo @(me) > /tmp/config
    $[vim /tmp/config]

configure

@$(cmd) – captured inject

Runs cmd, captures stdout, splits it using Lexer.split() (shell-aware, respects quoting), and injects the resulting tokens as separate arguments:

@ showcmd @$(echo -e '1\n2\r3 4\r\n5')
# ['1', '2\r3', '4', '5']

You can use the same function directly to split any command string:

from xonsh.parsers.lexer import Lexer
Lexer().split('echo "hello world" file.txt')
# ['echo', '"hello world"', 'file.txt']

Threading

Xonsh has a threading prediction mechanism that allows it to understand which commands can be captured. For example, echo has no interaction with the user and is capturable. However, some tools have mixed behavior – they can be run for either interactive or non-interactive tasks. The best example is ssh, which allows for remote terminal sessions and executing commands.

To handle different types of tasks, xonsh has the @thread and @unthread built-in decorator aliases. If you need to capture the output from an interactive tool that has a capturable mode, use @thread:

@ !(@thread ssh host -T 'echo remote')
CommandPipeline(output="remote")

Without @thread, ssh would be predicted as unthreadable (because it is normally interactive) and the captured operator would not be able to collect its output.

Conversely, @unthread forces a command to run in the foreground without threading – useful when a normally threadable command needs terminal access (e.g. entering a password prompt).

Summary table

Operator

Blocking

Capture stdout

Capture stderr

TTY input

TTY output

Raise

Returns

$(cmd)

yes

yes

no

yes

no for thread

yes

str (stdout)

!(cmd)

no

yes for thread

yes for thread

no

no for thread

no

CommandPipeline

![cmd]

yes

no

no

yes

no for threadable

yes

HiddenCommandPipeline

$[cmd]

yes

no

no

yes

yes

yes

None

@$(cmd)

yes

yes

no

yes

no for thread

yes

list

What all this means:

  • Blocking – whether xonsh waits for the process to finish before continuing.

  • Capture stdout – whether stdout is captured into a CommandPipeline object instead of being streamed to the terminal.

  • Capture stderr – whether stderr is captured into a CommandPipeline object.

  • TTY input – whether the process receives terminal input (stdin). Without it, interactive tools (e.g. fzf, vim) will be suspended by the OS.

  • TTY output – whether stdout is connected directly to the terminal. “no (threadable)” means the stream is redirected for threadable processes.

  • Raise – whether a non-zero return code raises CalledProcessError (when $RAISE_SUBPROC_ERROR is True).

  • Returns – the Python type of the value returned by the operator.

A thread (threadable, capturable) process is one that does not interact with the user. If an unthreadable process runs with a detached terminal it will be suspended by the OS automatically.

See also