XONSH-CHEATSHEET(1)

NAME

xonsh-cheatsheetCheat sheet for xonsh shell with copy-pastable examples. The best doc for the new users.

SYNOPSIS

$apt install -y

INFO

294 stars
17 forks
0 views

DESCRIPTION

Cheat sheet for xonsh shell with copy-pastable examples. The best doc for the new users.

README

Cheat sheet for the xonsh shell with copy-pastable examples. This is a good level of knowledge to start being productive.

If you like the cheatsheet click ⭐ on the repo and tweet about it.

Full screen reading

What is xonsh?

Xonsh is a Python-powered, cross-platform, Unix-gazing shell language and command prompt. The language is a superset of Python 3.6+ with additional shell primitives that you are used to from Bash and IPython. It works on all Python-compatible systems, including Linux, macOS, and Windows. The xonsh shell is developed by a community of 300+ volunteers and the xonsh philosophy based on the principle of cooperation.

If you don't want to learn step by step jump to demo examples.

What does xonsh mean?

The word "xonsh" comes from conch - a common name of a number of different sea snails or shells (🐚, @). So "xonsh" is pronounced like "consh" ([kɑːnʃ]) which is a playful reference to the word "shell", often used to describe command shells. "Consh" is sometimes interpreted as "console shell".

Over time the approach to replace a letter in the words to "x" and pronounce as short /k/ when used with vowels (e.g. "xat" sounds like "cat" [kæt]) became the way to create unique names for xonsh related solutions e.g. xontrib, xonfig, xunter. Adding "x" in the beginning is also the way to create xonsh-related name e.g. xpip.

Fun fact: when you run xonsh on *nix in the home directory the default prompt looks like user@host ~ @ - it's a nice visual metaphor of snail (~) that lives in the conch (@) and the conch is the home for snail.

You can find more visuals around xonsh in xonsh-logo repository.

Install xonsh

The right way

Before xonsh 0.22.2 here was the section about installation. But now xonsh has Xonsh Installation General Guide that is awesome start. Below we just have a talk about understanding the environment.

Install xonsh with package and environment management system

Xonsh is a Python-based shell, and to run xonsh you must have Python installed. The Python version and its packages can be installed and located anywhere: in the operating system directories, as part of a virtual environment, as part of the user directory, or as a virtual drive created temporarily behind the scenes by the Linux AppImage.

The first thing you have to remember is that when you execute import or any other Python code during a xonsh session, it will be executed in the Python environment that was used to run the current instance of xonsh. Use the xcontext builtin alias to check the xonsh context.

In other words, you can activate a virtual environment during a xonsh session (using mamba, conda, rye, pyenv, pipx) but the current session will continue to use packages from the environment that was used to run xonsh. And if you want to run xonsh with the packages from the currently activated virtual environment you have to install xonsh in that environment and run it directly. Keep in mind current $PATH and as result which xonsh when running something.

Thus the second thing you should remember is that when you run xonsh in a virtual environment it will try to load xonsh RC files (i.e. ~/.xonshrc) and because the virtual environment is different from the environment you ordinarily use, the loading of the RC file will tend to fail because of the lack of the appropriate set of packages. When you write your ~/.xonshrc it's good practice to check the existing external dependencies before loading them. See also xontrib-rc-awesome.

Install xonsh on macOS or Linux using conda

Here is the real life example but mostly created for educational reasons. See the best way to install xonsh in the next section.

You can use Conda (or faster replacement - mamba) with Conda-forge to install and use xonsh.

#
# Install python using brew
#
zsh  # Default macOS shell
# Install brew from https://brew.sh/
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install python  # or `python@3.11`

Install Miniconda from https://docs.conda.io/en/latest/miniconda.html

(example for Mac, use the link for your platform)

cd /tmp wget https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh chmod +x Miniconda3-latest-MacOSX-arm64.sh ./Miniconda3-latest-MacOSX-arm64.sh

Add conda init code that was printed to ~/.zshrc and restart zsh.

Or run /Users/username/miniconda3/bin/conda init zsh to add init to ~/.zshrc and restart zsh.

After restarting zsh you will see (base) in prompt.

This means that you're in the conda base environment.

Switch to Conda-forge channel

conda config --add channels conda-forge conda config --set channel_priority strict conda update --all --yes

Install xonsh to the base environment

conda install xonsh conda init xonsh # Add init to ~/.xonshrc. You can also add $CONDA_AUTO_ACTIVATE_BASE='false' to avoid conda loading at start

which xonsh

/Users/username/miniconda3/bin/xonsh

Run xonsh from the base environment

xonsh

How to work and understand the environments in conda:

# `xpip` is used to install packages to the current xonsh session location (now it's `base` environment)
xpip install ujson  

Example of creating the environment with a certain version of Python

conda search python | grep 3.10 conda create -n "py310" python=3.10 xonsh

xcontext # xonsh >= 0.22.0

[Current xonsh session]

xpython: /Users/username/miniconda3/bin/python # which xpython: xonsh ran from base environment

xpip: /Users/username/miniconda3/bin/pip # which xpip: pip from base environment from where xonsh ran

[Current commands environment]

xonsh: /Users/username/miniconda3/bin/xonsh # which xonsh: xonsh installed in base environment

python: /Users/username/miniconda3/bin/python # which python: base environment

pip: /Users/username/miniconda3/bin/pip # which pip: base environment

conda activate py310

Now the environment is py310 but current xonsh session is still in base environment

xcontext

[Current xonsh session]

xpython: /Users/username/miniconda3/bin/python # which xpython: xonsh ran from base environment

xpip: /Users/username/miniconda3/bin/pip # which xpip: pip from base environment from where xonsh ran

[Current commands environment]

xonsh: /Users/username/miniconda3/envs/py310/bin/xonsh # which xonsh: xonsh installed in py310 environment

python: /Users/username/miniconda3/envs/py310/bin/python # which python: py310 environment

pip: /Users/username/miniconda3/envs/py310/bin/pip # which pip: py310 environment

Run xonsh that installed in py310 environment from xonsh session run in base environment

xonsh conda activate py310

Now xonsh session is in py310 environment and the current environment is also py310

xcontext

[Current xonsh session]

xpython: /Users/username/miniconda3/envs/py310/bin/python # which xpython: xonsh ran from py310 environment

xpip: /Users/username/miniconda3/envs/py310/bin/pip # which xpip: pip from py310 environment from where xonsh ran

[Current commands environment]

xonsh: /Users/username/miniconda3/envs/py310/bin/xonsh # which xonsh: xonsh installed in py310 environment

python: /Users/username/miniconda3/envs/py310/bin/python # which python: py310 environment

pip: /Users/username/miniconda3/envs/py310/bin/pip # which pip: py310 environment

import ujson

No module named 'ujson' # YES because ujson was installed in base environment

On Mac we also recommend installing GNU coreutils to use the Linux default tools (i.e. ls, grep):

brew install coreutils
$PATH.append('/opt/homebrew/opt/coreutils/libexec/gnubin')  # add to ~/.xonshrc

How to understand the xonsh location

Which xonsh and which Python used to run the current xonsh session:

import sys
[sys.argv[0], sys.executable]
# ['/opt/homebrew/bin/xonsh', '/opt/homebrew/opt/python@3.11/bin/python3.11']

@(sys.executable) -m site

Full info about paths

Which xonsh and which python that will be executed to run new instances depends on the list of directories in $PATH or virtual environment:

