100 Languages Speedrun: Episode 66: Xonsh

Unix shells were great for interactive use, but terrible for scripting, where you really should use a language like Python or Ruby.

There's been so many attempts at creating a better shell scripting language, like Elvish I recently covered.

Xonsh decided to solve this problem once and for all by going "fuck it, Python is shell".

FizzBuzz

As Python in shell now, you can do this in Xonsh:

#!/usr/bin/env xonsh

def fizzbuzz(i):
  if i % 15 == 0:
    return "FizzBuzz"
  elif i % 3 == 0:
    return "Fizz"
  elif i % 5 == 0:
    return "Buzz"
  else:
    return str(i)

for i in range(1,101):
  print(fizzbuzz(i))

There's not much point doing the usual examples, as you can just do pretty much any Python code, and it will generally just work.

Shell

When not used as Python, you have full shell environment too, with some reasonable autocomplete builtin:

$ git <TAB>
    add             commit          maintenance     rm
    am              config          master          send-email
    amend           describe        merge           shortlog
    apply           diff            mergetool       show
    archive         difftool        mv              show-branch
    bisect          fetch           notes           sparse-checkout
    blame           format-patch    prune           st
    br              fpush           pull            stage
    branch          fsck            push            stash
    bundle          gc              range-diff      status
    cdiff           gitk            rebase          submodule
    checkout        grep            reflog          switch
    cherry          gui             remote          sx
    cherry-pick     help            repack          tag
    ci              init            replace         unstage
    citool          instaweb        request-pull    up
    clean           latexdiff       reset           wdiff
    clone           log             restore         whatchanged
    co              ls              revert          worktree

You can get some help with Wordle:

$ cat /usr/share/dict/words | pcregrep '^[^raton]{2}nce$'
bunce
dunce
fence
hence
mince
pence
sence
since
Vince
wince
yince

And so on.

How does it work?

Whenever you type anything, Xonsh checks if the code looks like Python or like Shell. And so it decides what to do.

You can explicitly request shell mode with $()

$ len($(seq 1 20))
51

Or explicitly request Python mode with @()

$ say @(60+9)

This is a rare case where Python has an advantage over Ruby, as Ruby's flexible syntax would actually interfere with this autodetection.

What does not work?

Xonsh tries its best, but there's enough overlap between Python and Shell syntax that sometimes you need to disambiguate.

For example this doesn't work:

$ LC_ALL=ru_RU date
xonsh: For full traceback set: $XONSH_SHOW_TRACEBACK = True
xonsh: subprocess mode: command not found: LC_ALL=ru_RU

Fortunately the alternative in this case isn't too bad:

$ $LC_ALL='ru_RU' date
суббота, 22 января 2022 г. 23:23:01 (GMT)

Unfortunately some shell features don't seem to have any equivalent. One that I use a lot is redirect-from-command:

$ diff <(fizzbuzz.scala) <(fizzbuzz.cobol)

Xonsh comes with Bash to Xonsh Translation Guide, but it only covers basics. I really wished they extended it to more advanced examples.

Cat Facts

You can fix shell and Python in the same script without much trouble:

#!/usr/bin/env xonsh

import json

doc = $(curl -s "https://cat-fact.herokuapp.com/facts")

for fact in json.loads(doc):
  print(fact["text"])
$ ./catfacts.xsh
Cats make about 100 different sounds. Dogs make only about 10.
Domestic cats spend about 70 percent of the day sleeping and 15 percent of the day grooming.
I don't know anything about cats.
The technical term for a cat’s hairball is a bezoar.
Cats are the most popular pet in the United States: There are 88 million pet cats and 74 million dogs.

SIGPIPE

Unfortunately this is completely broken in Xonsh. This is what we'd expect:

$ seq 1 1000000 | head -n 5
1
2
3
4
5

If we write this Python/Xonsh script:

#!/usr/bin/env xonsh

for i in range(1, 1000001):
  print(i)

We get this:

$ python3 numbers.xsh | head -n 5
1
2
3
4
5
Traceback (most recent call last):
  File "/Users/taw/100-languages-speedrun/episode-66-xonsh/numbers.xsh", line 4, in <module>
    print(i)
BrokenPipeError: [Errno 32] Broken pipe
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
BrokenPipeError: [Errno 32] Broken pipe

Now Xonsh should absolutely set up SIGPIPE handlers in its scripts to support this, as this is extremely basic shell programming pattern. It does not (and the error message is even worse). For as shell, I consider this a bug:

$ xonsh numbers.xsh | head -n 5
1
2
3
4
5
Traceback (most recent call last):
  File "/usr/local/bin/xonsh", line 8, in <module>
    sys.exit(main())
  File "/usr/local/Cellar/xonsh/0.11.0/libexec/lib/python3.10/site-packages/xonsh/__amalgam__.py", line 21799, in main
    _failback_to_other_shells(args, err)
  File "/usr/local/Cellar/xonsh/0.11.0/libexec/lib/python3.10/site-packages/xonsh/__amalgam__.py", line 21746, in _failback_to_other_shells
    raise err
  File "/usr/local/Cellar/xonsh/0.11.0/libexec/lib/python3.10/site-packages/xonsh/__amalgam__.py", line 21797, in main
    sys.exit(main_xonsh(args))
  File "/usr/local/Cellar/xonsh/0.11.0/libexec/lib/python3.10/site-packages/xonsh/__amalgam__.py", line 21853, in main_xonsh
    run_script_with_cache(
  File "/usr/local/Cellar/xonsh/0.11.0/libexec/lib/python3.10/site-packages/xonsh/__amalgam__.py", line 3662, in run_script_with_cache
    run_compiled_code(ccode, glb, loc, mode)
  File "/usr/local/Cellar/xonsh/0.11.0/libexec/lib/python3.10/site-packages/xonsh/__amalgam__.py", line 3563, in run_compiled_code
    func(code, glb, loc)
  File "./numbers.xsh", line 4, in <module>
    print(i)
BrokenPipeError: [Errno 32] Broken pipe
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
BrokenPipeError: [Errno 32] Broken pipe

Should you use Xonsh?

Of the new shells I tried, I like Xonsh a lot more than Elvish. It works out of the box a lot better, with features like Ctrl-A / Ctrl-E, git completion etc. just working. The end goal of full support for Python is also much more ambitious than what Elvish is aiming for, and the learning curve is lower as you presumably already know some Python and Shell, so you'll just need a few exceptions Xonsh had to make to make them work together, not a whole new language.

I don't think Xonsh is ready to recommend it for serious use, but I'm really tempted to give it a go for a few weeks as my primary shell, and that's not something I felt about any shell in a long while. I expect a lot of early adopter pain if I go for it, but it's not like that ever stopped me.

Just the two biggest issues I ran into - no equivalent of <(...) and broken SIGPIPE - really stop it from being an acceptable shell now, so I hope they both get fixed soon.

Code

All code examples for the series will be in this repository.

Code for the Xonsh episode is available here.