Xonsh: a Python and Bash Shell

Xonsh is a Python-powered, cross-platform shell language and command prompt, that works on all major systems including Linux, OSX, and Windows.

Bash scripts are fast and effective for small or batch applications. One of the limitations in Bash is it’s handling of math functions and floating point numbers.

Python is super popular for new programmers and it has a huge library of available functions.

For Raspberry Pi users Xonsh can offer a lot of opportunities to write some extremely lean scripts. Python can be used to connect to 3rd party devices and sensors and 1 line of Bash can be used for dialogs.

In this blog I will introduce Xonsh with some Raspberry Pi examples.

Getting Started

See the Xonsh documentation for installation directions on your specific system. For installation on Raspberry Pi/Ubuntu/Debian enter:

sudo apt install xonsh

To run the Xonsh, simply enter: xonsh

Out of the box Xonsh offers a configuration wizard and a tutorial.

Using Python

Python code can entered directly at the command line. The version of Python will depend on what is loaded on the base system. To check your version:

$ import sys
$ sys.version
'3.9.2 (default, Mar 12 2021, 04:06:34) \n[GCC 10.2.1 20210110]'

Like the interactive Python interface, print statements are not required to see the output:

$ 5 + 6
11
$ a=4;b=7
$ a+b
11
$ msg1="hi"
$ msg1 + " world"
'hi world'

Using Bash

Xonsh uses Python first, so an example with ls (the Bash list command):

$ # This first ls is used as a Bash list command
$ ls
Adafruit_DHT           LICENSE      README.md  dist      setup.py
Adafruit_DHT.egg-info  MANIFEST.in  build      examples  source
$ ls="this is a variable"
$ # Xonsh show the variable ls
$ ls
'this is a variable'

In the above example ls is first used as the Bash list command, but if a variable is defined with the same name, the variable is referenced.

Xonsh processes a Bash statement as a single line. This means that:

  • Bash for/while/if statements need to be all on one line
    • Remember to use white space between characters
  • Line extensions (with a “\”) are not supported
  • Bash functions are not supported.
    • You can write the function in Python instead of Bash

Using Python in Bash

Python statements are using within Bash by: @(Python statements). Below are two examples of using Python in Bash:

$ import sys
$ echo @(sys.version)
3.9.2 (default, Mar 12 2021, 04:06:34)
[GCC 10.2.1 20210110]
$ echo @("Answer=" +str(5+6))
Answer=11

Using Bash in Python

Bash variables can be used directly in Python, for example:

$ #Use Bash date and pass it to Python
$ now=$(date)
$ print("Now is: " + now)
Now is: Thu 24 Mar 2022 03:59:32 PM EDT

Pi Example: Show CPU Temp

Showing the Raspberry Pi’s CPU temperature is a simple example to show how Bash and Python can be used effectively together. Pi’s CPU temperature is stored internally as milli-DegC (1,000 times the degrees in Celsius). Floating point math in Bash is awkward, so this can be done in Python.

Below is a Xonsh script to read the CPU temperature, convert the value to one decimal point, and then present the result in a Bash/Zenity dialog:

#!/bin/xonsh
#
# temp2dlg.sh - use xonsh to get Pi temperature and show in a dialog
#

# Get the temperature with a Bash "cat" and variable
mtemp=$(cat /sys/class/thermal/thermal_zone0/temp)

# Use Python to convert a string of milli-DegC to DegC with 1 decimal
btemp=str(round(int(mtemp)/1000,1)) + " Deg C"
# Add some formatting
btemp="<span font='32' foreground='red'>" + btemp + "</span>"

# Use a Bash Zenity dialog to show the result
zenity --info --text=@(btemp) --title=Raspberry_PI_CPU_Temp  --width=300 &

To make the script executable and then run it:

$ chmod +x temp2dlg.sh
$ ./temp2dlg.sh

Raspberry Pi DHT11 Sensor Value

For the next project I used a DHT11 temperature/humidity sensor.

My goal for this project was to do a snapshot and present the data in a list dialog.

The code to read and present the data was only 7 lines. The Bash variable is passed between Bash statements just like a Python variable would be passed with the @(Python_statement) syntax. Really the only ugly part of the code was the long Zenity statement.

#!/bin/xonsh
#
# dht11_dlg.sh - using xonsh show DHT11 sensor data on a dialog
#
import time
import Adafruit_DHT
sensor=Adafruit_DHT.DHT11
gpio=17

humidity, temperature = Adafruit_DHT.read_retry(sensor, gpio)

thetime=$(date)

zenity --list --title=DHT11_Sensor_Data  --text=@(thetime) --column=Sensor --column=Value --column=Units Humidity @(humidity) "%" Temperature  @(temperature)  "Deg C"

Issues with Shells

Working between different shells and sub-shells can be a little confusing. I found that I occasionally got confused which shell I was working in. The ps command would tell me if xonsh is running:

I was able to pass an Xonsh script to a Bash script without any issues, but I found that for certain operations I needed to manually kill a Xonsh shell.

Below is an example using the DHT11 sensors, and the YAD command line dialog tool (install by: sudo apt install yad). This example had a Python function (show_data) that cycled every 2 seconds and piped the new sensor data to the dialog. Unfortunately the Xonsh shell was still running even after the dialog closed so I needed to use the pkill utility to terminate xonsh .

#!/bin/xonsh
#
# dht11_data.sh - using xonsh show DHT11 sensor data on a dialog
#
import time
import Adafruit_DHT
sensor=Adafruit_DHT.DHT11

gpio=17
# show_data - format sensor data for YAD multi-process bar format
def show_data():
	while True:
		time.sleep(2)
		humidity, temperature = Adafruit_DHT.read_retry(sensor, gpio)
		echo @("1#" + str(temperature) + " Deg C")
		echo @("1:" + str(temperature))
		echo @("2:#" + str(humidity) + " %")
		echo @("2:" + str(humidity))

echo "Showing realtime data..."
@(show_data)  | yad  --multi-progress --bar=Temperature --bar=Humidity --title=DHT11_Sensor_Data 
# exit and ensure that the subprocess is killed
echo "Exit and kill subprocess if running..."
pkill -f xonsh &

Summary

Xonsh has a lot of potential for users looking for simple scripting solutions.

For myself I’ll probably stick to either Bash or Python solutions, but I like that I have other options.

Leave a comment