How to write a command-line tool in Ruby

Tips,ruby 2 一月 2011 | View Comments

This is a guest blog posted on rubylearning.com

Introduction

Ruby, as a dynamic language, is always used for quick processing command-line tool for its simplicity and productivity.

This article talks about three ways to write a command-line tool.

Before we start, there are a few things you need to know:

  1. Put line #!/usr/bin/env ruby into the first line of your command-line file which will tell shell execute your file use Ruby
  2. Make sure your file is executable, run chmod u+x FILE_PATH
  3. Print help text if user use it in wrong way

Other people will not sure how to execute your command-line tool.

Conventions

I’ll use three definitions:

  1. Command-line file name
  2. Command
  3. Option

For example there is a command: ‘server start -e development’

  1. Command-line file name is ‘server’
  2. Command is the first argument ‘start’
  3. Option is the reset of argument pair ‘-e development’

Let’s go

We start from a simple example: write a command-line tool to start and stop the server.

Without any lib

case ARGV[0]
when "start"
  STDOUT.puts "called start"
when "start"
  STDOUT.puts "called stop"
else
  STDOUT.puts <<-EOF
Please provide command name

Usage:
  server start
  server stop
EOF
end

ARGV, all arguments will stored as a array in this variable.

What if you need to pass some option?

def parse_options
  options = {}
  case ARGV[1]
  when "-e"
    options[:e] = ARGV[2]
  when "-d"
    options[:d] = ARGV[2]
  end
  options
end

case ARGV[0]
when "start"
  STDOUT.puts "start on #{parse_options.inspect}"
when "stop"
  STDOUT.puts "stop on #{parse_options.inspect}"
else
  STDOUT.puts <<-EOF
Please provide command name

Usage:
  server start
  server stop
 
  options:
    -e ENVIRONMENT. Default: development
    -d daemon, true or false. Default: true
EOF
end

This code is simple but it has some disadvantages:

  • Writing option parser and help text in different places will bring you troubles when they are not matched.
  • Using array index to get options from ARGV, these magic numbers will create maintenance problem.

OptionParser

OptionParser is build-in ruby lib help you parse arguments.

we can refactor our code like this:

require 'optparse'

options = {}

opt_parser = OptionParser.new do |opt|
  opt.banner = "Usage: opt_parser COMMAND [OPTIONS]"
  opt.separator  ""
  opt.separator  "Commands"
  opt.separator  "     start: start server"
  opt.separator  "     stop: stop server"
  opt.separator  ""
  opt.separator  "Options"
 
  opt.on("-e","--environment ENVIRONMENT",
"Which environment you want server run. Default: development") do |environment|
    options[:environment] = environment
  end
 
  opt.on("-d","--daemon","running on daemon mode? Default: true") do
    options[:daemon] = true
  end
 
  opt.on("-h","--help","help") do
    puts opt_parser
  end
end

opt_parser.parse!

case ARGV[0]
when "start"
  puts "call start on options #{options.inspect}"
when "stop"
  puts "call stop on options #{options.inspect}"
else
  puts opt_parser
end

Try to execute this file without argument, you’ll find it prints very nice help text.

opt_parser.parse! is the method extract options from ARGV, extracted value will be deleted from ARGV.

OptionParser is better than that.

You can define options value type, then OptionParser will convert value to the type you defined like this:

opt.on("-e","--environment ENVIRONMENT",Numeric,
       "which environment you want server run") do |environment|
  options[:environment] = environment
       end
opt.on("--delay N", Float, "Delay N seconds before executing") do |n|
  options[:delay] = n
end
opt.on("-j x,y,z","--jurisdictions x,y,z", Array,
       "which jurisdiction will start") do |jurisdictions|
  options[:jurisdictions] = jurisdictions
       end
server_list = %w[a b c]
opt.on("-s SERVERS","--servers SERVERS", server_list,
       "which server will start between #{server_list.join(',')}") do |servers|
  options[:servers] = servers
       end

You can mark whether value of the option is mandatory.

# Mandatory argument.
opts.on("-r", "--require LIBRARY",
        "Require the LIBRARY before executing your script") do |lib|
  options.library << lib
        end

# Optional argument; multi-line description.
opts.on("-i", "--inplace [EXTENSION]",
        "Edit ARGV files in place",
        "  (make backup if EXTENSION supplied)") do |ext|
  options.inplace = true
  options.extension = ext || ''
  options.extension.sub!(/\A\.?(?=.)/, ".")  # Ensure extension begins with dot.
        end

For more details your can see this article and ruby rdoc

Benefit of OptionParser is: we don’t need to use array index to retrieve options and we write help text along with option definition.

