DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Terraform Best Practices: The 24 Practices You Should Adopt
  • Using Environment Variable With Angular
  • Let’s Build an End-to-End NFT Project Using Truffle Suite
  • Anypoint CLI Commands in MuleSoft

Trending

  • Enforcing Architecture With ArchUnit in Java
  • How to Ensure Cross-Time Zone Data Integrity and Consistency in Global Data Pipelines
  • Detection and Mitigation of Lateral Movement in Cloud Networks
  • Orchestrating Microservices with Dapr: A Unified Approach

How to Build a Simple CLI With Oclif

Not a fan of shell scripting? Use Oclif to build CLI tools with Node.js!

By 
Alvin Lee user avatar
Alvin Lee
DZone Core CORE ·
Jun. 24, 22 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
6.3K Views

Join the DZone community and get the full member experience.

Join For Free

Salesforce developers have contributed much to the open-source community. Among their many contributions is an important, but perhaps lesser-known, project named oclif. The Open CLI Framework was announced in early 2018 and has since grown to become the foundation for the Salesforce CLI and the Heroku CLI.

In this post, we will provide a brief overview of oclif, and then we’ll walk through how to build a simple CLI with oclif.

A Brief History of Oclif

Oclif started as an internal Heroku project. Heroku has always been focused on developer experience, and its CLI sets the standard for working with a service via the API. After all, Heroku is the creator of git push heroku for deployment—a standard now widely used across the industry.

If you've ever run heroku ps or sfdx auth:list, then you've used oclif. From the start, oclif was designed to be an open, extensible, lightweight framework for quickly building CLIs, both simple and complex.

More than four years after release, oclif has become the authoritative framework for building CLIs. Some of the most popular oclif components see more than a million weekly downloads. The oclif project is still under active development.

Some examples of high-profile companies or projects built via oclif include:

  • Salesforce
  • Heroku
  • Twilio
  • Adobe Firefly
  • Stream

Why Would a Developer Choose Oclif Today?

There are many reasons one might want to build a CLI. Perhaps your company has an API, and you'd like to make it easier for customers to consume it. Maybe you work with an internal API, and you'd like to run commands via the CLI to automate daily tasks. In these scenarios, you could always write Powershell or Bash scripts or build your own CLI from scratch, but oclif is the best option.

Oclif is built on Node.js. It runs on all major operating systems and has multiple distribution options. Along with being fast, oclif is also self-documenting and supports plugins, allowing developers to build and share reusable functionality. As oclif rapidly gains adoption, more and more libraries, plugins, and useful packages are becoming available. 

For example, cli-ux comes pre-packaged with the @oclif/core package and provides common UX functionality such as spinners and tables, and progress bars, which you can add to your CLI.

It's easy to see why oclif is a success and should be your choice for building a CLI.

Introduction to Our Mini-Project

Let’s set the scene for the CLI you will build. You want to build your own CLI for one of your passions: space travel.

You love space travel so much that you watch every SpaceX launch live, and you check the HowManyPeopleAreInSpaceRightNow.com page more than you care to admit. You want to streamline this obsession by building a CLI for space travel details, starting with a simple command that will show you the number of people currently in space. Recently, you discovered a service called Open Notify that has an API endpoint for this purpose.

We'll use the oclif generate command to create our project, which will scaffold a new CLI project with some sensible defaults. Projects created with this command use TypeScript by default—which is what we'll use for our project—but can be configured to use vanilla JavaScript as well.

Creating the Project

To start, you’ll need Node.js locally if you don’t already have it. The oclif project requires the use of an active LTS version of Node.js.

You can verify the version of Node.js that you have installed via this command:

Shell
 
/ $ node -v
v16.15.0

Next, install the oclif CLI globally:

Shell
 
/ $ npm install -g oclif

Now, it’s time to create the oclif project using the generate command:

Shell
 
