Command line application with Ruby
The goal of this article is to show you how you can create your command line application using the Ruby programming language and make it available via Homebrew, so anyone can install it quickly and use in seconds.
The article consists of two main parts: building the command line application and making it available via Homebrew.
Building command-line application
Our goal is to build a get_joke
application that will render a random joke about Chuck Norris. The command will also be able to parse the following arguments:
random
- random jokes that should be displayed. By default, one joke is shown, but if you are prepared to read more, you can pass the-r
or--random
argument with the number of jokes you would like to seefirst_name
- the first name you would like to see instead of Chuck. It should be possible to pass the custom first name with-f
or--first-name
argumentlast_name
- the last name you would like to see instead of Norris. It should be possible to pass the custom last name with-l
or--last-name
argumenthelp
- the help command that displays the list of available commands along with the short description. The command is rendered when the-h
or--help
argument is passed. The command should not display the joke when this argument is passed.
As the data source, we will use the http://api.icndb.com/jokes/random
endpoint, which provides a nice and simple API for getting jokes about Chuck.
The program will consist of three parts:
- Options parsing - we have to parse options passed to the
get_joke
command. We will achieve it with theOptionParser
class, which is a part of the Ruby standard library - Request URL creation - depending on the arguments passed, we have to build a URL that we will call to get the joke or jokes
- Request response parsing - once we will perform the request, we have to take care of parsing the response and displaying jokes in the console
We won't be using any custom Ruby gems, only code available out of the box with the Ruby.
Creating executable file
The very first step is to create an executable file. Create the project directory:
mkdir get_joke
cd get_joke/
and our executable file along with the proper permissions:
touch bin/get_joke
chmod +x bin/get_joke
Creating the program
We have to add the following shebang line to our script:
#!/usr/bin/env ruby
This line tells the shell what interpreter should be used to process the rest of the file. It's a preferred approach to the hardcoded interpreter's path to Ruby as it's more portable. We can now execute our program, and we shouldn't see any errors or output:
./bin/get_joke
Parsing arguments passed to the command
As I mentioned before, we will use the OptionParser
class, a standard Ruby class for parsing command-line arguments. You can check the documentation https://ruby-doc.org/stdlib-2.7.0/libdoc/optparse/rdoc/OptionParser.html
Let's add support for the first argument - random
:
#!/usr/bin/env ruby
require 'optparse'
options = {}
parser = OptionParser.new do |parser|
parser.on("-r", "--random RANDOM_JOKES_COUNT", "Render n random jokes")
end
parser.parse!(into: options)
puts options.inspect
As you can see, the on
method from OptionParser
takes three arguments:
- shortcut argument name, which is
-r
in our case - full argument name which is
--random
in our case - the description for the argument
You can now execute the command with -r
argument to see how it's parsed:
./bin/get_joke -r 4
# => { random: 4 }
We can add --first-name
and --last-name
arguments the same way:
#!/usr/bin/env ruby
require 'optparse'
options = {}
parser = OptionParser.new do |parser|
parser.on("-f", "--first-name FIRST_NAME", "Replacement for Chuck's first name")
parser.on("-l", "--last-name LAST_NAME", "Replacement for Chuck's last name")
parser.on("-r", "--random RANDOM_JOKES_COUNT", "Render n random jokes")
end
parser.parse!(into: options)
puts options.inspect
and execute our command to see how arguments are parsed:
./bin/get_joke --random 4 --first-name John -l Doe
# => { random: 4, :'first-name' => 'John', :'last-name' => 'Doe' }
The last step is to add support for the --help
arugment:
parser = OptionParser.new do |parser|
parser.on("-f", "--first-name FIRST_NAME", "Replacement for Chuck's first name")
parser.on("-l", "--last-name LAST_NAME", "Replacement for Chuck's last name")
parser.on("-r", "--random RANDOM_JOKES_COUNT", "Render n random jokes")
parser.on("-h", "--help", "Prints this help") do
puts parser
exit
end
end
The command will print a nice help output each time -h
or --help
argument will be passed:
./bin/get_joke --help
# Usage: get_joke [options]
# -f, --first-name FIRST_NAME Replacement for Chuck's first name
# -l, --last-name LAST_NAME Replacement for Chuck's last name
# -r, --random RANDOM_JOKES_COUNT Render n random jokes
# -h, --help Prints this help
The arguments parsing part is done, so we can now begin preparing the API URL to reflect the argument's values passed to our command.
Preparing API URL with values passed as arguments
As I mentioned before, we will use http://api.icndb.com/jokes/random
endpoint, which provides the jokes about Chuck Norris. The following combination of the API URL is available:
- Multiple random jokes -
https://api.icndb.com/jokes/random/4
- it will provide four random jokes about Chuck Norris - Jokes with the replaced first name -
http://api.icndb.com/jokes/random?firstName=name
- the value fromfirstName
will replace "Chuck" string in jokes - Jokes with replaced last name -
http://api.icndb.com/jokes/random?lastName=name
- the value fromlastName
param will replace "Norris" string in jokes
The above variants can be combined to request five random jokes where Chuck Norris's name will be replaced with John Doe.
Here is the part of the code responsible for preparing the proper request URL:
require 'uri'
require 'rack'
base_url = "http://api.icndb.com/jokes/random"
base_url += "/#{options.fetch(:random)}" if options.key?(:random)
uri = URI(base_url)
query = {
'firstName' => options[:'first-name'],
'lastName' => options[:'last-name']
}.delete_if { |key, value| value.nil? }
uri.query = Rack::Utils.build_query(query) unless query.empty?
puts uri.to_s
The part consists of two steps:
- Building the base URL - when the
random
argument is not passed, one random joke is returned by the default. When the argument is passed, we add it to the end of the path - Building query - we build a simple query and reject params where the value is not given. With a little help of
Rack::Utils
class, we created a query part of the URL
We can test it to see how it's working:
./bin/get_joke
# => http://api.icndb.com/jokes/random
./bin/get_joke -r 4
# => http://api.icndb.com/jokes/random/4
./bin/get_joke --random 4 --first-name John -l Doe
# => http://api.icndb.com/jokes/random/4?firstName=John&lastName=Doe
The second part is done so we can perform the request to the API and parse the response to render jokes in the console.
Parsing API response and rendering jokes
The goal is to use only code that is available out of the box in Ruby, so to perform the request, we will use the Net::HTTP
class and get_response
method. The response is in JSON
format, so we have to parse it before doing anything else:
require 'net/http'
require 'json'
response = Net::HTTP.get_response(uri).body
parsed_response = JSON.load(response)
If the type
attribute from the response equals success
, we can render jokes. Otherwise, something went wrong, and it won't be funny. We should have in mind that the format of the response is different depending on the number of jokes we request.
When one joke is requested, the joke is available as a hash. When multiple jokes are requested, the response contains an array of jokes:
require 'net/http'
require 'json'
response = Net::HTTP.get_response(uri).body
parsed_response = JSON.load(response)
if parsed_response['type'] == 'success'
value = parsed_response['value']
value = [value] if value.is_a?(Hash)
value.each_with_index do |joke, index|
puts ''
puts "Joke ##{index + 1}: #{joke['joke']}"
end
puts ''
else
puts 'Something went wrong, please try again'
end
We can now put all parts into one and test it:
#!/usr/bin/env ruby
require 'optparse'
require 'net/http'
require 'uri'
require 'json'
require 'rack'
options = {}
parser = OptionParser.new do |parser|
parser.on("-f", "--first-name FIRST_NAME", "Replacement for Chuck's first name")
parser.on("-l", "--last-name LAST_NAME", "Replacement for Chuck's last name")
parser.on("-r", "--random RANDOM_JOKES_COUNT", "Render n random jokes")
parser.on("-h", "--help", "Prints this help") do
puts parser
exit
end
end
parser.parse!(into: options)
base_url = "http://api.icndb.com/jokes/random"
base_url += "/#{options.fetch(:random)}" if options.key?(:random)
uri = URI(base_url)
query = {
'firstName' => options[:'first-name'],
'lastName' => options[:'last-name']
}.delete_if { |key, value| value.nil? }
uri.query = Rack::Utils.build_query(query) unless query.empty?
response = Net::HTTP.get_response(uri).body
parsed_response = JSON.load(response)
if parsed_response['type'] == 'success'
value = parsed_response['value']
value = [value] if value.is_a?(Hash)
value.each_with_index do |joke, index|
puts ''
puts "Joke ##{index + 1}: #{joke['joke']}"
end
puts ''
else
puts 'Something went wrong, please try again'
end
Let's have some fun:
./bin/get_joke
# => Joke #1: Chuck Norris originally wrote the first dictionary. The definition for each word is as follows - A swift roundhouse kick to the face.
let's have some more fun with a custom name:
./bin/get_joke -r 2 -f John
# => Joke #1: There is no theory of evolution, just a list of creatures John Norris allows to live.
# =>
# => Joke #2: When John Norris calls 1-900 numbers, he doesn't get charged. He holds up the phone and money falls out.
Our little program is now ready so that we can show it to the world.
Making the program available via Homebrew
Homebrew is a free and open-source software package management system for macOS and Linux. Thanks to Github's extensive use, we can easily publish our packages and make them available for other users.
Creating a Github repository
As I mentioned above, it's easier to publish our package when our code is hosted on Github. In this step, we will prepare the repository, which will be used by Homebrew to install packages in the system.
Prefix your repository with homebrew-
so it can be easily added to the Homebrew. In my case, my username is pdabrowski6
, and I added a repository called homebrew-get_joke
, so the URL is the following: https://github.com/pdabrowski6/homebrew-get_joke
Add your repository and note the username and repository name as we would need them in the next step.
Adding Homebrew formula
Quote: Homebrew Formulae is an online package browser for Homebrew – the macOS (and Linux) package manager.
To add a new formula, add the Formula
directory and create the get_joke.rb
file inside:
mkdir Formula
touch Formula/get_joke.rb
The next step is to create a straightforward formula definition. In our case, it consists of the following elements:
- Description
- Homepage - URL address to the homepage of the package
- Version - version number so Homebrew knows when to update the package after installation
- URL - URL to the zipped repository that contains source code
install
method - the installation process of our package
Taking into account the above elements, the complete formula definition can look as follows:
class GetJoke < Formula
desc "render a random joke about Chuck Norris"
homepage "https://github.com/github_username/github_repository"
version "0.1"
url "https://github.com/github_username/github_repository/archive/main.zip", :using => :curl
def install
bin.install "bin/get_joke"
end
end
You can now push all files to the Github repository you created in the previous step.
Installing the package
To let know Homebrew that we would like to use formulas from our package, we have to use the tap
command:
brew tap pdabrowski6/get_joke
As you can see, I used my GitHub username and the repository name , but without the homebrew prefix - thanks to the prefix, the tool knows that it's a package.
You can now install the package the same way you install other official packages:
brew install get_joke
The package was installed in your system; you can now use it:
get_joke
# => Joke #1: Some people ask for a Kleenex when they sneeze, Chuck Norris asks for a body bag.