Show nav
Heroku Dev Center
    • Getting Started
    • Reference
    • Learning
  • By Language
    • Node.js
    • Ruby
    • Java
    • PHP
    • Python
    • Go
    • Scala
    • Clojure
  • Develop
    • Heroku Architecture
    • Features
    • Command Line
    • Deployment
    • Security
    • Application Architecture
    • Extending Heroku
  • Support
  • More
    Additional Resources
    • Elements
    • Products
    • Pricing
    • Careers
    • Help
    • Status
    • Events
    Heroku Blog

    Heroku Blog

    Find out what's new with Heroku on our blog.

    Visit Blog
  • Log inorSign up
  • Getting Started
  • Reference
  • Learning
  • Reference

    • Heroku Architecture
    • Features
    • Command Line
    • Deployment
    • Troubleshooting
    • Collaboration
    • Security
    • Support
    • Accounts & Billing
    • Organization Accounts
    • Heroku Postgres
    • Heroku Redis
    • Apache Kafka on Heroku
    • Heroku Connect
    • Dev Center
    • Heroku Labs
    • Languages
    • Extending Heroku
    • Changelog

CLI Style Guide

Last updated 30 January 2017

Table of Contents

  • Mission statement
  • heroku-cli-util
  • Naming the command
  • Description
  • Input
  • Output
  • Colors
  • Human-readable output vs machine-readable output
  • Stdout/Stderr
  • Dependency guidelines

Heroku CLI plugins should provide a clear user experience, targeted primarily for human readability and usability, which delights the user, while at the same time supporting advanced users and output formats. This article provides a clear direction for designing delightful CLI plugins.

Mission statement

The Heroku CLI is for humans before machines. The primary goal of anyone developing CLI plugins should always be usability. Input and output should be consistent across commands to allow the user to easily learn how to interact with new commands.

heroku-cli-util

heroku-cli-util is a dependency that all plugins use. It offers a set of helpful UI components for working with plugin output. For example, if we had a plugin that needed to show a warning, we could do the following:

const cli = require('heroku-cli-util')
cli.warn("WARNING: something isn't quite right")
// ▸   WARNING: something isn't quite right

See the README for all of the components heroku-cli-util includes. For the rest of the code samples in this document, cli will refer to an instance of heroku-cli-util.

Naming the command

Plugins are made up of topics and commands. For the command heroku apps:create, apps is the topic and create is the command.

Generally topics are plural nouns and commands are verbs.

Ideally plugins should export a single topic, or add commands to an existing topic.

Topic and command names should always be a single, lowercase word without spaces, hyphens, underscores, or other word delimiters. Colons, however, are allowed as this is how to define subcommands (such as heroku apps:favorites:add).

Because topics are generally nouns, the root command of a topic usually lists those nouns. So in the case of heroku config, it will list all the config vars for an app. Never create a *:list command such as heroku config:list.

Description

Topic and command descriptions should be provided for all topics and commands. They should fit on 80 character width screens and they should not end in a period.

Input

Input to commands is typically provided by flags and args. Stdin can also be used in cases where it is useful to stream files or information in (such as with heroku run).

Flags

Flags are preferred to args. They involve a bit more typing, but make the use of the CLI clearer. For example, heroku fork used to accept an argument for the app to be created, as well as the standard --app flag to specify the source app to be forked.

So using heroku fork used to work like this:

$ heroku fork destapp -a sourceapp

This is confusing to the user since it isn’t clear which app they are forking from and which one they are forking to. By switching to required flags, we instead expect input in this form:

$ heroku fork --from sourceapp --to destapp

This also allows the user to specify the flags in any order, and gives them the confidence that they are running the command correctly. It also allows us to show better error messages.

Ensure that descriptions are provided for all flags, that the descriptions are in lowercase, that they are concise (so as to fit on narrow screens), and that they do not end in a period to match other flag descriptions.

While this functionality is not yet GA, flags will allow us to provide autocomplete in a much better fashion than args. This is because when the user types:

$ heroku info --app <tab><tab>

We know without question that the next thing to complete is an app name and not another flag or other type of argument.

See Developing CLI Plugins for more on how to use flags.

Arguments

Arguments are the basic way to provide input for a command. While flags are generally preferred, they are sometimes unnecessary in cases where there is only 1 argument, or the arguments are obvious and in an obvious order.

In the case of heroku access:add, we can specify the user we want to give access to with an argument, but the privileges are provided with a required flag:

$ heroku access:add user@example.com --privileges deploy

If this was done with only arguments, it wouldn’t be clear if the privileges should go before or after the email. Using a required flag instead allows the user to specify it either way.

Prompting

Prompting for missing input provides a nice way to show complicated options in the CLI. For example, heroku keys:add shows the following if multiple ssh keys are available to upload:

$ heroku keys:add
heroku keys:add
? Which SSH key would you like to upload? (Use arrow keys)
❯ /Users/jdickey/.ssh/id_legacy.pub
  /Users/jdickey/.ssh/id_personal.pub
  /Users/jdickey/.ssh/id_rsa.pub

Use inquirer to show prompts like this.