Disadvantage of OptionParser is: since different commands need using the same option parser, you cannot define different option parsers for different commands. To solve this problem, you can resort to Thor.

Thor

As you know Thor is a replacement of Rake. Let’s see how we use Thor to refactor our command-line tool.

require 'rubygems'
require 'thor'

class ThorExample < Thor
  desc "start", "start server"
  method_option :environment,:default => "development", :aliases => "-e",
:desc => "which environment you want server run."
  method_option :daemon, :type => :boolean, :default => false, :aliases => "-d",
:desc => "running on daemon mode?"
  def start
    puts "start #{options.inspect}"
  end

  desc "stop" ,"stop server"
  method_option :delay,  :default => 0, :aliases => "-w",
:desc => "wait server finish it's job"
  def stop
    puts "stop"
  end
end

ThorExample.start
  • desc defines command name and long description
  • method_option define option parser for this command
  • ThorExample.start is method to start parse argument

Execute it without argument, the output is:

Tasks:
  thor_example help [TASK]  # Describe available tasks or one specific task
  thor_example start        # start server
  thor_example stop         # stop server

Execute it with argument help start, you’ll get help text for command start:

Usage:
  thor_example start

Options:
  -e, [--environment=ENVIRONMENT]  # which environment you want server run.
                                   # Default: development
  -d, [--daemon]                   # running on daemon mode?

start server

As you can see, it’s very clean and easy to write.

For more detailed usage, you can visit Thor github page and its rdoc

Summary

Of course there are more ways to write command-line tool. Choose what best fit your requirement rather than the most powerful or latest one.

All the sample code is on github https://github.com/allenwei/ruby_command_line_sample

Tagged in , , ,

Tips – How to Get Ruby Bin Path using RbConfig::CONFIG

RubyOnRails 18 十二月 2010 | View Comments

ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
#=> /Users/allen/.rvm/rubies/ree-1.8.7-2010.02/bin/ruby
You can see, there are lot’s of useful info in Constant RbConfig::CONFIG

Tagged in , ,

Tips – Freeze your Constant

RubyOnRails,Tips 15 十二月 2010 | View Comments

Today we met a strong problem. One of our constant is changed out of my expectation.

In my mind, constant is the variable we can not change, but actually we are half right.

See code bellow:

A_CONSTANT = "value"

# this is correct we can not set new value to a constant
A_CONSTANT = "another value" #warning: already initialized constant A_CONSTANT

#But

A_CONSTANT.insert(0, "hack") #=>"hackanother value"

A_HASH = {:key => "value"}

A_HASH[:another_key] = "another_key"

A_HASH #=> {:key => "value", :another_key => "another_key"}

You may say, I won’t change constant like that. But what if you assign a new variable to a constant, like:

A_HASH = {:key => "value"}
a = A_HASH
#... several lines of code


# you may forgot a is a constant
a[:another_key] = "another_key"

I’m sure not everyone knows we can change constant “internally”.

So for sure, we can freeze our constant

A_HASH = {:key => "value"}.freeze
a = A_HASH
#... several lines of code


# you may forgot a is a constant
a[:another_key] = "another_value" # TypeError: can't modify frozen Hash


Tagged in , ,

Faster JSON Parser using yajl-ruby

RubyOnRails 17 十一月 2010 | View Comments

There are there JSON library in ruby json, json_pure and yajl-ruby

what’s the different between these tree

json is C binding of original C implement

json_pure is pure ruby implement, this is for non-MRI ruby like JRuby/Maglev

yajl-ruby is C binding for yajl

yajl stands for yet another json library

How is the speed

yail > json > json_pure

here is the benchmark from yajl source.

According to my test yajl is ~3x faster than json

what’s the feature of yajl

JSON parsing and encoding directly to and from an IO stream (file, socket, etc) or String. Compressed stream parsing and encoding supported for Bzip2, Gzip and Deflate.
Parse and encode multiple JSON objects to and from streams or strings continuously.
JSON gem compatibility API - allows yajl-ruby to be used as a drop-in replacement for the JSON gem
Basic HTTP client (only GET requests supported for now) which parses JSON directly off the response body as itâs being received
~3.5x faster than JSON.generate
~1.9x faster than JSON.parse
~4.5x faster than YAML.load
~377.5x faster than YAML.dump
~1.5x faster than Marshal.load
~2x faster than Marshal.dump

How to use

There already bunch of example on github pages, but doesn’t have example about parse a file with more than one json object. The difference is use block.

require 'yajl'
json = File.new('test.json', 'r') # file with more than one json
parser = Yajl::Parser.new
parser.parse(json) do |hash|
  puts hash.inspect
end

Tagged in , ,

