Building a Command Line Tool as a Ruby Gem, Part I

With the volume of work that software engineers do at the command line, the writing of command line tools to simplify and DRY up labor is an essential part of the development cycle. From the simple (adding alias ll='ls -laG' to your .bashrc) to the obtuse (alias searchCode='find -iname \*.rb | xargs grep $1') to the incomprehensible…

while (<>) {
    s/ \e[ #%()*+\-.\/]. |
       \r |
       (?:\e\[|\x9b) [ -?]* [@-~] |
       (?:\e\]|\x9d) .*? (?:\e\\|[\a\x9c]) |
       (?:\e[P^_]|[\x90\x9e\x9f]) .*? (?:\e\\|\x9c) |
       \e.|[\x80-\x9f] //xg;
       1 while s/[^\b][\b]//g;

…the modern developer would do well to build out their command line toolbelt.

Anyone who has struggled with proper tabbing in a Makefile or the correct spacing in defining a bash variable can attest to the difficulty (and learning curve) in writing command line tools using shell script. Fortunately, modern high-level programming languages like Python and Ruby provide structure to quickly and painlessly (really!) build command line tools. As Invoca is a Ruby shop, I’ll focus on Ruby.

Command line tools in Ruby are at the core of the work we do. A quick perusal of ~/.rbenv/shims reveals familiar names like bundle, gem, knife, rails, and rake. Each of these tools can be invoked from the command line, is implemented using the Ruby programming language, and typically eschews shell script altogether.

I recently used Ruby to build a command line sip client. The tool is a wrapper to another command line sip client, pjsua.

Now, why would I write a wrapper around an existing command line tool? Well, consider these two commands issued to perform precisely the same action (call a number on a sip server, wait 5 seconds, send a DTMF of 1, wait 5 seconds, hang up):

$ (sleep 5; echo "# 1"; sleep 5; echo h) | pjsua --log-level=0 --use-cli --id --playback-dev=2

$ invoke_call sip --promo-number=8555550053 --call-scenario="wait 5, press 1, wait 5" --client-number=8055551212

While the first slouches toward obfuscation (is that a bash subprocess being piped into pjsua?), and which of its flags are essential is confusing, the second is hardly concise either. Fortunately, simply typing the basic command clarifies usage:

=> ~/ invoke_call
  invoke_call help [COMMAND] # Describe available commands or one specific command
  invoke_call sip --call-scenario=CALL_SCENARIO --promo-number=PROMO_NUMBER --ringswitch-node=RINGSWITCH_NODE  # place a SIP call

Building out this tool in Ruby takes a complex, nested command that throws unclear errors when options are not passed correctly, and turns it into a straightforward command line tool with clear usage documentation and reasonable error handling.

The beauty of Ruby is that this is quick and easy to do using the extensive toolkit built by the Ruby community. I used bundle to instantiate my new gem’s directory structure and to build the final package to be installed by gem. I had my Cli class inherit from the thor gem and its extensive shortcuts for building command line tools.

Over the next few posts we’ll explore these Ruby tools, specifically rbenv, bundler, and thor, and walk step by step through building a simple command line tool using Ruby.


If you want something pithy, do what I did:

function callmy {
  invoke_call sip --call-scenario="$2" --promo-number=$1 --ringswitch-node=$MY_COLLAPSED_IP

Then try

$ callmy 8555551212 “wait 5, press 1, wait 5"

Leave a Reply

Your email address will not be published. Required fields are marked *

two × 9 =