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.