/ $ oclif generate space-cli

     _-----_
    |       |    ╭──────────────────────────╮
    |--(o)--|    │  Time to build an oclif  │
   `---------´   │    CLI! Version: 3.0.1   │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

Cloning into '/space-cli'...

At this point, you will be presented with some setup questions. For this project, you can leave them all blank to use the defaults (indicated by the parentheses), or you can choose to fill them out yourself. The final question will ask you to select a package manager. For our example, choose npm.

Starting With Oclif’s Hello World Command

From here, oclif will finish creating your CLI project for you. In the bin/ folder, you'll find some node scripts that you can run to test out your CLI while you're developing. These scripts will run the command from the built files in the dist/ folder. If you just run the script as is, you'll see something like this message:

Shell
 
/ $ cd space-cli/

/space-cli $ ./bin/run
oclif example Hello World CLI

VERSION
  space-cli/0.0.0 darwin-arm64 node-v16.15.0

USAGE
  $ space-cli [COMMAND]

TOPICS
  hello    Say hello to the world and others
  plugins  List installed plugins.

COMMANDS
  hello    Say hello
  help     Display help for space-cli.
  plugins  List installed plugins.

By default, if you don't specify a command to run for the CLI, it will display the help message. Let's try again:

Shell
 
/space-cli $ ./bin/run hello
 >   Error: Missing 1 required arg:
 >   person  Person to say hello to
 >   See more help with --help

This time, we received an error. We're missing a required argument: We need to specify who we're greeting!

Shell
 
/space-cli $ ./bin/run hello John
 >   Error: Missing required flag:
 >    -f, --from FROM  Whom is saying hello
 >   See more help with --help

We received another helpful error message. We need to specify the greeter as well, this time with a flag:

Shell
 
/space-cli $ ./bin/run hello John --from Jane
hello John from Jane! (./src/commands/hello/index.ts)

Finally, we've properly greeted John, and we can take a look at the hello command's code, which can be found in src/commands/hello/index.ts. It looks like this:

TypeScript
 
import {Command, Flags} from '@oclif/core'

export default class Hello extends Command {
  static description = 'Say hello'

  static examples = [
    `$ oex hello friend --from oclif
hello friend from oclif! (./src/commands/hello/index.ts)
`,
  ]

  static flags = {
    from: Flags.string({char: 'f', description: 'Whom is saying hello', required: true}),
  }

  static args = [{name: 'person', description: 'Person to say hello to', required: true}]

  async run(): Promise<void> {
    const {args, flags} = await this.parse(Hello)

    this.log(`hello ${args.person} from ${flags.from}! (./src/commands/hello/index.ts)`)
  }
}

As you can see, an oclif command is simply defined as a class with an async run() method, which unsurprisingly contains the code that is executed when the command runs. In addition, some static properties provide additional functionality, although they're all optional.

  • The description and examples properties are used for the help message.
  • The flags property is an object which defines the flags available for the command, where the keys of the object corresponding to the flag name. We'll dig into those a bit more later.
  • Finally, args is an array of objects representing arguments the command can take with some options.

The run() method parses the arguments and flags and then prints out a message using the person argument and from flag using this.log() (a non-blocking alternative to console.log). Notice both the flag and argument are configured with required: true, which is all it takes to get validation and helpful error messages like those we saw in our earlier testing.

Creating Our Own Command

Now that we understand the anatomy of a command, we’re ready to write our own. We'll call it humans, and it will print out the number of people currently in space. You can delete the hello folder in src/commands, since we won't need it anymore. The oclif CLI can help us scaffold new commands, too:

Shell
 
/space-cli $ oclif generate command humans

     _-----_
    |       |    ╭──────────────────────────╮
    |--(o)--|    │    Adding a command to   │
   `---------´   │ space-cli Version: 3.0.1 │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |     
   __'.___.'__   
 ´   `  |° ´ Y ` 

   create src\commands\humans.ts
   create test\commands\humans.test.ts

No change to package.json was detected. No package manager install will be executed.

Now we have a humans.ts file we can edit, and we can start writing our command. The Open Notify API endpoint we will use can be found at the following URL: http://open-notify.org/Open-Notify-API/People-In-Space/

As you can see in the description, the endpoint returns a simple JSON response with details about the humans currently in space. Replace the code in src/commands/humans.ts with the following:

TypeScript
 
import {Command} from '@oclif/core'
import {get} from 'node:http'

export default class HumanCommand extends Command {
  static description = 'Get the number of humans currently in space.'

  static examples = [
    '$ space-cli humans\nNumber of humans currently in space: 7',
  ]

  public async run(): Promise<void> {
    get('http://api.open-notify.org/astros.json', res => {
      res.on('data', d => {
        const details = JSON.parse(d)
        this.log(`Number of humans currently in space: ${details.number}`)
      })
    }).on('error', e => {
      this.error(e)
    })
  }
}

Here’s a breakdown of what we’re doing in the code above:

  1. Send a request to the Open Notify endpoint using the http package.
  2. Parse the JSON response.
  3. Output the number with a message.
  4. Catch and print any errors we may encounter along the way.