$PATH
# ['/home/user/miniconda3/bin', '/opt/homebrew/bin]

[$(ls -la @$(which xonsh)), $(ls -la @$(which python)), $(python -V)]

['/home/user/miniconda3/bin/xonsh', '/home/user/miniconda3/bin/python -> python3.11', 'Python 3.11.1']

python -m site

Full info about paths

pipx and xonsh

The pipx tool is also good for installing xonsh in case you need a certain Python version:

# Install Python before continuing
pip install pipx
pipx install --python python3.8 xonsh  # Here `python3.8` is the path to installed python. 
pipx run xonsh 
# or add /home/$USER/.local/bin to PATH (/etc/shells) to allow running just the `xonsh` command

If you have python but no pip on your system

If you have python but no pip just install it using ensurepip:

python -m ensurepip --upgrade
pip -V
pip install 'xonsh[full]'

Try xonsh without installation

Container

Using open source Podman is recommended but docker also ok.

# Container with specific Python version and latest release of xonsh
podman run --rm -it python:3.11-slim /bin/bash \
 -c "pip install 'xonsh[full]' && xonsh"

Container with specific Python version and xonsh from the master branch

podman run --rm -it python:3.11-slim /bin/bash
-c "apt update && apt install -y git && pip install -U git+https://github.com/xonsh/xonsh && xonsh"

Official xonsh container image may have an old version

podman run --rm -it xonsh/xonsh:slim

Linux-portable AppImage contains both Python 3 and xonsh in one file

wget https://github.com/xonsh/xonsh/releases/latest/download/xonsh-x86_64.AppImage -O xonsh
chmod +x xonsh
./xonsh

Then if you don’t have Python on your host, you can access it from the AppImage by running:

$PATH = [$APPDIR + '/usr/bin'] + $PATH python -m pip install tqdm --user # the tqdm package will be installed to ~/.local/ import tqdm

You can build your own xonsh AppImage with the packages you need in 15 minutes.

Xonsh basics

The xonsh language is a superset of Python 3 with additional shell support. As a result, you can mix shell commands and Python code as easily as possible. Right off the bat examples:

cd /tmp && ls                     # shell commands

21 + 21 # python command

for i in range(0, 42): # mix python echo @(i+1) # and the shell

