Using Thor and Ruby to build a CLI Sep 10 2018

Thor is a toolkit that can help us build command line interfaces(CLIs). You can find many tutorials on how to build a basic CLI using Thor. I want to explain the default behaviour of Thor and also when to use env to define the binary that will run your script.

We normally build custom scripts to automate tasks on our servers or local environments. Often these scripts require many flags or a big list of parameters. If the number of flags and parameters (from now on, options) are small then we won’t have a problem building the logic to handle them. However, when there are a number of options or we want to handle aliases, e.g. accepting the flag --delete or -d as the same operation, then things can become harder and more tedious to maintain.

Table of Contents

Single file scripts

If you have a script with only a few options, you can keep everything in one file and run it as a Thor script. Let’s look at a script that displays a list of files in a directory sorted by the amount of disk space they occupy. This example illustrates the basic usage of Thor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env ruby
require "thor"

class DiskSpaceCLI < Thor
  desc "show_stats", "Displays disk space stats"
  method_option :path, aliases: "-p", default: "./"
  method_option :count, aliases: "-c", default: "-1"
  def show_stats
    output = `du -s #{options[:path]}* `
    files = output.split("\n")
    files.sort! do |a,b|
      a_size = a.split("\t").first
      b_size = b.split("\t").first
      a_size.to_i <=> b_size.to_i
    end
    count = options[:count].to_i
    if count != -1 && count < files.size
      files = files[0..(count-1)]
    end
    puts files
  end 

end

DiskSpaceCLI.start(ARGV)

Let’s look at the first line:

1
#!/usr/bin/env ruby

Of course we can use a specific ruby binary, for example, /usr/bin/ruby. However, if we want to use the ruby command defined on the user $PATH then we should use the env command like the example script above. Or, if we use a version manager for ruby (like rbenv or rvm) and we would like the script to always run the version selected by our version manager then we should use the env command.

Now we add execution permissions to our script (chmod u+x [SCRIPT NAME]) and test it.

If we run the script without any parameters, we would get the default behavior which is a list of options that the script supports. Thor, by default, calls the help method. We can test this by over-writing the help method:

1
2
3
def help
  puts "I'm the default method"
end

Now if you run the script without any parameters you’ll see our message displayed on the screen. If we want to add an additional message or functionality but we still want to display the previous help method then we should add a call to super. For example,

1
2
3
4
def help
  puts "Below you’ll see the options supported by the script"
  super
end

This will display our message followed by the original help display.

Currently, if we want to run our show_stats method we have to call the script and pass the name of our method, for example, to show the stats of the files in the current directory and only show the top 3 files we would run the following command:

1
./script_name show_stats --path=./ --count=3

That looks a little bit awkward, especially if it only has one method. We want to call our script in the following way:

1
./script_name --path=./ --count=3

That is, without show_stats. In other words, we would like the script to have show_stats as the default method. How do we do that? To do that, we use the default_task macro. This allows us to set the task that will run by default. It will look something like this:

1
default_task :show_stats

Now we can run the script without having to explicitly tell it to run show_stats method. Our script will look like the following snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/env ruby
require "thor"

class DiskSpaceCLI < Thor
  desc "show_stats", "Displays disk space stats"
  method_option :path, aliases: "-p", default: "./"
  method_option :count, aliases: "-c", default: "-1"
  def show_stats
    output = `du -s #{options[:path]}* `
    files = output.split("\n")
    files.sort! do |a,b|
      a_size = a.split("\t").first
      b_size = b.split("\t").first
      a_size.to_i <=> b_size.to_i
    end
    count = options[:count].to_i
    if count != -1 && count < files.size
      files = files[0..(count-1)]
    end
    puts files
  end 

  def help
    puts "Below you'll see the options supported by the script"
    super
  end

   default_task :show_stats
end

DiskSpaceCLI.start(ARGV)

Test it and let me know what you think. I think this post covers enough to get you started but if you would like to know more, send me a message and I will add more posts on how to use Thor and Ruby to build CLIs (for example, how to test Thor scripts using RSpec or how to package a Ruby program in a gem).


** If you want to check what else I'm currently doing, be sure to follow me on twitter @rderik or subscribe to the newsletter. If you want to send me a direct message, you can send it to derik@rderik.com.