However, if prompting is required to complete a command, this means the user will not be able to script the command. Ensure that args or flags can always be provided to bypass the prompt. In this case, heroku keys:add can take in an argument for the path to an ssh key to skip the prompt.

Output

In general the CLI offers 2 types of commands, output commands that show data, as well as action commands that perform an action.

Output commands

Output commands are simply commands that display data to the user. They take many forms, but the simplest is just printing to stdout with cli.log():

cli.log('output this message to the user')
// output this message to the user

Using cli.log() instead of console.log() allows you to mock out stdout in tests. For more on this, see Testing CLI Plugins.

Styled hash

For displaying information on an object such as with heroku info, use cli.styledHash():

cli.styledHash({
  id: 100,
  name: 'this object'
}, ['id', 'name'])
// id:   100
// name: this object

Providing the names of all the keys of the object is important to ensure that they are displayed in the intended order.

cli.styledHash() generally should always have a cli.styledHeader() label before listing out the items.

Tables

For tabular data, use cli.table():

cli.table([
  {id: 100, name: 'object a'},
  {id: 101, name: 'object b'},
  {id: 102, name: 'object c'}
], {
  keys: [
    {key: 'id'},
    {key: 'name'}
  ]
})

// id   name
// ───  ────────
// 100  object a
// 101  object b
// 102  object c

Sometimes the table headers do not need to be labeled because they are obvious and can be replaced with a simpler cli.styledHeader() header.

See below for more details on using cli.table().

Headers

Headers (via cli.styledHeader()) allow us to decorate command output with extra information.

Showing multiple headers is generally discouraged as this makes commands difficult to use with grep. See below for how to make commands parseable with grep.

Action commands

Action commands are those that perform some remote task. For example, heroku maintenance:on puts an app into maintenance mode:

$ heroku maintenance:on --app myapp
Enabling maintenance mode for ⬢ myapp... done

Use cli.action() from heroku-cli-util to show this output. Using this component ensures that warnings and errors from the API are properly displayed, the spinner is displayed correctly when it is a tty, alternative output is used when not a tty, and that the spinner will work on the right platform.

Actions are displayed on stderr because they are out-of-band information on a running task.

Colors

Using color is encouraged in commands to help the user quickly read command output. Some nouns in the CLI such as apps and config vars have standard colors that should be used when possible:

cli.log(`this is an app: ${cli.color.app(myapp.name)}`)
cli.log(`this is a config var: ${cli.color.configVar(myapp.name)}`)

See the standard colors here.

When a standard color isn’t available, cli.color can be used to show other colors as well:

Application Diagram

Suggested colors are magenta, cyan, blue, green, and gray. Don’t forget that .dim and .bright, .underline, and background colors can also be used to provide more variety in color use.

Be mindful with color. Too many contrasting colors in the same place can quickly begin to compete for the user’s attention. Using just a couple of colors and maybe dim/bolding existing ones can often provide enough contrast.

Yellow and red may also be used, but note that these typically are saved for errors and warning messages.

Color can be disabled by the user by adding --no-color, setting COLOR=false, or when the output is not a tty.

Human-readable output vs machine-readable output

Terse, machine-readable output formats can also be useful but shouldn’t get in the way of making beautiful CLI output. When needed, commands should offer a --json and/or a --terse flag when valuable to allow users to easily parse and script the CLI.

Care should be taken that in future releases of commands that (when possible) commands do not change their inputs and stdout after general availability in ways that will break current scripts. Generally this means additional information is OK, but modifying existing output is problematic.

grep-parseable

Human-readable output should be grep-parseable, but not necessarily awk-parseable. For example, let’s look at heroku regions. heroku regions at one point showed output like the following:

$ heroku regions
Common Runtime
==============
eu         Europe
us         United States

Private Spaces
==============
frankfurt  Frankfurt, Germany
oregon     Oregon, United States
sydney     Sydney, Australia
tokyo      Tokyo, Japan
virginia   Virginia, United States

While this shows all the information about the available regions, you lose the ability to use grep to filter the data. Here is a better way to display this information:

$ heroku regions
ID         Location                 Runtime
─────────  ───────────────────────  ──────────────
eu         Europe                   Common Runtime
us         United States            Common Runtime
frankfurt  Frankfurt, Germany       Private Spaces
oregon     Oregon, United States    Private Spaces
sydney     Sydney, Australia        Private Spaces
tokyo      Tokyo, Japan             Private Spaces
virginia   Virginia, United States  Private Spaces

Generate these tables by using cli.table() from heroku-cli-util.

Now you can use grep to filter just common runtime spaces:

$ heroku regions | grep "Common Runtime"
eu         Europe                   Common Runtime
us         United States            Common Runtime

Or if to see just tokyo:

$ heroku regions | grep tokyo
tokyo      Tokyo, Japan             Private Spaces

The older header format would make retrieving the type of the region very difficult.

Grep is useful for almost all commands and care should be taken that it always shows useful format (even if the --context flag is needed to show sibling rows).

json-parseable

