Maciej Walkowiak

How I built vlad-cli - command line interface to Vlad Mihalcea

TLDR;

Vlad Mihalcea's blog is a default place I go to when I struggle with Hibernate or JPA, which lead to the following tweet exchange:

Joke or not joke - building such CLI can be a quick and fun project, so why not?

Searching through the blog

The first question is how to search through the blog. None of the public search engines provide search API, there are some third party options that scrape Google results and return them as JSON - but first - as far as I can tell it violates Google terms of use and it's a paid API. Nothing wrong with paying for API but not for this kind of project.

Fortunately, Vlad's blog is based on Wordpress. Wordpress by default comes with a search API - not sure what is used under the hood and how good are the search results but for sure they are as good as search functionality on Vlad's blog - so we can assume it's good enough.

A blog can be searched with executing curl on search endpoint:

$ curl -s https://vladmihalcea.com/wp-json/wp/v2/search\?search\=how\ to\ map\ json

[
  {
    "id": 5905,
    "title": "How to map JSON objects using generic Hibernate Types",
    "url": "https://vladmihalcea.com/how-to-map-json-objects-using-generic-hibernate-types/",
    "type": "post",
    "subtype": "post",
    "_links": {
      "self": [
        {
          "embeddable": true,
          "href": "https://vladmihalcea.com/wp-json/wp/v2/posts/5905"
        }
      ],
      "about": [
        {
          "href": "https://vladmihalcea.com/wp-json/wp/v2/types/post"
        }
      ],
      "collection": [
        {
          "href": "https://vladmihalcea.com/wp-json/wp/v2/search"
        }
      ]
    }
  },
  ...
]

vlad-cli as a bash script

The initial idea was just to write a very simple bash script that will open the first search result in a browser.

With jq we can easily extract url from json returned by curl:

curl -s https://vladmihalcea.com/wp-json/wp/v2/search\?search\="how to map json" | jq -r ".[0].url?"  

https://vladmihalcea.com/how-to-map-json-objects-using-generic-hibernate-types/

jq is a super handy utility I totally recommend checking. ".[0].url?" translates into: extract url property from the first entry in array and do not fail if it's not present.

Adding -r strips quotes from the result.

The last thing is to pass the result to the command that opens a web browser and put it together in a bash script

