Learning Elvish (but not the Middle-earth one)
There is some part of me that is drawn to obscure, odd technologies and tools. It’s kind of irrational, but also very exciting to tinker with. As I was looking for alternatives to my zsh setup, I couldn’t help but look at some of the more niche shells, even if it wasn’t very likely that I’ll settle on them.
Elvish was one of the options that caught my attention with its friendly website, not too stuffy documentation and being written in Go. It’s a non-POSIX shell, which offers some unusual TUI features and a scripting language with some very interesting ideas.
This post is my subjective impressions of it, which you are welcome to consume with some salt to taste. Shell usage tends to be a very personal thing, so take my opinions as a prompt for your own exploration, rather than a final verdict.

💻 Interactive usage
One of the main ideas in Elvish TUI is that it has multiple different modes, a bit like vim.
insert
andcommand
have to do with entering and editing the command line.completion
is exactly what you think it is, it gets activated when completing commands and arguments and such.navigation
allows you to browse the file system in a more visual way than the traditionalcd
andls
. It reminded me of my early days of Linux when I heavily used Midnight Commander and would definitely be appreciated by folks who want to use a shell, but are put off by its lack of visual context.location
goes together with it, allowing you to jump into recently visited directories.history
,histlist
andlastcmd
provide you different ways to recall your command history.
What’s nice about it, is that all those modes behave intuitively out of the
box. You don’t need a 1000-line config and a dozen plugins to hammer them into
something sensible, it just works. For those of us with the bash muscle memory,
use readline-binding
is all you need to keep your habits, and if you do want
to customize the behavior, shortcuts and triggers, it’s all available and
nicely documented.
The only notable major thing that doesn’t “just work” is completion for
well-known commands like git
or make
. You can find community-written modules
that provide all that and more, but it doesn’t come bundled with the shell.
Installing third-party modules is a topic deserving its own discussion, I’ll
come back to that. Writing your own completion rules is also pretty
straightforward.
Performance-wise it’s very snappy. When I first mentioned Elvish in the
DevZen podcast, some folks in the audience
mentioned that it was very slow for them, taking seconds to start up, but for me
that wasn’t an issue at all. The only gotcha was that when I ran it in
WSL2, by default
$PATH
inherited all the paths from Windows, which not only produced a lot of
junk suggestions, but also was very slow to enumerate due to how WSL accessed
Windows directories. That’s not Elvish’s fault, though, and easy
enough to fix,
so I only mention it in case someone else runs into the same problem.
An interesting performance feature is stale prompt: if your prompt rendering function takes too long to complete, after a short timeout Elvish will simply show the most recent version of the prompt, and asynchronously re-render it once the function completes. That can be nice if you want to show things like VCS status, which can be somewhat slow on large repos, but don’t want to deal with the latency. Frameworks like oh-my-zsh have to go to significant lengths to work around it, but Elvish makes it trivial.
The one feature that I wanted, but found missing is transient prompt. It’s almost there: you can configure Elvish to clear RPrompt before it executes the command, but it won’t re-render the main prompt. It wouldn’t be too hard to implement, but it’s not there yet. @xiaq is also working on the major TUI rewrite which should make such things easy to implement, but the timeline is “whenever it’s done” (fair enough).
Another feature I wished for is support for background processed. It’s pretty
common for me to open vim
, do some editing, press Ctrl-Z
to put it in the
background, run some commands, and go back to vim with fg
, but Elvish doesn’t
support that. It’s not the end of the world and is easily mitigated by tmux or
simply opening another terminal tab, but it breaks my muscle memory enough to be
uncomfortable.
🛠 Customization and modules
It’s pretty good! You can
customize your prompt however you want
by overriding $edit:prompt
and $edit:rprompt
functions, and the stale prompt
feature I mentioned allows you to worry less about performance.
This is what my prompt looks like when ported to Elvish, pretty readable.
|
|
You can also change the keybindings for each or all modes to your liking. The only change I felt the need to make is disabling “end of history” message when you press the down arrow while already at the most recent command:
|
|
What really impressed me is that Elvish comes with its own package/plugin
manager epm
out of the box. This is fantastic
because not only it spares me agonizing over which community-provided plugin
manager to choose, but it also means that all community packages will work
with it. Because there’s only one.
Overall, it’s pretty clearly inspired by go get
and it just works. So all
those missing completion rules are just one epm:install
command away, and
https://github.com/elves/awesome-elvish readily offers you the most commonly
used ones.
Unfortunately, epm
doesn’t seem to support installing a specific version of a
package, it always pulls the most recent one. Arguably, this is fine for most
people, but personally I prefer to audit all the code that my shell gets to
execute, so being able to review a specific version of a package and then stick
to it is pretty important for me. This is one of the bigger reasons I wanted to
move away from batteries-included frameworks like oh-my-zsh, in favor of
something lighter-weight and self-contained. I think it wouldn’t be hard to send
a PR adding it, although I haven’t asked.
📜 Scripting language
This is, arguably, Elvish’s most interesting side. I already mentioned it’s not POSIX-compatible, but that also allows it to eliminate a lot of pitfalls you would face with regular bash scripts. Despite some peculiarities, it wins in readability over bash any day. I will leave the job of describing the language in all details to the official documentation, and just call out a few things that I liked in particular.
Functions don’t have return values. Instead, they have structured and unstructured output streams, which Elvish calls byte outputs and value outputs respectively. Byte outputs are basically the usual stdin/stdout I/O we are used to in the traditional shells, and you can build pipelines with them:
|
|
Value outputs allow to pass streams of complex types like lists and maps between functions without having to rely on fragile conventions like “one value per line”:
|
|
In Elvish, put
plays a role similar to echo
, except that it outputs the
structured value, instead of dumping plain bytes into stdout. Needless to say,
this makes processing lists of structured data (say, login/host pairs) much
nicer. Nushell is built around a similar concept, but
dials it up to eleven.
Built-in each
and peach
functions execute a lambda function over their value
inputs sequentially or in parallel, which is orders of magnitude nicer than the
traditional xargs
.
Modules is another feature that traditional shells invent ad-hoc (and usually awkwardly), which Elvish has built-in. It comes with namespacing and makes organizing your code and third-party modules easy and unambiguous. It also comes with a small standard library of modules that provide built-ins for common tasks like string manipulation, OS interactions and so on. This is actually really handy for writing shell scripts that work both on Windows and *NIX systems.
To give myself a more practical impression of the language, I wrote a module that allows me to switch between multiple Go versions.
|
|
There is an official VSCode extension, which provides syntax highlighting and even rudimentary autocompletion, and several community-maintained plugins for other editors.
🤝 Community and development
All technical features aside, the community around the project is one of the major factors when I’m deciding whether to adopt a tool for daily use. The most excellent tool without a community and support is more of a liability than a help.
Elvish has been around for a while, and it has a nice, even if a small community. Community chat can be accessed through several different platforms (all cross-communicating with each other) and its regulars will happily provide advice and pointers for your problems.
It also has a remarkably accessible and comprehensive documentation, which equips you with almost everything you would ever need to know to be a happy user.
There are community-provided modules that address much of what one would wish
for in their daily-driver shell: https://github.com/elves/awesome-elvish. Among
them are some really neat ones like
aca/elvish-bash-completion,
which can import bash completions, or
tesujimath/bash-env-elvish
massively simplifies adapting bash-specific tools like nvm
to work with
Elvish.
I wish a lot of that came out of the box, but I also entirely understand @xiaq’s reluctance to maintain all that in the core repository. Which is a segue to how Elvish is developed and maintained.
It’s written in Go, which was a big plus for me personally. I know and use Go pretty much all the time, which means that installing Elvish is pretty easy for me (even ignoring pre-built binaries), I can use it on any OS, and should I run into a particularly annoying bug, I would have a pretty good chance to fix it myself. In fact, while I was experimenting with it, on several occasions I went poking around the source code to figure out why a built-in function wasn’t doing what I thought it should be doing, and it was pretty easy to find my answers. Hat tip for a clean and well-organized code base.
As far as I could gather, it is essentially @xiaq’s passion project, who is the only and final authority on the direction of the development:
The only person with direct commit access is the project’s founder @xiaq.
Third-party contributions are often accepted, but large changes are usually handled by @xiaq, as much as their spare time permits.
This is an entirely valid way to run an open source project, but it does create a bottleneck in how quickly Elvish can develop and improve. Forking, of course, is always an option, but then I would become my own bottleneck 😅
🏁 Final impression
I really, really liked Elvish and enjoyed tinkering with it 🧶😻
I won’t be using it as a daily-driver shell, at least in the foreseeable future, but this is more about my highly specific wishes than a criticism of the project as such:
- It is a bit too niche. I want to use the same shell at work and home, and it would require more effort than I’m willing to expend to make all the work tools work for me with Elvish.
- It offers very nice UX out of the box, but lacks in integrations like completion rules for common tools. For comparison, Fish ships with over a thousand completions out of the box.
- If I have to trust in community members for essential stuff like completion
rules, I’d like to be able to put my trust into one specific version of a
module, rather than indefinitely trusting that all future changes to the
repository would be benign. If
epm
supported version pinning it would be an ideal solution, but alas.
That said, if I find myself in a need of a cross-platform shell scripting language (and don’t wish to deal with Python), Elvish remains a pretty nice pick thanks to its OS-independent standard library.
Hat tip to @xiaq for creating and maintaining such a cool project, and to all the awesome community members who contribute to it!