Sometimes printing tables can grow to be too long to reasonably fit in the CLI. Using the --json flag allows plugin developers to provide users with much more data but still give them the ability to parse. For example, look at heroku releases:

$ heroku releases
=== myapp Releases
v122  Attach HEROKU_POSTG…   jeff@heroku.com               2016/04/26 19:58:19 -0700
v121  Set foo config vars    jeff@heroku.com               2016/04/24 21:15:18 -0700
v120  Update REDISCLOUD b…   rediscloud@addons.heroku.com  2016/04/24 11:00:28 -0700
v119  Attach REDISCLOUD (…   jeff@heroku.com               2016/04/24 10:59:56 -0700

If this is run with --json, then the full output of each release is generated (full output not displayed for readability):

$ heroku releases --json
[
  {
    "description": "Attach otherdb (@ref:postgresql-rectangular-2230)",
    "user": {
      "email": "jeff@heroku.com",
      "id": "5985f8c9-a63f-42a2-bec7-40b875bb986f"
    },
    "version": 111,
    "status": "success"
  }
]

If a user wanted to show just the version and user for each release, jq can be used to show just those 2 fields:

$ heroku releases --json | jq -r '"\(.[].version) \(.[].user.email)"'
122 jeff@heroku.com
121 jeff@heroku.com
120 rediscloud@addons.heroku.com
119 jeff@heroku.com

Ensuring that the CLI command can support both grep and jq allows us to offer powerful scripting functionality, but without sacrificing beautiful UX that would be required with a tool like awk.

Stdout/Stderr

Stdout should be used for all output and stderr for warning, errors and out of band information (like cli.action()).

Dependency guidelines

Be mindful of dependencies. The plugin architecture allows you to install any npm package to your plugin but this comes with some cost and potential hazards.

Lock down dependencies

Most importantly make sure that you lock down your dependency versions and not rely on semver—in other words freezing them to specific versions. If you don’t lock the dependencies down, the CLI’s autoupdater will update them to new versions without anyone pushing a release of the plugin. This is dangerous as a minor or even a patch release of a dependency could break your plugin and you may not be around to see it. Semver is a great guide, but bugs or unintended API changes can always happen.

In your package.json, set versions like this:

{
  "dependencies": {
    "heroku-cli-util": "5.7.1",
    "lodash": "4.6.1"
  }
}

Not loosely like this:

{
  "dependencies": {
    "heroku-cli-util": "^5.7.1",
    "lodash": "^4.6.1"
  }
}

npm will by default not use these exact versions when you add packages with npm install --save. You can either manually set the versions in the package.json or use the --save-exact flag to npm install. To make this behavior default, you can add these lines to your ~/.npmrc file:

save=true
save-exact=true

Native dependencies

Do not use native dependencies if possible. Native dependencies typically require the user of node-gyp, which requires Python, and generally is likely to have problems compiling on all user environment, especially Windows.

If you must use a native dependency, make sure that it works on various environments by testing in virtual machines and clearly document what is required to install the plugin on these environments. For Windows testing, Microsoft offers free virtual machines.

Be judicious with dependencies

Use npm-check to see if you have any out of date or unreferenced dependencies in your plugin.

Try to ensure you’re not using too many dependencies. This uses up the user’s disk space, makes install and update times longer, and could make the plugin slow. Use npm ls and du -d1 node_modules | sort -n to see how many dependencies a plugin has and how large they are.

Duplicate dependencies will be shared among other plugins if the use the same version. For example, if 2 different CLI plugins use lodash@4.6.1, that dependency will be shared among the 2 plugins. See this npm article for more on how this works.

Use dev dependencies

Use npm install --save-dev for packages that are only needed to work on the plugin and not for running it. This is good for testing packages, documentation packages, and linters like jshint. These packages will be installed if you run npm install within the plugin’s repo, but not installed by the user when they run heroku plugins:install.

Blacklisted dependencies

The following npm packages are not recommended:

Package Better Alternative
request cli.got() very large plugin with far too many dependencies. cli.got() will automatically resolve proxies.
underscore lodash lodash has more functional tools, built with node in mind, used by many other plugins
q bluebird or node bluebird used by other plugins and has a better feature set

Keep reading

  • Command Line
  • Developing CLI Plugins
  • Testing CLI Plugins

Feedback

Log in to submit feedback.

CLI Usage

Information & Support

  • Getting Started
  • Reference
  • Learning
  • Changelog
  • Blog
  • Support Channels
  • Status

Language Reference

  • Node.js
  • Ruby
  • Java
  • PHP
  • Python
  • Go
  • Scala
  • Clojure

Other Resources

  • Careers
  • Elements
  • Products
  • Pricing

Subscribe to our monthly newsletter

  • RSS
    • Dev Center Articles
    • Dev Center Changelog
    • Heroku Blog
    • Engineering Blog
  • Twitter
    • Dev Center Articles
    • Dev Center Changelog
    • Heroku
    • Heroku Status
  • Facebook
  • Instagram
  • Github
  • LinkedIn
Heroku is acompany

© Salesforce.com

  • heroku.com
  • Terms of Service
  • Privacy
  • Cookies