RVM new feature auto-switch gemset – project level gemset

RubyOnRails,Tips 15 八月 2010 | View Comments

Today I noticed RVM have project level gemset

It’s easy to use. Just put a .rvmrc into the folder you want to switch ruby or gemset

Like to we have a rails3 project we can put a .rvmrc into project folder. insert following content in.

rvm 1.8.7@rails3

It’s just like we run rvm use 1.9.7@rails3

The detail usage you can get from RVM Website

Tagged in , ,

Tips – How Many Argument a Method Has

RubyOnRails 10 六月 2010 | View Comments

Find a method in arity

Here is doc from ruby doc

Returns an indication of the number of arguments accepted by a method. Returns a nonnegative integer for methods that take a fixed number of arguments. For Ruby methods that take a variable number of arguments, returns -n-1, where n is the number of required arguments. For methods written in C, returns -1 if the call takes a variable number of arguments.

  class C
     def one;    end
     def two(a); end
     def three(*a);  end
     def four(a, b); end
     def five(a, b, *c);    end
     def six(a, b, *c, &d); end
   end
   c = C.new
   c.method(:one).arity     #=> 0
   c.method(:two).arity     #=> 1
   c.method(:three).arity   #=> -1
   c.method(:four).arity    #=> 2
   c.method(:five).arity    #=> -3
   c.method(:six).arity     #=> -3

   "cat".method(:size).arity      #=> 0
   "cat".method(:replace).arity   #=> 1
   "cat".method(:squeeze).arity   #=> -1
   "cat".method(:count).arity     #=> -1

Tagged in ,

Tips – Set Default Value for A Hash

RubyOnRails,Tips 10 六月 2010 | View Comments

Can hash have a default value, when it doesn’t have a key?

Answer is yes!

h = Hash.new("default")
h["wrong_value"] # => "default"

Tagged in , ,

Manage Your Gem Using RVM Gemset

RubyOnRails,Tips 5 六月 2010 | View Comments

I think you already heard of RVM, a great tool for ruby version management.

What I like most is Gemset, It allow you manage you gems in separate namespace.

First, Install RVM. The official website have great document

Then Install ruby, I’ll install ruby 1.8.7

rvm install 1.8.7

After you installed ruby, you will have default gemset and global gemset
List all you gemset

rvm gemset list

Create new gemset

rvm gemset create new_gemset

Use your newly created gemset

rvm gemset use new_gemset

You can also set your newly created gemset as default

rvm use 1.8.7@new_gemset --default

Here you may ask, do I need install all basic gem like ruby-debug for every gemset?

RVM has already solved your problem.

First, switch to your global gemset.

rvm use 1.8.7@global

Then Install gems which you think it is default gem

Then, you can switch back to your gemset.

You may think, I may installed some gems twice, here you can uninstall the gem you have already installed in global gemset.

Tagged in , ,

Tips – Wrap a method which might be overrided by subclass

RubyOnRails,Tips 14 四月 2010 | View Comments

Problem

As you may know you can easily using alias_method_chain to wrap a method, but somethings it won’t work if your subclass override this method.

Aussuming you have class A, and B

require 'rubygems'
require 'active_support'

class A
  def hello
    puts "hello"
  end
 
  def hello_with_from
    hello_without_from
    p self.class.name
  end
  alias_method_chain :hello, :from
 
end
class B < A
  def hello
    puts "hello"
  end
end

B.new.hello # => hello

alias_method_chain doesn’t work here. because class B override hello method.

Sollution

replace alias_method_chain :hello, :from to

  #alias_method_chain :hello, :from
  def self.method_added(method_name)
    return if @_in_method_added
    @_in_method_added = true
    if method_name.to_s == "hello"
      alias_method_chain :hello, :from
    end
    @_in_method_added = false
  end

method_added is the method which be called when adding method into class. So we add alias_method_chain after class B add method hello.

Whole Code

require 'rubygems'
require 'active_support'

class A
  def hello
    puts "hello"
  end

  def hello_with_from
    hello_without_from
    puts self.class.name
  end

  def self.method_added(meth)
    return if @_in_method_added
    @_in_method_added = true
    if meth.to_s == "hello"
      alias_method_chain :hello, :from
    end
    @_in_method_added = false
  end
end

class B < A
  def hello
    puts "hello"
  end
end

B.new.hello # => hello \n B

Tagged in ,

Tips – How to test a module

RubyOnRails,Tips 19 三月 2010 | View Comments

   @my_module = Object.new
   @my_module.extend(YourModule)
   #Then test methods in a Module

Tagged in , ,

Ads Plugin created by Jake Ruston's Wordpress Plugins - Powered by and football database.