#!/bin/sh
str="$*"
open $(curl -s https://vladmihalcea.com/wp-json/wp/v2/search\?search\="$str" | jq -r ".[0].url?")
exit;

str="$*" concatenates all arguments so that we can execute the script like:

$ ./vlad.sh how to map json

This script works, but has the following drawbacks:

  • open command works only on Mac - other OS have different commands that serve the same purpose
  • the first result is not necessarily the best
  • how to distribute bash script as an easily installable package?

I decided to address them by rewriting the script to NodeJS, mainly because most developers are used to installing packages through npm, there is a mindblowing number of available packages to use and it's freaking easy to publish package making it available to anyone to install.

Building clis with Node

Finding reliable CLI framework in Node ecosystem is quite straightforward - oclif - made by Heroku and used by Heroku to build their CLI (which is awesome). It's also used by Twillio. I don't really need more recommendations than that.

First, we need to create project:

npx oclif single vlad-cli

Then there are few usual questions to answer like who is the owner, what is the package name etc and finally if we want to build it with TypeScript or JavaScript.

? npm package name vlad-cli
? command bin name the CLI will export vlad-cli
? description 
? author Maciej Walkowiak @maciejwalkowiak
? version 0.0.0
? license MIT
? Who is the GitHub owner of repository (https://github.com/OWNER/repo) maciejwalkowiak
? What is the GitHub name of repository (https://github.com/owner/REPO) vlad-cli
? Select a package manager yarn
? TypeScript No
? Use eslint (linter for JavaScript and Typescript) No
? Use mocha (testing framework) No

Now we have application skeleton generated, we can already run it with ./bin/run

$ ./bin/run 
hello world from ./src/index.js

Passing the search query to the command

At this stage if we just want to pass search query to the command we will get the following output:

$ ./bin/run how to map json
 β€Ί   Error: Unexpected arguments: how, to, map, json
 β€Ί   See more help with --help

Each word is considered to be a separate argument and by default with oclif you have to explicitly list all available arguments.

We can disable this behavior by setting static strict = false on VladCliCommand. Then we can obtain an array of arguments from the result of parse:

class VladCliCommand extends Command {
  static strict = false

  async run() {
    const {argv} = this.parse(VladCliCommand)
    const query = argv.join(' ')
    // ...
  }
}

Fetching search results

Now we need to fetch search results from Wordpress search API. If you have worked with JavaScript at least a bit you are likely familiar with fetch function. It's not available in Node environment so we need to add a dependency:

$ yarn add node-fetch

... and fetch the results. We can use async/await methods if you don't want to break your neck with promises:

const fetch = require('node-fetch')

class VladCliCommand extends Command {
  static strict = false

  async run() {
    const {argv} = this.parse(VladCliCommand)
    const query = argv.join(' ')

    const searchResponse = await fetch(`https://vladmihalcea.com/wp-json/wp/v2/search?search=${query}`)
    const json = await searchResponse.json()

    // ...
  }
}

Prompting user to choose article

We are not going to open the first available search result, instead, we want to give the user option to choose one from the list.

There is no utility for that built into oclif but there is a fantastic JavaScript library that does it (and much more): Inquirer.js

$ yarn add inquirer
const fetch = require('node-fetch')
const inquirer = require('inquirer')

class VladCliCommand extends Command {
  static strict = false

  async run() {
    const {flags, argv} = this.parse(VladCliCommand)
    const query = argv.join(' ')

    const searchResponse = await fetch(`https://vladmihalcea.com/wp-json/wp/v2/search?search=${query}`)
    const json = await searchResponse.json()

    let response = await inquirer.prompt([{
      name: 'stage',
      message: 'πŸ€” Choose article',
      type: 'rawlist',
      choices: json.map(function (entry) {
        return {name: entry.title}
      })
    }])

    // ...
  }
}

This will give us a list of options that can be chosen with arrows or putting the number:

./bin/run how to map json
? πŸ€” Choose article 
  1) How to map JSON objects using generic Hibernate Types
  2) How to map SQL Server JSON columns using JPA and Hibernate
  3) How to map Oracle JSON columns using JPA and Hibernate
  4) How to map a String JPA property to a JSON column using Hibernate
  5) How to map a JSON collection using JPA and Hibernate
  6) How to map a PostgreSQL ARRAY to a Java List with JPA and Hibernate
(Move up and down to reveal more choices)
  Answer: 

The choice is saved in response.stage property so to get the url of a chosen article we need to filter previously returned json by comparing titles. Not the greatest solution since it would fail if two articles have the same title, but since it's a just-for-fun project and it's unlikely it would ever happen we can just follow this path:

const result = json.filter(entry => entry.title === responses.stage)

Opening article in a browser

As I mentioned - Node ecosystem is crazy wide. There is a library for everything - including cross platform open command: opn.

$ yarn add opn

And just call opn function on the url. That's how we end up with final version of the script:

const fetch = require('node-fetch')
const inquirer = require('inquirer')
const opn = require('opn')

class VladCliCommand extends Command {
  static strict = false

  async run() {
    const {flags, argv} = this.parse(VladCliCommand)
    const query = argv.join(' ')

    const searchResponse = await fetch(`https://vladmihalcea.com/wp-json/wp/v2/search?search=${query}`)
    const json = await searchResponse.json()

    let response = await inquirer.prompt([{
      name: 'stage',
      message: 'πŸ€” Choose article',
      type: 'rawlist',
      choices: json.map(function (entry) {
        return {name: entry.title}
      })
    }])

    const result = json.filter(entry => entry.title === response.stage)

    opn(result[0].url)
  }
}

That's more or less it. I skipped some parts like handling connection errors, empty results etc. but I think you got the point.

Publishing to NPM

In comparison to Maven official repository, it's very easy to publish a NPM package. You just need to sign up at https://www.npmjs.com/ and run npm publish. If you prefer other distribution than NPM, oclif is your friend. You can build installers for Mac, Windows, Ubuntu/Debian packages and more.

Summary

The vlad-cli package is available now to install:

$ npm install -g vlad-cli

It was definitely fun experience, oclif is a fantastic tool and if I only come up with another idea for CLI tool I am definitely going to use it too. The only other tool I can think of I would be keen to use is Quarkus with it's new command mode, but I am not sure how to actually realease quarkus-built-package without putting too much effort into it πŸ˜‰

Full source code is available at https://github.com/maciejwalkowiak/vlad-cli.

The last but not least - highlight of this whole experiment 🎼 "I can't wait to Hibernate" 🎼 song: