12 Factor CLI Apps
CLIs are a fantastic way to build products. Unlike web applications, they take a small fraction of the time to build and are much more powerful. With the web, you can do whatever the developer programmed. With CLIs, you can easily mash-up multiple tools together yourself to perform advanced tasks. They require more technical expertise to use, but still work well for admin tasks, power-user tasks, or developer products.
At Heroku, we’ve come up with a methodology called the 12 factor app. It’s a set of principles designed to make great web applications that are easy to maintain. In that spirit, here are 12 CLI factors to keep in mind when building your next CLI application. Following these principles will offer CLI UX that users will love.
We’ve also built a CLI framework called oclif that is designed to follow these principles and build great CLIs in Node.
1. Great help is essential
Having good help documentation for a CLI is extremely important. It’s far more important than when building a web application as you can’t guide the user with a UI.
A CLI should provide in-CLI help and help on the web (READMEs are a great place). That provides the immediate-ness of not needing to leave the terminal while also giving Google the opportunity to help your users (make sure Google is indexing the docs too btw). I would skip man pages as they aren’t used that often anymore. They don’t work on windows anyways.
In the CLI, make sure all of the following displays the help. You can’t control what the user inputs so all of these must show help.
$ mycli
$ mycli --help
$ mycli help
$ mycli -h
$ mycli subcommand --help
$ mycli subcommand -h
-h,--help
should be a reserved flag used for help only. In the case of $ mycli subcommand help
. You can’t guarantee that help
isn’t an argument that should be passed to the subcommand. In that case, it’s better to only show the help if it would otherwise error out with an invalid argument error. There is actually a Heroku app named “help” which has caused this to happen to me in the past.
Shell completion is another great way to offer help to the users.
In terms of the help itself, show a description of the command, description of the arguments, description of all the flags, and most importantly: provide examples of common usage of the CLI. Even if the usage is obvious to you, it’s by far the most common referenced bit of documentation users will find.
Of course, by building a CLI with oclif you get all this for free. Online docs, in-CLI docs, and autocomplete. We’re even working on a linter to help you enforce descriptions everywhere.
Example oclif readme auto-documentation
Note that while I don’t think in general it is worth spending a lot of time on man pages, I do plan to add man page support to oclif soon anyways. The current in-CLI docs are already very similar to man page style so it should be a pretty easy addition. For a framework it makes sense since I can do it once for all CLIs.
2. Prefer flags to args
A CLI can accept 2 types of shell inputs: flags and args. Flags require a bit more typing, but make the CLI much clearer. For example, in the Heroku CLI we used to have a command called heroku fork.
It took in a source app to copy from and a destination app to copy to. Initially, this used a flag and an argument like this:
$ heroku fork FROMAPP --app TOAPP
Using a flag and an arg, it was really confusing which was the source and which was the destination app. We changed this to use flags for both:
$ heroku fork --from FROMAPP --to TOAPP
This way it’s clear which is the source and which is the destination.
Note that we actually removed this command from the Heroku CLI but it’s still a great example of how args can be confusing.
Sometimes args are just fine though when the argument is obvious such as $ rm file_to_remove
. A good rule of thumb is 1 type of argument is fine, 2 types are very suspect, and 3 are never good.
For variable length arguments, it’s fine to have multiple arguments. (For example, $ rm file1 file2 file3
). It’s just when they’re different types that it becomes confusing to the user.
Flags are also much easier to write autocomplete logic for as you know exactly what the value should be.
For CLIs that pass flags off to some other process (such as heroku run
), the flag parser should accept a --
argument to denote that it should stop parsing and simply pass everything down as an argument. This allows you to run a command like heroku run -a myapp -- myscript.sh -a arg1
(This shows how -a
can be a flag for heroku run
but also a different -a
is passed to the dyno).
3. What version am I on?
Ensure you can get the CLI version through any of the following:
$ mycli version
$ mycli --version
$ mycli -V
Unless it’s a single-command CLI that also has a -v,--verbose
flag, $ mycli -v
should also just display the CLI version. It’s frustrating to run 3 different commands to get the version for a CLI until you find the right one.
The version command is a main place you’ll ask users for debugging information so it’s a good place to add any helpful extra information aside from just the version number that might help you diagnose issues.
I also suggest sending the version string as the User-Agent so you can debug server-side issues. (Assuming your CLI uses an API of some sort)
4. Mind the streams
Stdout and stderr provide a way for you to output messages to the user while also allowing them to redirect content to a file. For example:
$ myapp > foo.txt
Warning: something went wrong
Because this warning is on stderr, it doesn’t end up in the file. Putting the warning on stdout would not only hide the warning here, but it would be especially problematic for structured data like JSON or binary. Use stderr for errors and warnings which by default will always end up on the screen even if stdout is redirected.
Not everything on stderr is an error though. For example, you can use curl to download a file but the progress output is on stderr. This allows you to redirect the stdout while still seeing the progress.
In short: stdout is for output, stderr is for messaging.
If you run a subcommand in your CLI, make sure you pipe the stderr of that subcommand up to the user always. This way any issues are surfaced ultimately to the user’s screen.
5. Handle things going wrong
Things go wrong in CLIs much more often than in web apps. Without a UI to guide the user, the only thing we can do is display an error to the user. This is expected behavior and part of using any CLI.
First and foremost, make your errors really great. A great error message should contain the following:
- Error code
- Error title
- Error description
- How to fix the error
- URL for more information
For example, if our CLI errored out with a file permission issue, we could show the following:
$ myapp dump -o myfile.out
Error: EPERM - Invalid permissions on myfile.out
Cannot write to myfile.out, file does not have write permissions
Fix with: chmod +w myfile.out
https://github.com/jdxcode/myapp
Just think if every CLI was this helpful how incredible it would be to be a programmer.
Sometimes though you will have unhandled errors you didn’t expect the user to run into. For that, have a way to view full traceback information as well as full debug output with environment variables.
In oclif we use the debug module which allows us to output debug statements grouped by component if the DEBUG
environment variable is set. We have a lot of verbose logging if debug is fully enabled which is incredibly valuable to us when debugging issues.
Error logs can also be useful for post-mortem debugging but ensure they have timestamps, truncate them occasionally so they don’t eat up space on disk, and make sure they don’t contain ansi color codes.
6. Be fancy!
Modern CLIs shouldn’t be afraid to show off. Use colors/dimming to highlight important information. Use spinners and progress bars to show long-running tasks to tell the user you’re still working. Leverage OS notifications when a very long-running task is done.
spinner example
Still, you need to be able to fall back and know when to fall back to more basic behavior. If the user’s stdout isn’t connected to a tty (usually this means their piping to a file), then don’t display colors on stdout. (likewise with stderr)
Spinners and progress bars are also not a good idea when it’s not a tty. These work by outputting ansi codes to overwrite which only works on a screen. You never want to output those codes to a file.
The user may have reasons for just not wanting this fancy output. Respect this if TERM=dumb
, NO_COLOR is set, or if they specify --no-color
. I would also suggest adding in an app-specific MYAPP_NOCOLOR=1
environment variable as well in case they want to disable color on just your CLI.
7. Prompt if you can
For accepting input, if stdin is not a tty then prompt rather than forcing the user to specify a flag. Never require a prompt though. The user needs to be able to automate your CLI in a script so allow them to override prompts always.
Prompt example
8. Use tables
Note that cli.table()
from cli-ux@5 allows you to easily create tables following these principles.
Tables are a very common way to output data in a CLI. It’s important that each row of your output is a single ‘entry’ of data. Never output table borders. It’s noisy and a huge pain for parsing. This is an example of what not to do:
By keeping each row to a single entry, you can do things like pipe to wc to get the count of lines, or grep to filter each line:
listing files, piping to wc to count number of files, then doing the same of files with “.js” in the name. Note that wc has 3 counts: lines, words, and characters. You may also notice ls follows factor #6 where it behaves differently when piped vs when it is outputting to the screen (tty).
Be mindful of the screen width. Only show a few columns by default but allow the user to pass --columns
with a comma-separated list of column names to add less common types.
Truncate rows that are going to spill over the current screen width unless --no-truncate
is set.
Show column headers by default but allow them to be hidden with --no-headers
.
Allow users to pass --filter
to filter specific columns. (grep can usually do this, but a flag can filter on specific cell values)
Allow sorting by column with --sort
. Allow inverse and multi-column sort as well.
Allow output in csv or json. Displaying raw output as json is a great way to output structured data. It can be manipulated with jq. While jq is incredibly useful, cut and awk are simpler tools that work better with csv data.
9. Be speedy
CLIs need to start quickly. Use $ time mycli
to benchmark your CLI. Here is a rough guide:
- <100ms: very fast (sadly, not feasible for scripting languages)
- 100ms–500ms: fast enough, aim here
- 500ms-2s: usable, but not going to impress anyone
- 2s+: languid, users will prefer to avoid your CLI at this point
Obviously if your CLI is performing a major task like downloading a large file or something heavily CPU-bound it won’t perform as quickly. In that case, make sure to show a progress bar or at least a spinner. Even just a spinner will give the impression the CLI is much faster than it is.
10. Encourage contributions
Keep your code open source. This allows users to poke around and diagnose problems themselves. It’s healthy to the community to offer a sample in case it’s useful to others. It makes organizations look great as well.
Make sure you pick a license of course. GitHub and GitLab are great places to put your CLI and the README gives you a perfect place to provide an overview of the CLI.
Write up how to run the CLI locally and run the test suites. Offer a contribution guideline doc to tell contributors what you expect in terms of commit syntax, code quality, tests, or whatever else is important for them to know.
Add a code of conduct. Even if you don’t feel that it’s necessary. It’s important to some people and they’ll feel much better by seeing one. To others they probably won’t even notice it. It’ll be helpful in the event someone is being rude and you have a document to point to.
In oclif, the plugins system offers a great way for people to extend your CLI. These plugins can later be included as a core plugin to provide functionality to all users.
11. Be clear about subcommands
There are 2 types of CLIs: single and multi-command. Single-command CLIs are basic UNIX-style CLIs like cp
or grep
. Multi-commands are more like git
or npm
which accept a subcommand as the first argument.
If a CLI is simple and only performs one basic task, it’s good fit for a single-command CLI. Most CLIs, however, will probably benefit from using subcommands.
Either way, if the user doesn’t pass anything arguments to the CLI, it’s always better to list the subcommands (for multi) or display the help (for single) rather than do some default behavior. Usually the user will do this before doing anything else.
When you start using subcommands, it doesn’t take long before sub-subcommands become useful (we call these “topics” in oclif). Git chooses to separate subcommands from topics with spaces:
$ git submodule add git@github.com:oclif/command
Where in the Heroku CLI we use colons:
$ heroku domains:add www.myapp.com
Colons are preferable to help delineate the command between the arguments passed to the command. The user quickly learns that argument 1 is the command and how to get help for it.
Getting into the weeds a little bit, there is another technical reason why we prefer colons. For topic-level commands like $ heroku domains
we list all the domains of an app. If we used spaces to separate commands from subcommands andwanted this topic-level command to accept an argument, the parser could have no way to determine if the argument was a subcommand or argument to the topic command. Therefore, using spaces to separate makes it so you cannot have topic-commands also accept an argument.
12. Follow XDG-spec
XDG-spec is a great standard that should be used to find out where to put files. Unless environment variables like XDG_CONFIG_HOME
say otherwise, use ~/.config/myapp
for config files, and ~/.local/share/myapp
for data files.
For cache files though, use ~/.cache/myapp
on Unix but on MacOS it’s better to default to ~/Library/Caches/myapp
. On Windows you can use %LOCALAPPDATA%\myapp
.
12 Factor CLI Apps的更多相关文章
- 12 Factor App
The Twelve-Factor App Introduction In the modern era, software is commonly delivered as a service: c ...
- docker应用容器化准则—12 factor
在云的时代,越来越多的传统应用需要迁移到云环境下,新应用也要求能适应云的架构设计和开发模式.而12-factor提供了一套标准的云原生应用开发的最佳原则. 在容器云项目中应用容器化主要参考12-Fac ...
- 12 factor 目录
I. 基准代码 一份基准代码,多份部署 II. 依赖 显式声明依赖关系 III. 配置 在环境中存储配置 IV. 后端服务 把后端服务当作附加资源 V. 构建,发布,运行 严格分离构建和运行 VI. ...
- 如何写出安全的、基本功能完善的Bash脚本
每个人或多或少总会碰到要使用并且自己完成编写一个最基础的Bash脚本的情况.真实情况是,没有人会说"哇哦,我喜欢写这些脚本".所以这也是为什么很少有人在写的时候专注在这些脚本上. ...
- oclif cli app开发简单试用
oclif 是heroku 开源的cli 开发框架,有一篇关于12 factor cli app 开发的文章很值得看看 https://medium.com/@jdxcode/12-factor-cl ...
- 161027、Java 中的 12 大要素及其他因素
对于许多人来说,"原生云"和"应用程序的12要素"是同义词.本文的目的是说有很多的原生云只坚持了最初的12个因素.在大多数情况下,Java 能胜任这一任务.在本 ...
- [Oracle EBS R12]SQL Queries and Multi-Org Architecture in Release 12 (Doc ID 462383.1)
In this Document Abstract History Details Previous Releases Release 12 Multi-Org Session ...
- SQL Queries and Multi-Org Architecture in Release 12
In this Document Abstract History Details Previous Releases Release 12 Multi-Org Session ...
- Configuration Reference In Vue CLI 3.0
Configuration Reference This project is sponsored by #Global CLI Config Some global configurations ...
随机推荐
- DownLoadImage
Private Declare Function URLDownloadToFile Lib "urlmon" Alias "URLDownloadToFileA&quo ...
- python-day39--数据库
1.什么是数据:描述事物的特征,提取对自己有用的信息 称之为数据 2..什么是数据库: 数据库即存放数据的仓库,只不过这个仓库是在计算机存储设备上,而且数据是按一定的格式存放的 为什么要用数据库: ...
- IP分类
IP: IP分为公有ip和私有ip. 私有ip分为以下5类: 类别 ip范围 子网掩码 A 1.0.0.0------127.255.255.255 255.0.0.0 B 128.0.0.0---1 ...
- hpu积分赛(回溯法)
问题 : 不开心的小明① 时间限制: 1 Sec 内存限制: 128 MB 提交: 2 解决: 1 题目描述 一天, 小明很不开心,先是向女神表白被拒, 数学又考了0分, 回家的路上又丢了钥匙, 他非 ...
- Eclipse properties文件编辑插件
安装 Properties Editor 步骤:help--->Install New Software...---> 名称:Properties Editor URL:http://pr ...
- js 数组复制问题
师兄面试回来问个问题,js中数组怎么复制,工作中没遇到,面试也涨见识 了,他给我说了下,太晚没留心,打早起来研究下,写个dom,来看下 代码如下 <!doctype html> <h ...
- POJ 3308 Paratroopers 最大流,乘积化和 难度:2
Paratroopers Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 7267 Accepted: 2194 Desc ...
- prayer OJ M
这一题是一把辛酸泪啊...一个半小时ac的... 首先,考虑到如果要一条路径最小,那么肯定是没有值大于等于3的 显然如果有一个大于等于3的,那么这个数把路径分成两份,一份有k个,一个n-k-1个 那么 ...
- 在主Android Activity中加载Fragment的一般简易方法 ,来模拟一个微信界面。
在Fragment的生命周期中,需要重点关注onCreate.onCreateView.onViewCreated.Activity与Fragment生命周期在设计模式上大体一致. package c ...
- 软工作业No.9 第六周 事后诸葛亮分析报告
甜美女孩项目2048结果 整理:邓画月.曾祎祺 设想和目标 1. 我们的软件要解决什么问题?是否定义得很清楚?是否对典型用户和典型场景有清晰的描述? 弄一个给用户消磨时间的游戏,定义的很清楚.该游戏玩 ...