Command line apps with Ruby

Notes from Command-Line Apps in Ruby.

$ cd /Users/myname/Documents/command_line_apps_in_ruby_demo

app.rb

require "thor"

class App < Thor
  desc "hello WORD", "Prints 'Hello WORD' to the screen."
  def hello word
    puts "Hello #{word}"
  end

  desc "list_recipes [KEYWORD] [OPTIONS]", "List all recipes. If a keyword is given, it filters the list based off it."
  option :format
  def list_recipes keyword=nil
    # puts options[:format]
    recipes = [
      {
        title: "Ratatouille",
        cooking_time: "60 min",
        ingredients:  %w(potatoes carrots peppers onions zucchini tomatoes)
      },
      {
        title: "Mac & Cheese",
        cooking_time: "20 min",
        ingredients: %w(macarroni cheese mustard milk)
      },
      {
        title: "Caesar Salad",
        cooking_time: "10 min",
        ingredients:  %w(chicken lettuce croutons eggs)
      }
    ]

  recipes_to_be_listed = if keyword.nil? then recipes
                         else recipes.select { |recipe| recipe[:title].downcase.include? keyword.downcase}
                         end

    recipes_to_be_listed.each do | recipe |
      puts "-------------"
      puts "Recipe: #{recipe[:title]}"
      puts "It takes: #{recipe[:cooking_time]} to cook."
      puts "The ingredients are: #{recipe[:ingredients].join(", ")}"
      puts ""
    end
  end      
end

App.start ARGV
# ARGV is for options, arguments, subcommand to be parsed in app.

desc will be used when you type ruby app.rb to describe the method.

To find out a recipes which name includes "oui" and print out format option to test it if it picks up the format or not.

$ ruby app.rb list_recipes oui --format sometext

Adding options and arguments

require "thor"

