CLI Development; Part 1
Tower of Babel
I've recently been working on a command line tool. You can read more about it in my previous blog post. In the process of writing it I've stumbled upon a couple of ideas and best practices that I'd like to share with you in a series of blog posts. In this first post I'll go over picking the programming language and package manager for your CLI.
A single executable
The first decision we developers face when starting a new project is which language to use. In this case, like in most other, I'd suggest you think of your users first. The main way they will interact with your CLI is by executing it on the command line. (Yeah, I know, I deserve a Captain Obvious badge for this observation. Bear with me.) When building a CLI you should try to make it as simple as possible to execute. And what's easier to run that a single self-contained executable?
This puts languages that require an interpreter or a virtual machine (like Python or Java) at a disadvantage. Sure, most Linux distros come with Python pre-installed, but even they might have conflicting versions. And Windows users don't have it out of the box.
This machine disparity leads into a second consideration: building executables for multiple platforms. You users will most likely be spread over Linux, Windows and MacOS. You should build your CLI app for all three platforms. Having a toolchain that supports compilation to multiple target platforms from the same machine will make your life easier. You will be able to compile your code locally for any platform. And your CI pipeline will be simpler (no need for dedicated Mac nodes).
npm installed. However, this can still limit you in the future. For example you might be locked into an older version of Node until you are sure all of your users have upgraded.
Note that it might not be enough for technology to be ubiquitous among your users if it's cumbersome to use. For example, I'd caution against packaging your CLI as a
.jar file, even if you are targeting Java developers. Java classpath is just too much of a mess.
No external dependencies
In addition to not using an external runtime, your CLI app should also avoid using external dynamically linked libraries. Depending on your packaging and distribution setup, you can satisfy all of your dependencies at install time. However this will complicate your setup. There will be situations when just giving your users an executable file that they can immediately run pays off.
In order to satisfy this requirement you should look for a language that can bundle your dependencies within a single compiled executable. This will result in bigger file sizes, but it's much more practical. It's also worth considering how well your language interacts with the OS. You should look for both platform agnostic and powerful APIs for working with underlying OS baked into the language. Having a solid standard library helps a language meet these requirements.
If your language of choice matches all suggestions from the previous sections you can easily build a statically linked executable for any major OS. You can give that executable to your users and they can run it as is. This is super useful for testing early builds or custom flavors of your app. It does require some effort from your users to set up their
PATH to include your executable. That's something a package manager can help with.
Another argument in favor of using a package manager is upgrade flow. You will inevitably release new versions of your CLI. Package manager will alert your users that a new version is out and will make upgrading easy. It's hard to overstate benefits of having users on the latest versions of your app.
If you base your tool on a cross-platform technology like Node chances are that ecosystem has a preferred package manager, like
npm. If you choose to build native executables you should look for native package manager. This is where cross platform approach makes things easier. However, having your app compile into standalone executable simplifies integration with multiple package managers.
You will need to consider your users' habits when choosing package managers. Mac users are probably used to Homebrew. Linux has more diversity. You can start by building a Debian package, then listen to your users' feedback and include more packages as they are requested. On Windows the situation is not so clear. Chocolatey is one option, but it may not be widely adopted by your users. As a rule you should avoid forcing users to adopt a new tool just to install your app. If it come to that prefer manual installation process.
What I ended up with
For a language I picked Go. It provides dead simple compilation into a single statically linked executable for all major OS platforms. It comes with very strong standard library, good APIs for interacting with underlying OS and a vibrant open-source community.
If your audience allows for it you might be able to stick with Node +
npm combination. Alternatively you might pick some other natively compiled language. For example Rust is one popular option, though compilation for multiple targets is a bit more involved than with Go. You can find more about using Rust to build CLIs here. Lastly, you can even use Java with something like GrallVM. With Grall you can build native executables that don't require JRE to run.
For packaging I choose to create Homebrew and Debian packages. Both builds were relatively simple to automate using Jenkins CI. Homebrew in particular is easy as all it requires is a Git repository with a Ruby script describing your package. Since my CLI is used internally at work, I publish my packages to internally hosted Bitbucket and Artifactory. My Windows users do not have a favorite package manager, so I leave them with executables they can simply download anywhere onto their
PATH and use like that.
In the next installment of this blog series I'll go over what makes CLI app a good command line citizen. I'll cover topics like usability and consistency. If that seems like something you would be interested in consider: