What are those shell init commands?
I just looked at my .zshrc
for all the custom stuff I added to it and I noticed the following lines:
It seems like many tools have some requirement around augmenting your shell and environment in some way for them to do their job. Here is Deno’s script for example:
This one is fairly short and straightforward. It’s checking to make sure the Deno CLI is available in $PATH
and adding it if not so you can just run deno
in your shell. More interestingly, many modern CLI tools bundle their init script as a subcommand in the CLI binary and use some form of eval $(fancy-cli init your-shell)
as you may have noticed on the first 4 lines. Among other reasons, this preserves the ease-of-distribution feature of these tools as being a single binary that you place somewhere in your $PATH
and they contain all the logic to run and extend your shell with functionality.
Why do these exist?
zoxide
is a fanstastic utility that I cannot live without. It’s a smarter cd
that tracks frequently visited directories and makes it so you can jump to them with just a few characters. For example, running z blog
(or even z b
) would jump me to $HOME/github.com/disintegrator/blog
if that’s the most visited path for the given input characters.
Try writing a Go program that cd
s to your favourite folder:
If you run this command in your current shell, nothing interesting happens and you might already know why. Your current shell is a process on your machine and this Go program starts up as a process and spins a new shell again as a process. That sub-shell’s working directory changes but then its process terminates. That change will not propagate upwards to the parent processes which include your current shell process. So how can build a fancy CLI tool that does some work and cd
s you to a directory? Or, in the case of starship
, how do we build a tool that reads some data about your system and current directory and reflects it in your prompt?
The broad stroke answer is that when you’re building CLI tools in your favourite programming languages, you will need a small sidecar of a shell script, like the ones above in my .zshrc
, that adds functions and aliases to your shell upon startup. This would supercharge your current shell and every other interactive shell you start up in your terminal.
Since our program cannot drive the current shell, we need to change it to just feed us data and have functions in the current shell consume it. These functions are added to the current shell using an init script we’ll build.
Making it work
I’m going to assume you’re using Zsh or Bash and I’m going to skip over some more robust error handling which you normally want to have around. It should not be too hard to adapt this to your preferred shell.
In our Go project, we’ll update our main.go
to look like this:
Now we build our program:
Check out the output of these commands:
Notice the final line of that second command is the path and notice how it’s catpured by the shell script we embedded in our Go program. Let’s make it work in our current shell by running:
Now you’ll notice you have a new command (read: alias):
You should hopefully see something like this:
(Note that your shell prompt might look different than mine.)
What’s going on here is that the Go program is printing some interesting content to STDERR
and then printing the destination directory to STDOUT
. The eval
we ran is injecting a __noisy_cd
function with a nicer alias to it into the current shell. That function is capturing STDOUT
and then feeding it to cd
but the difference is this is happening in the current shell and not in a sub-shell. That’s why we see the directory change after the command completes.
Wrapping up
I’ve been getting into building my own personal CLI to help me be more productive and as a way to learn more Rust (though I still love Go!). I needed to work out how to properly integrate it into my shell and give it extra powers. It’s mostly what you’ve seen so far with bit more hardening which I’m sure you’ll figure out.