Callable Aliases¶
In xonsh, a Python function can be registered as a shell command (alias). The
function declares which arguments it needs, and xonsh fills them automatically
based on parameter names. When a function runs as an alias, xonsh redirects
sys.stdout and sys.stderr inside it, making it possible to capture all
output that happens within the function — including print() calls and
subprocess commands.
Signature Parameters¶
Callable aliases receive arguments matched by name. You can request any combination of the following parameters in any order:
Parameter |
Description |
|---|---|
|
List of strings — command-line arguments passed to the alias. |
|
File-like object for reading piped input, or |
|
File-like object for writing standard output. |
|
File-like object for writing standard error. |
|
|
|
List of |
|
The name under which the alias was registered. |
|
The name actually used to invoke the alias (may differ if one alias points to another). |
|
A local environment overlay dict. Values set here shadow the global env during alias execution and are visible to subprocesses. Removed automatically when the alias exits. When you set env[‘VAR’]=1 it’s like with @.env.swap(VAR=1) for the rest of the callable alias code. |
You only need to declare the parameters you actually use:
@ @aliases.register
def _hello():
print('hello')
@ hello
hello
@ @aliases.register
def _upper(args, stdin=None):
text = stdin.read() if stdin else ' '.join(args)
print(text.upper())
@ upper hello world
HELLO WORLD
@ echo quiet | upper
QUIET
@ @aliases.register
def _shout(args, stdin=None, stdout=None):
"""Read stdin, write to stdout."""
for line in stdin or []:
stdout.write(line.upper())
@ echo 'hello\nworld' | shout
HELLO
WORLD
Alias Name and Called Alias Name¶
When one alias points to another, it can be useful to know how the alias was
invoked. The alias_name and called_alias_name parameters provide this:
@ @aliases.register('groot')
def _groot(alias_name=None, called_alias_name=None):
print(f'I am {alias_name}! You called me {called_alias_name}.')
@ aliases['tree'] = 'groot'
@ groot
I am groot! You called me groot.
@ tree
I am groot! You called me tree.
alias_name is set at registration time and never changes.
called_alias_name is set at each invocation and reflects the name the user
actually typed.
Local Environment Overlay¶
The env parameter provides a local environment overlay. Values set in
env shadow the global environment during alias execution — both for xonsh
$VAR reads and for subprocesses. When the alias exits, the overlay is
removed and the global environment is unchanged.
Direct writes to $VAR or @.env modify the global environment as usual
and persist after the alias exits:
@ @aliases.register
def _ca(env=None):
$GLOBAL = 2
echo GLOBAL before overlay = $GLOBAL
env['GLOBAL'] = 1
echo GLOBAL after overlay = $GLOBAL
printenv GLOBAL
@ ca
GLOBAL before overlay = 2
GLOBAL after overlay = 1
1
@ echo GLOBAL after alias = $GLOBAL
GLOBAL after alias = 2
Inside the alias, $GLOBAL returns 1 (overlay has priority) and
subprocesses see GLOBAL=1 in their environment. After the alias exits,
$GLOBAL is back to 2 (the global value set by $GLOBAL = 2).
Return Command Aliases¶
The @aliases.return_command decorator creates aliases that return a new
command to execute instead of running it themselves. The body of the alias
can run its own commands first, then return the command xonsh should execute
on its behalf.
The alias may return its result in either of two forms:
1. A non-empty list — just the command tokens. The returned command has no env overlay; if you need to set env vars for it you must use the dict form below.
@ @aliases.register
@aliases.return_command
def _rca(args):
return ['xonsh', '-c', 'echo hello']
2. A dict with a required "cmd" key (non-empty list of tokens) and
an optional "env" key (dict) — the command tokens plus an env overlay
that applies only to the returned command.
@ @aliases.register
@aliases.return_command
def _rca(args):
return {
'cmd': ['xonsh', '-c', 'echo $RETURNED'],
'env': {'RETURNED': 'set_by_dict'},
}
@ rca
set_by_dict
@ $RETURNED
Unknown environment variable: $RETURNED
The env= kwarg of a return_command alias behaves exactly like the
env= kwarg of an ordinary callable alias: it is a local overlay active
only during the function body. Mutating it affects commands the alias runs
inline (e.g. via $[...], !(), or subprocess syntax), but it does
not flow to the returned command. To set env for the returned command,
the alias must use the dict form above.
@ @aliases.register
@aliases.return_command
def _rca(args, env=None):
env['BODY_ONLY'] = 'visible_inside'
# A subprocess spawned here sees BODY_ONLY=visible_inside
$[env | grep BODY_ONLY]
# But the returned command does NOT — it has no overlay at all.
return ['env']
Direct writes to $VAR or @.env still modify the global environment
and persist after the alias exits, as for any callable alias.
The following example exercises all four env-flow paths of a
return_command alias in one place: the env= kwarg overlay (body-only),
a direct global write (persists), a dict-return "env" overlay (applies only
to the returned command), and the global value that flows through both.
@ $GLOBAL = 1
@ @aliases.register
@aliases.return_command
def _rca(env):
# ``env`` is the body-scoped overlay (introduced in 0.23.0).
# Mutating it affects commands the alias runs inline.
env['LOCAL'] = 1
xonsh -c @('echo g=$GLOBAL l=$LOCAL')
# Direct write to the global env — persists after the alias exits.
$GLOBAL = 2
return {
'cmd': ['xonsh', '-c', 'echo g=$GLOBAL l=$LOCAL'],
'env': {'LOCAL': 2},
}
@ rca
# xonsh inside the alias body:
# g=1 from the global $GLOBAL set before the alias
# l=1 from the ``env=`` kwarg overlay (body-scoped)
g=1 l=1
# the returned xonsh command:
# g=2 from the direct write ``$GLOBAL = 2`` in the body
# l=2 from the dict-return ``"env"`` overlay
g=2 l=2
@ $LOCAL
# the body overlay is gone,
# and the dict overlay only
# applied to the returned command
Unknown environment variable: $LOCAL
@ $GLOBAL
2 # the direct write persisted
Return Values¶
Callable aliases can return values in several forms:
@ @aliases.register
def _ret0():
return 0 # integer return code (0 = success)
@ ret0
@ $LAST_RETURN_CODE
0
@ @aliases.register
def _ret_str():
return 'hello from return'
@ $(ret-str)
'hello from return\n'
@ @aliases.register
def _ret_tuple():
return ('out text', 'err text', 1)
@ !(ret-tuple)
err text
CommandPipeline(returncode=1, output='out text\n', errors='err text\n')
Anything printed with print() inside the function is also captured
automatically.
Capturing and Stream Redirection¶
Inside a callable alias, sys.stdout and sys.stderr are temporarily
replaced with the alias’s own streams. The stdout and stderr function
arguments point to the same redirected streams. This means that
print() and print(file=stdout) are equivalent — both write to the
alias’s captured output, not directly to the terminal:
# These are all the same stream inside a callable alias:
sys.stdout is stdout # True
sys.stderr is stderr # True
print("x") # goes to alias stdout
print("x", file=stdout) # same
stdout.write("x\n") # same
@ @aliases.register
def _demo(args, stdout=None, stderr=None):
print("via print()")
stdout.write("via stdout.write()\n")
echo "via subprocess"
print("error", file=stderr)
@ output = $(_demo)
error
@ print(output)
via print()
via stdout.write()
via subprocess
Here is a more complete example showing how different output methods behave under capture:
@ @aliases.register
def _printer(args, stdin, stdout, stderr):
"""Ultimate printer."""
print("print out")
print("print err", file=@.imp.sys.stderr)
print("print out alias stdout", file=stdout)
print("print err alias stderr", file=stderr)
echo @("echo out")
echo @("echo err") o>e
$(echo @("$() echo out"))
$(echo @("$() echo err") o>e)
!(echo @("!() echo out"))
!(echo @("!() echo err") o>e)
![echo @("![] echo out")]
![echo @("![] echo err") o>e]
$[echo @("$[] echo out")]
$[echo @("$[] echo err") o>e]
execx('echo "execx echo out"')
execx('echo "execx echo err" o>e')
@ $(printer)
print err
print err alias stderr
echo err
$() echo err
![] echo err
$[] echo out
$[] echo err
execx echo err
'print out\necho out\nprint out alias stdout\n![] echo out\nexecx echo out\n'
@ !(printer)
$() echo err
$[] echo out
$[] echo err
CommandPipeline(
returncode=0,
output='print out\necho out\nprint out alias stdout\n![] echo out\nexecx echo out\n',
errors='print err\necho err\nprint err alias stderr\n![] echo err\nexecx echo err\n'
)
When the alias is captured with $() or !(), its stdout is collected.
When called uncaptured (bare command), output goes to the terminal as usual.
The stderr argument and sys.stderr are also redirected — use
sys.__stderr__ if you need to bypass capture:
@ @aliases.register
def _loud(args, stdin=None):
print("this is captured")
print("this goes to real terminal", file=@.imp.sys.__stderr__)
@ output = $(_loud)
this goes to real terminal
@ print(output)
this is captured
Streams¶
Inside a callable alias xonsh replaces sys.stdout and sys.stderr
with the alias’s own streams. The stdout and stderr function
arguments point to the same redirected streams. So bare print()
just works — in pipes, in capture, everywhere:
@aliases.register
def _greet(args):
print("hello") # goes to pipe, capture, or terminal — wherever needed
# all of these work:
greet # prints to terminal
greet | grep hello # piped to grep
output = $(greet) # captured into variable
The stdout / stderr arguments give you the same stream as a file
object, which is useful when you need to pass it to functions that accept
a file argument, or when you need .write() for finer control:
@aliases.register
def _json_dump(args, stdout=None):
import json
data = {"key": "value"}
json.dump(data, stdout) # json.dump needs a file object
stdout.write("\n")
Reading from stdin
stdin is None when the alias is called standalone, and a readable
stream when piped into. This is the one argument you must use explicitly
— there is no automatic redirection of sys.stdin for aliases:
@aliases.register
def _upper(args, stdin=None):
if stdin is not None:
for line in stdin:
print(line.strip().upper())
@ echo hello | upper
HELLO
Binary data — bypassing universal newlines
stdin, stdout, and stderr are text streams
(io.TextIOWrapper). Reading them applies universal newlines to
incoming bytes: \r\n and lone \r are translated to \n. That
is convenient for text but can corrupts binary data flowing through the
alias — every CR is silently rewritten.
To pass bytes through unchanged, use the underlying .buffer on both
ends:
@aliases.register
def _passthru(args, stdin=None, stdout=None):
"""Copy stdin to stdout byte-for-byte."""
if stdin is None:
return
for chunk in iter(lambda: stdin.buffer.read(65536), b""):
stdout.buffer.write(chunk)
stdout.buffer.flush()
@ cat /usr/bin/python | passthru > /tmp/copy
@ # bytes preserved exactly — diff /usr/bin/python /tmp/copy is empty
This applies symmetrically: stdout.write(text) goes through the
text layer (and on Windows translates \n to \r\n), while
stdout.buffer.write(b"...") writes raw bytes. Reach for
.buffer whenever the alias is a passthrough or otherwise
binary-aware — cat-likes, hashing, compression, image/audio
filters, ssh-style streaming.
Reading a file directly with open(path, "rb") is also unaffected:
the text layer only applies to stdin/stdout/stderr. So
mycat file > out (no upstream pipe) is safe; something | mycat
> out needs stdin.buffer to stay byte-clean.
Threading¶
By default, callable aliases run in a separate thread so they can be used in pipelines and run in the background. This is usually what you want.
However, some aliases need to run on the main thread — for example,
interactive tools (vim, less, htop), debuggers, profilers, or anything that
modifies terminal state. Use the
unthreadable decorator to force foreground execution.
Threading and capturability can also be controlled at call time using the
@thread, @unthread command decorators. These override the function’s
decorators for a single invocation:
@ @unthread my-alias # force main thread for this call
@ @thread my-alias # force background thread for this call
To set it permanently on the function, use the Python decorator:
@ @aliases.register
@aliases.unthreadable
def _vi(args, stdin=None):
vim @(args)
@ vi myfile.txt
# opens vim on the main thread
Capturability¶
By default, callable aliases are capturable — their output can be collected
with $(), !(), or piped to another command. This is how most aliases
work.
Some aliases launch interactive programs (editors, pagers, TUI apps) that
take over the terminal. These must not be captured, or the program will not
display correctly. Use @aliases.uncapturable, typically together with
@aliases.unthreadable:
@ @aliases.register
@aliases.uncapturable
@aliases.unthreadable
def _edit(args, stdin=None):
vim @(args)
@ edit myfile.txt
# opens vim directly in the terminal
Summary of default behavior:
Property |
Default |
Decorator to change |
|---|---|---|
Threading |
Threaded (runs in background thread) |
|
Capturing |
Capturable (output can be collected) |
|
Click Integration¶
If the click package is installed,
xonsh exposes two helpers on the aliases object:
aliases.click— theclickmodule itself, for decorating functions with@aliases.click.option(...),@aliases.click.argument(...), etc.aliases.register_click_command— a decorator that registers a click command as a xonsh alias.
Both are loaded lazily on first access — sessions that never touch click don’t pay the import cost, and nothing breaks on systems where click is not installed.
@ @aliases.register_click_command
@aliases.click.option('--count', default=1, help='Number of greetings.')
@aliases.click.option('--name', help='The person to greet.')
def _hello(ctx, count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for i in range(count):
print(name, file=ctx.stdout)
@ hello --count 3 --name World
World
World
World
The decorator mirrors the calling conventions of @aliases.register:
@aliases.register_click_command # bare, name from function
@aliases.register_click_command() # empty parentheses, same thing
@aliases.register_click_command("my-name") # explicit alias name
The function’s first argument is a click.Context subclass that carries
every standard xonsh alias parameter from Signature Parameters as an
attribute of the same name — ctx.stdin, ctx.stdout, ctx.stderr,
ctx.spec, ctx.stack, ctx.decorators, ctx.alias_name,
ctx.called_alias_name, ctx.env. The only exception is args,
which is exposed as ctx.alias_args to avoid clashing with the built-in
click.Context.args that click uses for option parsing.
The click module itself is also attached as ctx.click, so
callbacks can call ctx.click.echo(...), ctx.click.secho(...), etc.
without a separate import click.
Use these when a click command needs the underlying xonsh streams or
environment overlay — for example, print(text, file=ctx.stdout) writes
to the alias’s captured output the same way a regular callable alias does.
Tab completion is wired up automatically: option flags, click.Choice
option values, positional click.Choice arguments, and sub-commands of
a click.Group are all suggested without any extra configuration.
@ hello --<TAB>
--count --help --name
String Aliases and ExecAlias¶
When you assign a string to an alias, xonsh stores it in one of two ways depending on the content.
A simple string like "ls -la" is split into a list of tokens and
stored as ["ls", "-la"]. This is equivalent to assigning the list
directly:
@ aliases['ll'] = 'ls -la'
@ aliases['ll'] = ['ls', '-la'] # same thing
A string that contains xonsh expressions (@(), $()), pipes (|),
redirections (>, <), or logical operators (&&, ||) cannot be
represented as a simple list — it needs to be compiled and executed as xonsh
code. Xonsh wraps such strings in an ExecAlias, which is a callable alias
under the hood:
@ aliases['answer'] = 'echo @(21+21)'
@ answer
42
@ aliases['findpy'] = 'ls | grep $arg0'
@ findpy .py
@ aliases['combo'] = 'echo start && echo end'
@ combo
start
end
Arguments¶
When an ExecAlias runs, the arguments passed to it are available as
temporary environment variables:
$args— the full list of arguments.$arg0,$arg1, … — individual positional arguments.
These variables exist only while the alias body is running and are removed afterwards.
@ aliases['greet'] = 'echo Hello, $arg0!'
@ greet World
Hello, World!
@ aliases['piu'] = 'pip install -U @($args)'
@ piu xonsh prompt_toolkit
@ aliases['cdls'] = 'cd $arg0 && ls'
@ cdls /tmp
Arguments are not passed automatically — you need to use $args or
$arg<n> explicitly. If you don’t reference them, they are ignored:
@ aliases['noargs'] = 'echo @("arguments are ignored")'
@ noargs 1 2 3
arguments are ignored
@ aliases['withargs'] = 'echo the arguments are: @($args)'
@ withargs 1 2 3
the arguments are: 1 2 3
Equivalence with Callable Aliases¶
An ExecAlias is a shorthand for a callable alias. These three definitions
are equivalent:
@ aliases['answer'] = 'echo @(21+21)'
@ aliases['answer'] = lambda: $[echo @(21+21)]
@ @aliases.register
def _answer():
echo @(21+21)