For this first iteration of the command, we didn't need any flags or arguments, so we're not defining any properties for those.

Testing Our Basic Command

Now, we can test out our new command. First, we'll have to rebuild the dist/ files, and then we can run our command just like the hello world example from before:

Shell
 
/spacecli $ npm run build

> space-cli@0.0.0 build
> shx rm -rf dist && tsc -b


/spacecli $ ./bin/run humans
Number of humans currently in space: 7

Pretty straightforward, isn’t it? You now have a simple CLI project, built via the oclif framework, that can instantly tell you the number of people in space.

Enhancing Our Command With Flags and a Nicer UI

Knowing how many people are currently in space is nice, but we can get even more space data! The endpoint we're using provides more details about the spacefarers, including their names and which spacecraft they are on.

We’ll take our command one step further, demonstrating how to use flags and giving our command a nicer UI. We can output our data as a table with the cli-ux package, which has been rolled into @oclif/core (as of version 1.2.0). To ensure we have access to cli-ux, let’s update our packages.

Shell
 
/spacecli $ npm update

We can add an optional --table flag to our humans command to print out this data in a table. We use the CliUx.ux.table() function for this pretty output.

TypeScript
 
import {Command, Flags, CliUx} from '@oclif/core'
import {get} from 'node:http'

export default class HumansCommand extends Command {
  static description = 'Get the number of humans currently in space.'

  static examples = [
    '$ space-cli\nNumber of humans currently in space: 7',
  ]

  static flags = {
    table: Flags.boolean({char: 't', description: 'display who is in space and where with a table'}),
  }

  public async run(): Promise<void> {
    const {flags} = await this.parse(HumansCommand)

    get('http://api.open-notify.org/astros.json', res => {
      res.on('data', d => {
        const details = JSON.parse(d)
        this.log(`Number of humans currently in space: ${details.number}`)
        if (flags.table) {
          CliUx.ux.table(details.people, {name: {}, craft: {}})
        }
      })
    }).on('error', e => {
      this.error(e)
    })
  }
}

In our updated code, our first step was to bring back the flags property. This time we're defining a boolean flag—it's either there, or it isn’t—as opposed to string flags which take a string as an argument. We also define a description and a shorthand -t for the flag in the options object that we're passing in.

Next, we parse the flag in our run method. If it’s present, we display a table with CliUx.ux.table(). The first argument, details.people, is the data we want to display in the table, while the second argument is an object that defines the columns in the table. In this case, we define a name and a craft column, each with an empty object. (There are some configuration options for the table columns, but we don't need any in this case.) Oclif will look for those properties on the data object that we pass in and take care of everything else for us!

We can build and rerun the command with the new table flag to see what that looks like:

Shell
 
/spacecli $ ./bin/run humans --table
Number of humans currently in space: 10
 Name                   Craft    
 ───────────────── ──────── 
 Oleg Artemyev          ISS      
 Denis Matveev          ISS      
 Sergey Korsakov        ISS      
 Kjell Lindgren         ISS      
 Bob Hines              ISS      
 Samantha Cristoforetti ISS      
 Jessica Watkins        ISS      
 Cai Xuzhe              Tiangong 
 Chen Dong              Tiangong 
 Liu Yang               Tiangong 

Beautiful!

Add Some More Functionality to Your Own

At this point, our example project is complete, but you can easily build more on top of it. The Open Notify service provides an API endpoint to get the current location of the International Space Station. You might add that functionality, too, with a command such as space-cli iss to return the location when run.

What About Distribution?

You might be thinking about distribution options for sharing your awesome new CLI. You could publish this project to npm via a simple command. You could create a tarball to distribute the project internally to your team or coworkers. You could also create a Homebrew formula if you wanted to share it with macOS users. Oclif can help you with any of these options.

We started this article by reviewing the history of oclif, along with the many reasons why it should be your first choice when creating a CLI. Some of its advantages include speed, extensibility, and a variety of distribution options. We learned how to scaffold a CLI project and add new commands to it, and built a simple CLI as an example.

Now that you’ve been equipped with the knowledge and a new tool, go out and be dangerous.

Command-line interface Build (game engine) Command (computing) Space (architecture)

Published at DZone with permission of Alvin Lee. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Terraform Best Practices: The 24 Practices You Should Adopt
  • Using Environment Variable With Angular
  • Let’s Build an End-to-End NFT Project Using Truffle Suite
  • Anypoint CLI Commands in MuleSoft

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!