len($(curl https://xon.sh)) # mix python and the shell

$CONCH='snail' ls # shell style setting env variable for command

with @.env.swap(CONCH='snail'): # or using context manager echo $CONCH

with p'/tmp/dir'.mkdir().cd(): # make directory touch tmpfile.txt # and operate inside

$PATH.append('/tmp') # PATH is list

p'/etc/passwd'.read_text().find('root') # path-string returns Path # (https://docs.python.org/3/library/pathlib.html)

for line in $(cat /etc/passwd).splitlines(): # read the lines from the output echo @(line.split(':')[0]) # prepare line on Python and echo

for file in gp*.*: # reading the list of files as Path-objects if file.exists(): # using rich functionality of Path-objects du -sh @(file) # and pass it to the shell command

import json # python libraries are always at hand if info := $(podman info --format '{{json .}}'): print('ContainersRunning:', json.loads(info)['ContainersRunning'])

@.imp.json.loads($(echo '{"a":1}')) # xonsh inline importer

xpip install xontrib-dalias && xontrib load dalias y = $(@yaml dig +yaml google.com) # convert output into object y[0]['message']['query_time']

podman exec -it @($(@json podman ps --format json)['ID']) bash

Finally fork https://github.com/anki-code/xontrib-rc-awesome

to convert your ~/.xonshrc into a pip-installable package

with the extensions you need on board.

Looks nice? Install xonsh!

Most frequent things that newcomers totally overlook

1. Shell commands, also known as subprocess commands

The first thing you should remember is that the shell commands are not the calls of another shell (i.e. bash). Xonsh has its own parser implementation for subprocess commands, and this is why a command like echo {1..5} \; (brace expansion and escape characters in bash) won't work. Most sh-shell features can be replaced by sane Python alternatives. For example, the earlier command could be expressed as echo @(range(1,6)) ';'.

If you think that only xonsh has the sh-uncompatible elements in its parser, you are mistaken. If we compare Bash and Zsh we will find that pip install package[subpackage] command will work in Bash but in Zsh the error will be raised because Zsh has a special meaning for square braces. It's normal to have an evolution in the syntax and features.

Be calm and accept the sane and self-consistent Python-driven mindset.

Note:

  • Most novices try to copy and paste sh-lang commands that contain special characters and get syntax errors in xonsh. If you want to run environment agnostic sh-lang's commands that you copy from the internet just use the macro call in xonsh bash -c! echo {123} or use xontrib-sh to run context-free bash commands in xonsh by adding ! at the beginning of the command.
  • We highly recommend to taking a look at the section Install xonsh with package and environment management system.

2. Strings and arguments in shell commands

The second potential misunderstanding comes from the first. Use quotes to escape special characters, the special meaning of braces, or pass a string as an argument. When in doubt, use quotes!

You should clearly understand the difference:

sh-lang shellsxonsh
1. Has an escape character:
echo 123\ 456
# 123 456
1. Use quotes:
echo "123 456"
# 123 456
Escape character to wrap and so on:
echo "123\
456"
# 123456
2. Open the quotes:
echo --arg="val"
# --arg=val
# and:
echo --arg "val" # --arg val

2. Save quotes:
echo --arg="val"
# --arg="val"
# But if argument quoted entirely:
echo --arg "val" # --arg val
3. Brackets have no meaning:
echo {123} [456]
# {123} [456]


3. Brackets have meaning:
echo {123} [456]
# SyntaxError
echo "{123}" '[456]' # {123} [456]

Note:

  • You can wrap any argument into Python string substitution:
    name = 'snail'
    echo @('--name=' + name.upper())
    # --name=SNAIL
    
  • You can use the showcmd command to show the arguments list:
    showcmd echo The @('args') @(['list', 'is']) $(echo here) "and" --say="hello" to @([]) you
    # ['echo', 'The', 'args', 'list', 'is', 'here', 'and', '--say="hello"', 'to', 'you']
    

3. The process substitution operator $() returns output with universal new lines

In sh-compatible shells, the process substitution operator $() executes the command and then splits the output and uses those parts as arguments. The command echo $(echo -e "1 2\n3") will have three distinct arguments, 1, 2 and 3 that will passed to the first echo.

In xonsh shell the $() operator is smarter (xonsh >= 0.17.0):

  • Return the line if it's single line e.g. $(whoami) will return 'user'.
  • Return universal new lines for multiple lines e.g. $(ls) will return '1\n2\n3\n'.
  • Finally you can use xontrib-dalias to have a list of lines e.g. l = $(@lines cat file).

Note:

  • To do what sh-compatible shells are doing with the $() operator, the xonsh shell has the @$() operator that will be described in the next chapter.
    showcmd echo @$(echo "1\n2 3\n4")
    # ['echo', '1', '2', '3', '4']
    
  • To transform the output you can use python substitution e.g. use splitlines:
    showcmd echo @($(echo "1\n2 3\n4").splitlines())  # the first echo will get three arguments: "1", "2 3", "4"
    # ['echo', '1', '2 3', '4']
    

4. Threading

Xonsh runs subprocess commands or callable aliases using threading prediction mechanism that simply called "threading" or "(un)threadable" words. This threading prediction was introduced to have an ability to capture any output from processes that are completely non interactive e.g. echo or grep. When you run !(echo 1) the echo process will be predicted as thredable and current terminal will be detached and stdout, stderr and everything will be captured. How to change the predicted threading value using @thread and @unthread aliases you can find below.

Operators

Overview

Operators:

  • $() is to run processes and capture the stdout. Almost the same as in traditional shells.

  • !() is to run sync or async threadable (capturable) processes. The terminal is detached for the process in this mode to deliver non-blocking behavior. To block the process and wait for result use .end(), .out, .rtn and other attributes that forces getting the result.

  • ![] is to run processes without any capturing but return CommandPipeline with base info: pid, return code, timinig, etc. This operator is working when you run plain commands e.g. just echo hello.

  • $[] is to run processes without any capturing and any catching the result. Use it for uncapturable processes (e.g. vim) if you want to stream output directly to the terminal and without any capturing.

Examples:

id $(whoami)  # xonsh >= 0.17.0

worker1 = !(sleep 3) # Non-blocking. echo 'Something is happening while worker1 is working.' if worker1.rtn == 0: # Blocking. The .rtn attribute call has .end() under the hood. echo 'worker1 is done'

Note. There is issue with this case that will be fixed in xonsh > 0.19.0.

file = p'~/.xonshrc' if ![ls @(file)]: head @(file)

$[vim ~/.xonshrc]

From tech side (most of the behavior is dictated by OS):

OperatorBlockingCapture stdoutCapture stderrAttach TTY inputAttach TTY outputReturn
$()yesyesnoyesno for threadablestdout
!()noyes for threadableyes for threadablenono for threadableCommandPipeline
![]yesnonoyesno for threadableHiddenCommandPipeline
$[]yesstrict nonoyesyesNone

Here:

  • Threadable (capturable) process is the process without any interaction with user. Note that if unthreadable process will run with detached terminal it will be suspended by OS automatically.
  • Capturing "strict no" means that stream will be passed to the main terminal from any place of calling.

Note:

  • If you want to run interactive xonsh from bash script you need to have interactive shebang (i.e. #!/bin/bash -i) to avoid suspending by OS.

$() - capture and return output without printing stdout and stderr

Technical name of this operator: captured stdout. Python call: __xonsh__.subproc_captured_stdout().

Captures stdout and returns single line or miltiline output with universal new lines:

# xonsh >= 0.17.0

$(whoami) # Python mode

'user'

id $(whoami) # Subproc mode

uid=501(user) gid=20(staff)

showcmd $(echo -e '1\n2\r3 4\r\n5') # Subproc mode

['1\n2\n3 4\n5\n']

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

'1\n2\n3 4\n5\n'

You can change the behavior by setting $XONSH_SUBPROC_OUTPUT_FORMAT (xonsh >= 0.17.0):

$XONSH_SUBPROC_OUTPUT_FORMAT = 'list_lines'

$(ls /)

['/bin', '/etc', '/home']

!() - capture all and return object without printing stdout and stderr

Technical name of this operator: captured object or full capturing with non blocking mode. Python call: __xonsh__.subproc_captured_object()

Captures stdout and returns CommandPipeline. Truthy if successful (returncode == 0), compares to, iterates over lines of stdout:

ret = !(echo 123)
ret
#CommandPipeline(
#  pid=404136,
#  returncode=0,
#  args=['echo', '123'],
#  alias=None,
#  timestamps=[1604742882.1826484, 1604742885.1393967],
#  executed_cmd=['echo', '123'],
#  input='',
#  output='123\n',
#  errors=None
#)   

if ret: print('Success')
#Success

for l in ret: print(l)
#123

Note! This is non blocking operator: no waiting for enging output. To get the output you need to convert an object to a string, invoke .end(), ask for .rtn or use the .out to force ending the process and read output from internal buffers:

r = !(ls /)
r.output
# ''

r.end() r.output

'bin\netc\n...'

r = !(ls /) r.out # out is forcing ending

'bin\netc\n...'

r = !(ls /) print(r) # r will be converted to str and the ending will be forced

bin

etc

...

Note! When you're using full capturing the stdout and stderr will be captured and there will be no terminal (tty) connected. You can use this operator only for non interactive tools running. If you will do !(ls | fzf) or !(python -c "input()") the executed command will be suspended by POSIX OS (1, 2) because the process is waiting for input in background. Use uncaptured operators for interactive tools and read the futher materials around unthreadable mode to do things right.

$[] - not capturing (return None), print stdout and stderr

Technical name of this operator: uncaptured mode. Python call: __xonsh__.subproc_uncaptured().

Passes stdout to the screen and returns None:

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

This is the same as echo 123, but this syntax allows explicitly running a subprocess command.

![] - print stdout/stderr and return hidden object

Technical name of this operator: uncaptured hidden object. Python call: __xonsh__.subproc_captured_hiddenobject()

Note! The behavior may be different if $XONSH_CAPTURE_ALWAYS is True or False (default).

Passes stdout to the screen and returns HiddenCommandPipeline:

with @.env.swap(XONSH_CAPTURE_ALWAYS=False):  # Default.
    r = ![echo -e '1\n2\r3 4\r\n5']
    # 1               # Stream output of the command
    # 3 4
    # 5
    r               # No return value because it's HiddenCommandPipeline object
    r.out           
    # ''            # Empty because `$XONSH_CAPTURE_ALWAYS = False`.
    r.returncode
    # 0

with @.env.swap(XONSH_CAPTURE_ALWAYS=True): r = ![echo -e '1\n2\r3 4\r\n5'] # 1 # Stream output of the command # 3 4 # 5 r # No return value because it's HiddenCommandPipeline object r.out # But it has the properties from CommandPipeline # '1\n2\r3 4\n5\n' r.returncode # 0

Elegant checking the result of the command using walrus operator:

if r := ![ls NO]:
    print(f'It works! Return code: {r.returncode}')
else:
    print(f'It fails! Return code: {r.returncode}')

ls: cannot access 'NO': No such file or directory

It fails! Return code: 2

This operator is used under the hood for running commands at the interactive xonsh prompt.

@() - use Python code as an argument or a callable alias

Evaluates Python and passes the arguments:

showcmd 'Supported:' @('string') @(['list','of','strings']) 
#['Supported:', 'string', 'list', 'of', 'strings']

echo -n '!' | @(lambda args, stdin: 'Callable' + stdin.read()) #Callable!

@$() - split output of the command by white spaces for arguments list

Technical name: captured inject output. API call: __xonsh__.subproc_captured_inject()

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

This is mostly what bash's $() operator does.

Environment Variables

Three ways to get environment:

# Get the list of environment variables
@.env

Recommended for Python code and xontribs:

from xonsh.built_ins import XSH # Import current xonsh session. XSH.env # Get the list of environment variables using xonsh session (XSH).

Operating with environment variables:

$VAR = 'value'    # Set environment variable

env = @.env # short typing env.get('VAR', 'novalue') # the good practice to have a fallback for missing value

'value'

env.get('VAR2', 'novalue') # the good practice to have a fallback for missing value

'novalue'

'VAR' in env # Check environment variable exists #True

print($VAR) with @.env.swap(VAR='another value', NEW_VAR='new value'): # Change VAR for commands block print($VAR) print($VAR) #value #another value #value

$VAR='new value' xonsh -c r'echo $VAR' # Change variable for subprocess command #new value

@.env.get('VAR', 'novalue') # the way to get env variable with default value

'value'

Python and subprocess mode:

print("my home is $HOME")                        # Python mode
# my home is $HOME

print("my home is " + $HOME) # Python mode

my home is /home/snail

echo "my home is $HOME" as well as '$HOME' # Subprocess mode

my home is /home/snail as well as /home/snail

Work with $PATH:

$PATH
# EnvPath(
# ['/usr/bin',
#  '/sbin',
#  '/bin']
# )

$PATH.append('/tmp') # Append path '/tmp' at end of $PATH list $PATH.prepend('/tmp') # (xonsh>0.15.1) Insert path '/tmp' at front of $PATH list $PATH.insert(0, '/tmp') # Insert path '/tmp' to appropriate position of $PATH list $PATH.remove('/tmp') # Remove path '/tmp' (first match)

$PATH.add(p"/bin", front=True, replace=True)) # Insert path '/bin' at front of $PATH list and replace existing entries $PATH.add(p"/bin", front=True) # Insert path '/bin' at front of $PATH list $PATH.add(p"/bin", front=False, replace=True)) # Insert path '/bin' at end of $PATH list and replace existing entries

Setup local paths by prepending to path via a loop in .xonshrc:

user_bins = [
    p'~/.cargo/bin',
    p'~/.pyenv/bin',
    p'~/.poetry/bin',
    p'~/.local/bin', 
]

for dir in user_bins: if dir.is_dir() and dir.exists(): $PATH.add(dir, front=True, replace=True)

Register your variables:

@.env.register('MY_VAR1', type='int', default=0, doc='This is a demo variable 1')
@.env.register('MY_VAR2', type='int', default=0, doc='This is a demo variable 2')
$MY_<Tab>
# MY_VAR1 -> This is a demo variable 1
# MY_VAR1 -> This is a demo variable 2
$MY_VAR1
# 0

See also the list of xonsh default environment variables.

Aliases

Simple aliases

aliases['g'] = 'git status -sb'           # Add alias as string
aliases['gp'] = ['git', 'pull']           # Add alias as list of arguments
aliases['e'] = 'echo @(2+2)'              # Add xonsh executable alias (ExecAlias)
aliases['b'] = lambda: "Banana!\n"        # Add alias as simple callable lambda

aliases |= { # Add aliases from the dict (recommended) 'a': 'echo a', 'b': 'echo b', }

aliases['e'] = 'echo @($arg0) @($args[2:])' # Use $argN in ExecAlias e 1 2 3 4

1 3 4

del aliases['b'] # Delete alias

Easy wrapping a command by using ExecAlias with built-in $args (or $arg0, $arg1, etc) variable:

aliases['echo-new'] = "echo @($args) new"
$(echo-new hello)
# 'hello new\n'
$(echo-new -n hello)
# 'hello new'

Easy switch environment using alias (using xontrib-dalias is the right path):

aliases['lines'] = "$XONSH_SUBPROC_OUTPUT_FORMAT = 'list_lines'; echo $XONSH_SUBPROC_OUTPUT_FORMAT"
aliases['stream'] = "$XONSH_SUBPROC_OUTPUT_FORMAT = 'stream_lines'; echo $XONSH_SUBPROC_OUTPUT_FORMAT"
lines
# list_lines
$(ls)
# ['file1', 'file2', 'file3']
stream
# stream_lines
$(ls)
# 'file1\nfile2\nfile3\n'

Also with handy """-string to use " and ' without escaping:

aliases['scmd'] = """showcmd @([a for a in $args if a != "cutme"])"""

scmd

usage: showcmd [-h|--help|cmd args]

Displays the command and arguments as a list ...

scmd 1 2 cutme 3 #['1', '2', '3']

Function as an alias for group of commands

Ordinarily "alias" word refers to a subprogram that has exit code and arguments for subprocess mode. In xonsh you can group commands and reuse it as Python functions or classes:

def hello(name):
    echo hello @(name)

hello('Alex')

hello Alex

or class:

class my:
    @classmethod
    def hello(cls, name):
        echo hello @(name)

my.hello('Alex')

hello Alex

Alias that returns command

If you need to transform command use @aliases.return_command:

@aliases.register
@aliases.return_command
def _xsudo(args):
    """Sudo with expanding aliases."""
    return ['sudo', '--', *aliases.eval_alias(args)]

aliases['install'] = "apt install cowsay" xsudo install

Password:

Install cowsay

@aliases.register @aliases.return_command def _vi(args): """Universal vi editor.""" if $(which vim 2>/dev/null): return ['vim'] + args else: return ['vi'] + args

vi /etc/hosts

Note! Using alias that returns command is much more preferable than callable alias if you need to just change the command. Callable alias is a complex process wrapper and in case of choice between return command alias and callable alias the right choice is the first one.

Callable aliases

def _myargs1(args):
#def _myargs2(args, stdin=None):
#def _myargs3(args, stdin=None, stdout=None):
#def _myargs4(args, stdin=None, stdout=None, stderr=None):
#def _myargs5(args, stdin=None, stdout=None, stderr=None, spec=None):
#def _myargs6(args, stdin=None, stdout=None, stderr=None, spec=None, stack=None):
    print(args)
    # print(args, file=stdout)  # Using stdout directly is the best practice to support pipes/tests/future.

aliases['args'] = _myargs1 del _myargs1

args 1 2 3 #['1', '2', '3']

Simple definition with decorator:

@aliases.register
def _hello():
    echo world

hello

world

Read stdin and write to stdout (real-life example - xontrib-pipeliner):

# Add an exclamation point to each line
def _exc(args, stdin, stdout):
    for line in stdin.readlines():
        print(line.strip() + '!', file=stdout, flush=True)

aliases['exc'] = _exc

echo hello | exc

hello!

# JSON to YAML
@aliases.register("j2y")
def __j2y(args, stdin, stdout):
    import json, yaml
    print(yaml.dump(json.loads(stdin.read())), file=stdout)

# YAML to JSON
@aliases.register("y2j")
def __y2j(args, stdin, stdout):
    import yaml, json
    json.dump(yaml.safe_load(stdin), stdout, indent=4)

echo '{"hello":{"world":"42"}}' | j2y
# hello:
#   world: 42

echo 'hello:\n  world: 42' | y2j
# {
#     "hello": {
#         "world": "42"
#     }
# }

Capturing:

Callable aliases tend to be capturable. Only the explicitly denoted uncaptured subprocess operator $[] is uncapturable, and the subprocess's stdout passes directly through xonsh to the screen.

@aliases.register
def _hunter():
    print('catch me')
    echo if
    $[echo you]
    ![echo can]
hunter
# catch me
# if
# you
# can
$(hunter)
# you
# 'catch me\nif\ncan\n'

Calambur! The "callable alias" could be shortanized to "callias". The name Callias is primarily a gender-neutral name of Greek origin that means Beauty.

Decorator Alias

Using DecoratorAlias and callable output_format you can create transformer:

from xonsh.procs.specs import SpecAttrDecoratorAlias as dalias  # xonsh >= 0.18.0

aliases['@noerr'] = dalias({"raise_subproc_error": False}, "Set raise_subproc_error to False.") aliases['@lines'] = dalias({"output_format": 'list_lines'}, "Set list_lines output format.") aliases['@json'] = dalias({"output_format": lambda lines: @.imp.json.loads('\n'.join(lines))}, "Set json output format.") aliases['@path'] = dalias({"output_format": lambda lines: @.imp.pathlib.Path(':'.join(lines))}, "Set path output format.") aliases['@yaml'] = dalias({"output_format": lambda lines: @.imp.yaml.safe_load('\n'.join(lines))}, "Set yaml output format.")

Now you can:

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

$(@json echo '{"a":1}') # Try with curl ;)

dict({"a":1})

$(@path which xonsh)

Path('/path/to/xonsh')

$(@path which xonsh).parent

Path('/path/to')

aliases['ydig'] = '@yaml dig +yaml' # Update dig via brew install bind to have +yaml. y = $(ydig google.com) y[0]['type']

'MESSAGE'

$RAISE_SUBPROC_ERROR = True if ![@noerr ls nononofile]: # Do not raise exception in case of error. echo file

See also xontrib-dalias.

Abbrevs

There is xontrib-abbrevs as an alternative to aliases. You can create abbrev and set the position of editing:

xpip install xontrib-abbrevs
xontrib load abbrevs

abbrevs['gst'] = 'git status' gst # Once you hit <space> or <return> 'gst' gets expanded to 'git status'.

abbrevs['gp'] = "git push <edit> --force" # Set the edit position. abbrevs['@'] = "@(<edit>)" # Make shortcut. abbrevs['...'] = "cd ../.." # Workaround for syntax intersections with Python i.e. elepsis object from Python here.

You can set a callback that receives the current command buffer and the word that triggered abbrev

abbrevs['*'] = lambda buffer, word: "asterisk" if buffer.text.startswith('echo') else word ls * # will stay echo * # will be transformed to echo asterisk

Path strings

The p-string returns Path object:

path = p'~/.xonshrc'
path
# Path('/home/snail/.xonshrc')

[path.name, path.exists(), path.parent]

['.xonshrc', True, Path('/home/snail')]

[f for f in path.parent.glob('*') if 'xonsh' in f.name]

[Path('/home/snail/.xonshrc')]

dir1 = 'hello' dir2 = 'world' path = p'/tmp' / dir1 / dir2 / 'from/dir' / f'{dir1}' path

Path('/tmp/hello/world/from/dir/hello')

The best description of how string literlas is working is in the table from tutorial.

A simple way to read and write the file content using Path string:

text_len = p'/tmp/hello'.write_text('Hello world')
content = p'/tmp/hello'.read_text()
content
# 'Hello world'

Globbing - get the list of files from path by mask or regexp

To Normal globbing add g before back quotes:

ls *.*
ls g`*.*`

for f in gp/tmp/*.*: # p is to return path objects print(f.name)

for f in gp/tmp/*/**: # ** is to glob subdirectories print(f)

To Regular Expression Globbing add r before back quotes:

ls `.*`
ls r`.*`

for f in rp.*: # p is to return path instances print(f.exists())

To Custom function globbing add @ and the function name before back quotes:

def foo(s):
    return [i for i in os.listdir('.') if i.startswith(s)]
cd /
@foo`bi`
#['bin']

Macros

Simple macros

def m(x : str):
    return x

No macro calls:

[m('me'), m(42), m(m)]

['me', 42, <function main.m>]

Macro calls:

[m!('me'), m!(42), m!(identity), m!(42), m!( 42 ), m!(import os)]

["'me'", '42', 'identity', '42', '42', 'import os']

m!(if True: pass)

'if True:\n pass'

Real life example:

from_json = lambda cmd: __import__("json").loads(evalx(f"$({cmd})"))
o = from_json!(echo '{"a":1}')
o
#{'a': 1}
type(o)
# dict

Subprocess Macros

echo! "Hello!"
# "Hello!"

bash -c! echo "Hello!"

Hello!

podman run -it --rm xonsh/xonsh:slim xonsh -c! 2+2

4

Inside of a macro, all additional munging is turned off:


echo $USER
# lou

echo! $USER

$USER

Macro block

Builtin macro Block

from xonsh.contexts import Block
with! Block() as b:
    qwe
    asd
    zxc

b.macro_block

'qwe\nasd\nzxc\n\n'

b.lines

['qwe', 'asd', 'zxc', '']

Custom JSON block

import json

class JsonBlock: xonsh_block = str

def __enter__(self):
    return json.loads(self.macro_block)

def __exit__(self, *exc):
    del self.macro_block, self.macro_globals, self.macro_locals

with! JsonBlock() as j: { "Hello": "world!" }

j['Hello']

world!

Custom Podman block

The example is from xontrib-macro-lib:

from xonsh.contexts import Block

class Container(Block): """Run xonsh codeblock in a container."""

def __init__(self):
   self.image = &#39;xonsh/xonsh:slim&#39;

def __exit__(self, *a, **kw):
    $[podman run -it --rm @(self.image) /usr/local/bin/xonsh -c @(self.macro_block)]

with! Container() as d: pip install lolcat echo "We're in the container now!" | lolcat

Macro blocks library

See also xontrib-macro-lib.

Tab-Completion

completer list  # List the active completers

Take a look into xontrib-fish-completer - it provides more rich completion than default bash completer.

Create your own completer:

def dummy_completer(prefix, line, begidx, endidx, ctx):
    '''
    Completes everything with options "lou" and "carcolh",
    regardless of the value of prefix.
    '''
    return {"lou", "carcolh"}

completer add dummy dummy_completer # Add completer: completer add &lt;NAME&gt; &lt;FUNC&gt;

Now press Tab key and you'll get {"lou", "carcolh"} in completions

completer remove dummy

Bind hotkeys in prompt toolkit shell

Uncover the power of prompt_toolkit by binding the hotkeys. Run this snippet or add it to ~/.xonshrc:

from prompt_toolkit.keys import Keys

@events.on_ptk_create def custom_keybindings(bindings, **kw):

# Press F1 and get the list of files
@bindings.add(Keys.F1)  # or for Mac try `@bindings.add(&quot;c-k&quot;)  # control+k`
def run_ls(event):
    ls -l
    event.cli.renderer.erase()

# Press F3 to insert the grep command
@bindings.add(Keys.F3)  # or for Mac try `@bindings.add(&quot;c-k&quot;)  # control+k`
def add_grep(event):
    event.current_buffer.insert_text(&#39;| grep -i &#39;)

# Clear line by pressing `Escape` key
@bindings.add(&quot;escape&quot;)
def clear_line(event):
    event.current_buffer.delete_before_cursor(1000)
    

See also: discussions about prompt-toolkit, more about key bindings, event.current_buffer.

Xontrib - extension or plugin for xonsh

Xontrib lists:

To install xontribs xonsh has xpip - a predefined alias pointing to the pip command associated with the Python executable running this xonsh. Using xpip is the right way to install xontrib to be confident that the xontrib will be installed in the right environment.

If you want to create your own xontrib using xontrib-template is the best way:

xpip install copier jinja2-time cookiecutter
copier copy --trust gh:xonsh/xontrib-template .

Xontrib as a bridge

You can integrate python tools into xonsh context and environment using xontrib e.g. see fstrider xontrib where xontrib allows to inject xonsh context into file system navigation tool.

Xonsh CLI app

To create full-featured xonsh app with CLI, debug and tests try xonsh-awesome-cli-app and articles around click, rich, typer, etc.

How to get the script path

Get the script path from $ARG0:

echo @("""echo This script is in @(p"$ARG0".parent)""") > /tmp/getpath.xsh
chmod +x /tmp/getpath.xsh
xonsh /tmp/getpath.xsh
# This script is in /tmp

Unit tests with xonsh and pytest

Start here: How do I write unit tests?.

History

There are two history backends: json and sqlite which xonsh has by default. The json backend creates a json file with commands history on every xonsh session. The sqlite backend has one file with SQL-database.

We recommend using the sqlite backend because it saves the command on every execution, and querying the history using SQL is very handy, i.e. history-search, history-pull.

echo 123
# 123

xonsh.history[-1]

HistoryEntry(cmd='echo 123', out='123\n', rtn=0, ts=[1614527550.2158427, 1614527550.2382812])

history -2

echo 123

history info

backend: sqlite

sessionid: 637e577c-e5c3-4115-a3fd-99026f113464

filename: /home/user/.local/share/xonsh/xonsh-history.sqlite

session items: 2

all items: 8533

gc options: (100000, 'commands')

sqlite3 $XONSH_HISTORY_FILE "SELECT inp FROM xonsh_history ORDER BY tsb LIMIT 1;"

echo 123

aliases['history-search'] = """sqlite3 $XONSH_HISTORY_FILE @("SELECT inp FROM xonsh_history WHERE inp LIKE '%" + $arg0 + "%' AND inp NOT LIKE 'history-%' ORDER BY tsb DESC LIMIT 10");""" cd /tmp history-search "cd /"

cd /tmp

history-search! cd / # macro call

cd /tmp

pip install sqlite_web sqlite_web $XONSH_HISTORY_FILE # Open the database in the browser

history pull # Pull the history from parallel sessions and add to the current session. [xonsh -V > 0.13.4]

There is a third-party history backend that's supplied in xontribs: xontrib-history-encrypt.

Interactive mode events

When you're in xonsh interactive mode you can register an event, i.e.:

@events.on_chdir
def mychdir(olddir, newdir, **kw):
    echo Jump from @(olddir) to @(newdir)

cd /tmp

Jump from /home/snail to /tmp

Help

Add ? (regular help) or ?? (super help) to the command:

ls?
# man page for ls

import json json?

json module help

json??

json module super help

Known issues and workarounds

ModuleNotFoundError

Sometimes when you're using virtual environments you can forget about the current version and location of Python and try to import packages in xonsh resulting in a ModuleNotFoundError error. Often this means you installed the package in another environment and didn't realise it. Use xcontext to understand the environment and xpip to install packages in xonsh environment.

Shadowing of console tools or shell syntax with Python builtins

In case of name or syntax collision try to use aliases or abbrevs to resolve the conflict.

The case with id or zip:

id
# <function id>
ID
# uid=501(user) gid=20(staff) groups=20(staff) ...
zip
# zip builtin
ZIP
# zip [-options] [-b path] ...
Zip
# zip [-options] [-b path] ...

The case with ellipsis:

aliases['...'] = 'cd ../..'  # looks nice, but
...
# Elepsis

del aliases['...'] xpip install xontrib-abbrevs && xontrib load abbrevs abbrevs['...'] = 'cd ../..' ... # becomes cd ../..

The case with import:

cd /tmp
$PATH.append('/tmp')
echo 'echo I am import' > import && chmod +x import

import # Run subprocess ./import

I am import

import args # Run Python import of args module

ModuleNotFoundError: No module named 'args'

aliases['imp'] = "import" imp

I am import

Uncaptured output

If you want to capture the output from a tool that basically interactive but has captured mode. For example basically ssh return the remote terminal that should be unthredable and uncapturable. But if you use it for getting the data from remote host you would like to capture it. There are three workarounds:

!(@thread ssh host -T "echo 1")  # Switch to thread (xonsh >= 0.18.3).
#CommandPipeline(returncode=0, output='1\n')

!(echo 123 | head) # stream to captured #CommandPipeline(returncode=0, output='123\n')

!(bash -c "echo 123") # wrap to capturable tool #CommandPipeline(returncode=0, output='123\n')

How to check the predictor result:

__xonsh__.commands_cache.predict_threadable(['echo'])
# True

xonsh.commands_cache.predict_threadable(['ssh'])

False

Frozen terminal in interactive tools

If you run a console tool and get a frozen terminal (Ctrl+c, Ctrl+d is not working), this can be that the tool was interpreted as a threaded and capturable program but the tool actually has interactive elements that expect input from the user. There are bunch of workarounds:

@unthread ./tool.sh
with @.env.swap(THREAD_SUBPROCS=False): $[./tool.sh]
$[./tool.sh]

New python version

Because of xonsh syntax was based on Python syntax you can face with parser issues if you install the new Python version and run xonsh. Check that you're specifying certain version of Python when you're using xonsh in your projects and there is no situation when python can be updated witout testing.

Tips and tricks

Make your own installable xonsh RC file

Start by forking xontrib-rc-awesome.

Inline scripting

Inline import

Use @.imp as inline importer (xonsh >= 0.18.2):

@.imp.json.loads($(echo '{"a":1}'))
# {'a': 1}
@.imp.datetime.datetime.now().isoformat()
# '2024-02-12T15:29:57.125696'
@.imp.hashlib.md5(b'Hello world').hexdigest()
# '3e25960a79dbc69b674cd4ec67a72c62'

Don't forget about autocompletion e.g. @.imp.date<tab>.

Inline statements

Use $[] to have inline statements:

for i in range(1,5): $[echo @(i)]
if $(which vim): $[echo vim]
$[echo vim] if $(which vim) else $[echo vi]
with @.env.swap(QWE=1): $[bash -c 'echo $QWE']
$A=1 $B=2 bash -c 'echo $A $B'

Triple quotes

To avoid escape characters (i.e. echo "\"hello\"") and make strings more elegant use triple quotes:

echo """{"hello":'world'}"""
# {"hello":'world'}

Python walrus operator in action

Use in subprocess:

echo Hello @(_name := input('Name: '))  # Use `_` to keep env clean.
echo Hello again @(_name)
# Name: Mike
# Hello Mike
# Hello again Mike

Use with commands:

(servers := $(@json echo '["srv1", "srv2"]'))
# list(['srv1', 'srv2'])
echo @(servers[0])
# srv1

Pack anything into conch

@.snail = '_'
@.sqrt = @.imp.math.sqrt
answer = @.sqrt(len(@.snail))

@.myproject = @.imp.types.SimpleNamespace() @.myproject.key = '424242'

Check the command

The showcmd builtin have -e argument that can uncover a command:

aliases['a'] = 'echo a'
aliases['b'] = 'a b'
aliases['c'] = 'b c'
c d
# a b c d

showcmd c d ['c', 'd'] showcmd -e c d # -e, --expand-alias ['echo', 'a', 'b', 'c', 'd']

Run Xonsh as if it’s the first launch

xonsh --save-origin-env  # Save current environment
# ...
# Changing environment, activating virtual envs, doing things.
# ...
xonsh --load-origin-env
# The xonsh session will be like it was when you did `xonsh --save-origin-env`.

For example you have project.xsh in your ~/projects/myproject/ that full of settings for the project: env variables, virtual envs, aliases, prompt style, history file setting, etc. Use case:

xonsh --save-origin-env
# You're working in your general session.
cd ~/projects/myproject/
xonsh --load-origin-env --rc ~/.xonshrc project.xsh
# Now you work in a project-specific environment without pulling the environment from the parent session.
exit
# Back to parent session.

Set next command or suggest one

$XONSH_PROMPT_NEXT_CMD = 'echo hello'
# Prompt has `echo hello`.

$XONSH_PROMPT_NEXT_CMD_SUGGESTION = 'echo hello'

Prompt suggests echo hello.

Jump from aliases to CLI apps

If you realize that your alias becomes the app it's time to look at xonsh-awesome-cli-app.

Make interruption tolerant transactions

When you have group of commands (transaction) it's good to use DisableInterrupt from xontrib-macro:

echo start
with! @.imp.xontrib.macro.signal.DisableInterrupt():
    echo 'sleep start'
    sleep 10
    echo 'sleep end'
echo finish

start

sleep start

[Press Ctrl+C]

KeyboardInterrupt will be raised at the end of current transaction.

sleep end

Exception KeyboardInterrupt: KeyboardInterrupt was received during transaction.

Using a text block in the command line

The first way is to use multiline strings:

echo @("""
line 1
line 2
line 3
""".strip()) > file.txt

$(cat file.txt)

'line 1\nline 2\nline 3\n'

The second way is to use xonsh macro block via xontrib-macro:

xpip install xontrib-macro

from xontrib.macro.data import Write

with! Write('/tmp/t/hello.xsh', chmod=0o700, replace=True, makedir=True, verbose=True): echo world

/tmp/t/hello.xsh

world

Run commands in a container:

podman run -it --rm xonsh/xonsh:slim xonsh -c @("""
pip install --disable-pip-version-check -q lolcat
echo "We're in the podman container now!" | lolcat
""")

Don't forget that Alt+Enter can run the command from any place where the cursor is.

Use prompt instead of input for multiline input

In python there is input function but it has no support of multiline pasting. Use prompt:

from prompt_toolkit import prompt

echo @(prompt('Content:\n')) > /tmp/myfile

Content:

<Paste multiline text "1\n\2\n" from clipboard>

cat /tmp/myfile

1

2

One line version using inline importer (@.imp):

echo @(@.imp.prompt_toolkit.prompt("Content:\n")) > /tmp/myfile

Using the name of the alias in alias logic

@aliases.register(",")
@aliases.register(",,")
@aliases.register(",,,")
@aliases.register(",,,,")
def _superdot():
    cd @("../" * len($__ALIAS_NAME))

, # cd ../ ,, # cd ../../ ,,, # cd ../../../

Transparent callable environment variables

For example, you want to have the current timestamp in every command but instead of nesting like @(dt()) you want sugar:

class TimestampCl:
    def __repr__(self):
        return str(@.imp.datetime.datetime.now().isoformat())

$dt = TimestampCl()

or one-liner:

$dt = type('TimeCl', (object,), {'repr':lambda self: str(@.imp.datetime.datetime.now().isoformat()) })()

echo $dt sleep 1 echo $dt

2024-03-05T23:34:50.188014

2024-03-05T23:34:51.259861

Use env context manager to keep command and namespace clean

Using AWS CLI to extract cost:

with @.env.swap(START=$(date -d "30 days ago" +%Y-%m-%d), END=$(date +%Y-%m-%d)):
    aws ce get-cost-and-usage \
        --time-period 'Start=$START,End=$END' \
        --granularity DAILY \
        --metrics "UnblendedCost" \
        --group-by 'Type=DIMENSION,Key=SERVICE'

PostgreSQL connect with password input:

with @.env.swap(PGPASSWORD=@.imp.getpass.getpass('pgpass:')):
    for db in ['db1', 'db2']:
        psql postgresql://user@host:5439/@(db) -c 'select 1'

Ask to input argument and with autocomplete

Ask simple input:

echo @(input('Text: '))
# Text: hello
# hello

echo Hello @(_name := input('Name: ')) # Use _ to keep env clean. echo Hello again @(_name)

Name: Mike

Hello Mike

Hello again Mike

$ENV_NAME = input('Name: ') # Use input to set and reuse env variable echo Name is $ENV_NAME

Name: Alex

Name is Alex

The way to have continuous interactive search.

while 1: ![cat /etc/passwd | grep --color -i @(input('\nUsername: '))] while 1: ![cat @(f:='/etc/passwd') | grep --color -i @(input(f+': '))] # walrus happy

Ask for input with completion and history:

from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.history import FileHistory

def ask(title : str, completions : list = []): filename = ''.join(c for c in title if c.isalpha()) history = FileHistory($XONSH_DATA_DIR + f'/ask_{filename}.txt') completer = WordCompleter(completions) session = PromptSession(completer=completer, history=history) user_input = session.prompt(f'{title}: ') return user_input

echo I am saying @(ask('What to say'))

What to say: hello

I am saying hello

echo Give @(ask('Fruit', ['apple', 'banana', 'orange'])) to @(ask('To', [$(whoami).strip()]))

Fruit: <Tab>

Fruit: apple

To: <Tab>

To: user

Give apple to user

$MY_DIR = ask('Dir', $(ls /).splitlines())

Dir: <Tab>

From the shell to REST API for one step

If you want to run shell commands from REST API you can create a flask wrapper using xontrib-macro:

xpip install flask xontrib-macro

cd /tmp

from xontrib.macro.data import Write with! Write('myapi.xsh', chmod=0o700): import json from flask import Flask, request app = Flask(name) @app.route('/echo', methods=['GET']) def index(): say = request.args.get('say') result = $(echo -n @(say)) # run subprocess command return json.dumps({'result': result}) app.run(host="127.0.0.1", port=5000) # debug=True

./myapi.xsh

Running on http://127.0.0.1:5000

curl 'http://127.0.0.1:5000/echo?say=cow&#39;

{"result": "cow"}

Don't forget about API security.

Run xonsh with clean environment

Sometimes you want to experiment with commands but don't want to affect history or state on main terminal env:

xonsh --no-rc --no-env  # yes, but you can have warning about no TTY or no HOME
aliases['xonsh-no-env'] = 'xonsh --no-rc --no-env -DPATH -DTERM -DHOME'  # nice (xonsh >= 0.22.2)
xonsh-no-env

Run pure xonsh engine

Basically xonsh session loads RC files, inherit environment, uses dynamic colors, git callbacks in prompt, saves commands to history and more. Most of this features are disabled in not interactive mode (xonsh -c 'echo 1'). But in some cases you can want to rid of all features to reduce overhead on running completely. Here is the path:

xonsh --no-rc --no-env --shell-type readline \
      -DCOLOR_INPUT=0 -DCOLOR_RESULTS=0 -DPROMPT='@ ' \
      -DXONSH_HISTORY_BACKEND=dummy -DXONTRIBS_AUTOLOAD_DISABLED=1

Here:

  • --no-rc to prevent loading RC files.
  • --no-env to prevent inheriting the environment.
  • --shell-type readline to use cheapest shell backend (readline).
  • -DCOLOR_INPUT=0 to disable colors and file completer that can read files to choose the right color.
  • -DCOLOR_RESULTS=0 to disable colors in output.
  • -DPROMPT='@ ' to disable prompt with gitstatus and other complex fields.
  • -DXONSH_HISTORY_BACKEND=dummy to disable history backend.
  • -DXONTRIBS_AUTOLOAD_DISABLED=1 to avoid loading xontribs.

Interactively debugging a script

If you want to have a breakpoint to debug a script, use the standard Python pdb:

xpip install xontrib-macro
from xontrib.macro.data import Write
with! Write('/tmp/run.xsh', chmod=0o700, replace=True, makedir=True):
    echo hello
    $VAR = 1
    var = 2
import pdb
pdb.set_trace()   # interactive debug

echo finish

xonsh /tmp/run.xsh

hello

> /tmp/run.xsh(9)<module>()

-> echo finish

(Pdb)

var

2

@.env['VAR']

1

exit

bdb.BdbQuit

Using xonsh wherever you go through the SSH

You've stuffed your command shell with aliases, tools, and colors but you lose it all when using ssh. The mission of the xxh project is to bring your favorite shell wherever you go through ssh without root access or system installations.

How to modify a command before execution?

To change the command between pressing enter and execution there is the on_transform_command event:

xpip install lolcat

@events.on_transform_command def _(cmd, **kw): if cmd.startswith('echo') and 'lolcat' not in cmd:
# Be careful with the condition! The modified command will be passed # to on_transform_command event again and again until the event # returns the same command. Newbies can make a mistake here and # end up with unintended looping. return cmd.rstrip() + ' | lolcat' else: return cmd

echo 123456789 # <Enter>

Execution: echo 123456789 | lolcat

Comma separated thousands in output (custom formatter)

Here is a snippet from @maxwellfire:

50000+50000
# 100000

500+500.123

1000.123

import xonsh.pretty xonsh.pretty.for_type(type(1), lambda int, printer, cycle: printer.text(f'{int:,}')) xonsh.pretty.for_type(type(1.0), lambda float, printer, cycle: printer.text(f'{float:,}'))

50000+50000

100,000

500+500.123

1,000.123

Builtin change directory context manager for scripting

cd /tmp

pwd with p"./a/b".mkdir(mode=0o777, parents=True, exist_ok=True).cd(): pwd pwd

/tmp

/tmp/a/b

/tmp

Juggling of exit code using python substitution

cd into a directory and, if the count of files is less then 100, run ls:

aliases['cdls'] = "cd @($arg0) && @(lambda: 1 if len(g`./*`) > 100 else 0) && ls"
cdls / && pwd
# bin dev etc ...
# /
cdls /usr/sbin && pwd
# /usr/sbin

How to paste and edit multiple lines of code while in interactive mode

In some terminals (Konsole in Linux or Windows Terminal for WSL) you can press ctrl-x ctrl-e to open up an editor (nano in Linux) in the terminal session, paste the code there, edit and then quit out. Your multiple-line code will be pasted and executed.

Waiting for the job done

sleep 100 &  # job 1
sleep 100 &  # job 2
sleep 100 &  # job 3

while $(jobs): time.sleep(1)

print('Job done!')

Nice combination with xontrib-cmd-durations that can show desktop notification when the command is done.

Do asynchronous task in interactive prompt

For example you can continuously pulling history from other sessions or just run this in xonsh to print every second:

$SHELL_TYPE
# 'prompt_toolkit'

import asyncio from prompt_toolkit.shortcuts import print_formatted_text

async def print_and_sleep(): while True: print_formatted_text('hey!')
await asyncio.sleep(2)

loop = asyncio.get_event_loop() loop.create_task(print_and_sleep())

How to trace xonsh code?

Trace with hunter:

pip install hunter
$PYTHONHUNTER='depth_lt=10,stdlib=False' $XONSH_DEBUG=1 xonsh -c 'echo 1'

Or try xunter for tracing and profiling.

From Bash to Xonsh

Read Bash to Xonsh Translation Guide, run bash -c! echo 123 or install xontrib-sh.

Xonsh and Windows

First of all we recommend using WSL 2 with Manjaro (that maintains a rolling release) on Windows. Don't forget to fix PATH.

But if you want to use xonsh in a Windows environment:

Using env variables with with inline ExecAlias

man case:

aliases['mans'] = '''man -P @("less -p '" + $arg1 + "' " + @.env.get("MANS_ARGS","")) $arg0'''
aliases['mansn'] = '$MANS_ARGS="-N" mans @($args)'

mans ls dir # search "dir" on man ls page mansn ls 'long list' # search "long list" on man ls page and show line numbers

AWS CLI case:

aws configure --profile p1
aws configure --profile p2

aliases['aws-p1'] = "$AWS_DEFAULT_PROFILE='p1' @('aws') @($args)" aliases['aws-p2'] = "$AWS_DEFAULT_PROFILE='p2' @('aws') @($args)"

aws-p2 s3 ls s3://my-profile1-bucket/ # The same as aws s3 ls --profile p2 s3://my-profile1-bucket/

Expert level: How to inject wrapper into xonsh code?

There is an approach that was introduced in xontrib-free-cwd that allows to wrap a function from internal component. For example using this technique and functools.wraps() we can wrap prompt-toolkit private prompt() function to set default argument and fill the text of next command in the prompt:

# ~/.xonshrc
@events.on_ptk_create
def _setup_next_cmd_default(prompter, history, completer, bindings, **kw):
    """Add wrapper for `prompter.prompt`."""
    def _inject_default_wrapper(func):
        if hasattr(func, "_orgfunc"):  # already wrapped
            return func
    @@.imp.functools.wraps(func)
    def wrapper(*args, **kwargs):
        if &quot;default&quot; not in kwargs:
            kwargs[&quot;default&quot;] = @.env.get(&quot;XONSH_PROMPT_NEXT_CMD&quot;, &quot;&quot;)
            $XONSH_PROMPT_NEXT_CMD = &quot;&quot;
        return func(*args, **kwargs)

    wrapper._orgfunc = func  # mark that wrapper applied
    return wrapper

prompter.prompt = _inject_default_wrapper(prompter.prompt)

$XONSH_PROMPT_NEXT_CMD="2+2"
# 2+2<coursor>

This feature was implemented in xonsh in xonsh/6037.

Answers to the holy war questions

Bash is everywhere! Why xonsh?

Python is everywhere as well ;)

Xonsh is slower! Why xonsh?

You can spend significantly more time Googling and debugging sh-based solutions as well as significantly more time to make the payload work after running a command. Yeah, xonsh is a bit slower but you will not notice that in real-life tasks :)

Also, take a look:

My fancy prompt in another shell is super duper! Why xonsh?

The fancy prompt is the tip of the iceberg. Xonsh shell brings other important features to love: sane language, powerful aliases, agile extensions, history backends, fully customisable tab completion, macro commands and blocks, behaviour customisation via environment variables, and more, and more, and more :)

Xonsh has issues! Why xonsh?

Compared to 20-40-year-old shells, yeah, xonsh is a 10-year-old youngster. But we've used it day by day to solve our tasks with success and happiness :)

Become a xonsh developer and contributor

Moved to xonsh-developer-toolkit.

Thank you!

Thank you for reading! This cheatsheet is just the tip of the iceberg of the xonsh shell; you can find more in the official documentation.

Also you can install the cheatsheet xontrib:

xpip install xontrib-cheatsheet
xontrib load cheatsheet
cheatsheet
# Opening: https://github.com/anki-code/xonsh-cheatsheet/blob/main/README.md

If you like the cheatsheet, click ⭐ on the repo and tweet.

Credits

SEE ALSO

clihub3/4/2026XONSH-CHEATSHEET(1)