class App < Thor
  desc "hello WORD", "Prints 'Hello WORD' to the screen."
  def hello word
    puts "Hello #{word}"
  end

  desc "list_recipes [KEYWORD] [OPTIONS]", "List all recipes. If a keyword is given, it filters the list based off it."
  option :format
  option :show_time, type: :boolean, default: true #--show-time --no-show-time
  def list_recipes keyword=nil
    recipes = [
      {
        title: "Ratatouille",
        cooking_time: "60 min",
        ingredients:  %w(potatoes carrots peppers onions zucchini tomatoes)
      },
      {
        title: "Mac & Cheese",
        cooking_time: "20 min",
        ingredients: %w(macarroni cheese mustard milk)
      },
      {
        title: "Caesar Salad",
        cooking_time: "10 min",
        ingredients:  %w(chicken lettuce croutons eggs)
      }
    ]

    recipes_to_be_listed = if keyword.nil? then recipes
                         else recipes.select { |recipe| recipe[:title].downcase.include? keyword.downcase}
                         end

    recipes_to_be_listed.each do | recipe |
      if options[:format].nil?
        print_default recipe
      else options[:format] == "oneline"
        print_oneline recipe
      end

    end
  end      

  private

  def print_default recipe
    puts "-------------"
    puts "Recipe: #{recipe[:title]}"
    puts "It takes: #{recipe[:cooking_time]} to cook."
    puts "The ingredients are: #{recipe[:ingredients].join(", ")}"
    puts ""
  end

  def print_oneline recipe
    if options[:show_time]
      time = "(#{recipe[:cooking_time]})"
    else
      time = ""
    end

    puts %Q{#{recipe[:title]} #{time}}
  end

end

App.start ARGV
# ARGV is for options, arguments, subcommand to be parsed in app.

option :show_time in code and --show-time in command line. Thor is cleaver enough to understand no in --no-show-time. Test this with the followings.

# 1.
$ ruby app.rb list_recipes --format oneline
# or 
$ ruby app.rb list_recipes --format "oneline"

# 2.
$ ruby app.rb list_recipes --format="oneline"

# 3.
$ ruby app.rb list_recipes --format="oneline" --no-show-time

option :show_time, type: :boolean, default: true #--show-time --no-show-time

If there is no default: true, the default is false. Thor is clever enough to recognize --show-time as true and --no-show-time as false.

In option you use "-" like --show-time, but in method use under-bar, options[:show_time].

If you add required true, like option: format, required: true, then you are required the option.

Subcommands

Adding recipes add method

Examples are "git remote add", "git remote list", etc.

Every Thor application can be hooked inside another application.

subcommand <name of command>, <name of class to hook>. This can be used as app.rb recipes add. The add options will be defined as Thor options, not arguments. Arguments are explicit in the form of options. Rather than app.rb recipes add title time description, it is better to be app.rb recipes add --title="" --time="" --description=""

app2.rb

require "thor"

RECIPES = [
      {
        title: "Ratatouille",
        cooking_time: "60 min",
        ingredients:  %w(potatoes carrots peppers onions zucchini tomatoes)
      },
      {
        title: "Mac & Cheese",
        cooking_time: "20 min",
        ingredients: %w(macarroni cheese mustard milk)
      },
      {
        title: "Caesar Salad",
        cooking_time: "10 min",
        ingredients:  %w(chicken lettuce croutons eggs)
      }
    ]

class Recipes < Thor
  desc "add --title --cooking-time --description", "Adds a new recipe."
  option :title, required: true
  option :cooking_time, required: true
  option :description, required: true

  def add # app.rb recipes add --title="" --cooking-time="" --description=""
    recipe = {
      title: options[:title],
      cooking_time: options[:cooking_time],
      description: options[:description]
    }

    RECIPES << recipe

    RECIPES.each do |recipe|
      puts recipe[:title]
    end
  end
end

class App < Thor
  desc "hello WORD", "Prints 'Hello WORD' to the screen."
  def hello word
    puts "Hello #{word}"
  end

  desc "recipes", "Manages recipes"
  subcommand "recipes", Recipes # app.rb recipes add 

  desc "list_recipes [KEYWORD] [OPTIONS]", "List all recipes. If a keyword is given, it filters the list based off it."
  option :format
  option :show_time, type: :boolean, default: true #--show-time --no-show-time
  def list_recipes keyword=nil
    recipes = RECIPES
    recipes_to_be_listed = if keyword.nil? then recipes
                         else recipes.select { |recipe| recipe[:title].downcase.include? keyword.downcase}
                         end

    recipes_to_be_listed.each do | recipe |
      if options[:format].nil?
        print_default recipe
      else options[:format] == "oneline"
        print_oneline recipe
      end

    end
  end   

  private

  def print_default recipe
    puts "-------------"
    puts "Recipe: #{recipe[:title]}"
    puts "It takes: #{recipe[:cooking_time]} to cook."
    puts "The ingredients are: #{recipe[:ingredients].join(", ")}"
    puts ""
  end

  def print_oneline recipe
    if options[:show_time]
      time = "(#{recipe[:cooking_time]})"
    else 
      time = ""
    end

    puts %Q{#{recipe[:title]} #{time}}
  end

end

App.start ARGV
# ARGV is for options, arguments, subcommand to be parsed in app.

Test it.

# 1. 
$ ruby app2.rb recipes add # this will give an error

# 2. 
$ ruby app2.rb recipes add --title="Steak" --cooking-time="10 min" --description="Good ol' steak"

Adding recipes list

app3.rb

require "thor"

RECIPES = [
      {
        title: "Ratatouille",
        cooking_time: "60 min",
        ingredients:  %w(potatoes carrots peppers onions zucchini tomatoes)
      },
      {
        title: "Mac & Cheese",
        cooking_time: "20 min",
        ingredients: %w(macarroni cheese mustard milk)
      },
      {
        title: "Caesar Salad",
        cooking_time: "10 min",
        ingredients:  %w(chicken lettuce croutons eggs)
      }
    ]

class Recipes < Thor
  desc "add --title --cooking-time --description", "Adds a new recipe."
  option :title, required: true
  option :cooking_time, required: true
  option :description, required: true

  def add # app.rb recipes add --title="" --cooking-time="" --description=""
    recipe = {
      title: options[:title],
      cooking_time: options[:cooking_time],
      description: options[:description]
    }

    RECIPES << recipe

    RECIPES.each do |recipe|
      puts recipe[:title]
    end
  end

  desc "list [KEYWORD] [OPTIONS]", "List all recipes. If a keyword is given, it filters the list based off it."
  option :format
  option :show_time, type: :boolean, default: true #--show-time --no-show-time

  def list keyword=nil
    recipes = RECIPES
    recipes_to_be_listed = if keyword.nil? then recipes
                         else recipes.select { |recipe| recipe[:title].downcase.include? keyword.downcase}
                         end

    recipes_to_be_listed.each do | recipe |
      if options[:format].nil?
        print_default recipe
      else options[:format] == "oneline"
        print_oneline recipe
      end

    end
  end   

  private

  def print_default recipe
    puts "-------------"
    puts "Recipe: #{recipe[:title]}"
    puts "It takes: #{recipe[:cooking_time]} to cook."
    puts "The ingredients are: #{recipe[:ingredients].join(", ")}"
    puts ""
  end

  def print_oneline recipe
    if options[:show_time]
      time = "(#{recipe[:cooking_time]})"
    else 
      time = ""
    end

    puts %Q{#{recipe[:title]} #{time}}
  end
end

class App < Thor
  desc "hello WORD", "Prints 'Hello WORD' to the screen."
  def hello word
    puts "Hello #{word}"
  end

  desc "recipes", "Manages recipes"
  subcommand "recipes", Recipes # app.rb recipes add 


end

App.start ARGV
# ARGV is for options, arguments, subcommand to be parsed in app.

Test it.

# 1. 
$ ruby app3.rb recipes list

# 2. This will show help message for recipes.
$ ruby app3.rb recipes

Thor will automatically insert options, so change, desc "add --title --cooking-time --description", "Adds a new recipe." to desc "add", "Adds a new recipe."

Adding alias for options. This allow to type -t rather than --title etc. You can use only one letter after -, like, -t, -c or -d. - is mandatory.

option :title, required: true, aliases: "-t"
  option :cooking_time, required: true, aliases: "-c"
  option :description, required: true, aliases: "-d"

Test it.

# 1. To display help
$ ruby app4.rb recipes

# 2.
$ ruby app4.rb recipes add -c="10 min" -t "Steak" -d "Good ol' steak."

GLI

Aims for a higher standard. Better documentation, better testing, better dependency management.

Install gem gli

$ gem install gli
# Initialize with name of app and methods you want.
$ gli init recipes list add

switch in GLI is the same as boolean flag in Thor.

In bin/recipes.rb

# In Thor
option :s, type :boolean
# In GLI
switch [:s,:switch]

# flag in GLI is the same as option in Thor without using boolean type.
Written on October 27, 2013