Passing Arguments to Scripts

Passing Arguments to Scripts#

ARGV#

We already know ARGV from the previous chapters: it is an Array with all the arguments as strings. The rules are exactly the same as in the shell scripting, for example a string with the space must be between apostrophe.

$ ruby -e 'ARGV.each {|x| puts "#{x}, #{x.class}"}' 1 text 'with space'
1, String
text, String
with space, String

ARGF#

It is very common to pass the file names as the script arguments. ARGF provides very handy shortcut for such scripts - it is an I/O stream with the contents of all files passed in arguments. You can use it as a normal file object, so read, readline etc. It is important to remember that ARGF combines all the names given as atributes; for example, giving the same file twice would will cause double output:

$ ruby -e 'puts ARGF.readlines' hello_world.rb hello_world.rb
#!/usr/bin/env ruby
puts "Hello, " + ARGV.first + "."

#!/usr/bin/env ruby
puts "Hello, " + ARGV.first + "."

If there is no arguments given, ARGF is an I/O stream reading from the stdin. It is quite useful when you are writing a small script, and you want to allow user to decide if to pass a file name or to pass something to the scripts stdin.

$ echo "here be dragons" | ruby -e 'puts ARGF.read'
here be dragons

What about if we do not want to pass only the files to the script (some switches like -rf)? It is good to know, that ARGF reads all the files which are not in the ARGV array. So, for all non-file arguments, just remove them from the array.

Another interesting property of ARGF is that it removes the file names from the ARGV after read it.

The script below is a small utility to count number of lines in a given files. When the first argument begins with a dash, it is removed from the ARGV array using Array.shift method. Then the scripts outputs the number of lines in all files combined.

#!/usr/bin/env ruby
unless ARGV.empty?
  if ARGV.first.start_with?("-")
    case ARGV.shift  # shift takes the first argument and removes it from the array
    when '-v', '--verbose'
      verbose = true
    when '--version'
      puts "1.0"
      exit 0         # exit script with status 0 (all OK)
    end
  end
end
if verbose
  puts "Files #{ARGV.join(', ')} contains #{ARGF.readlines.count} lines."
else
  puts "#{ARGF.readlines.count}"
end

Even being so simple, the script above is quite universal. It takes the files as an arguments or - if no arguments given - counts the number of lines in stdin:

$ ./wc.rb /etc/hosts /etc/passwd
236

$ ./wc.rb --verbose /etc/hosts /etc/passwd
Files /etc/hosts, /etc/passwd contains 236 lines.

$ cat /etc/passwd | ./wc.rb --verbose
Files  contains 86 lines.

OptionParser class#

Parsing the arguments can be tricky and complex issue, especially with the big, complicated scripts. But, as usual, there is a helper class for such operations, it is called OptionParser. It allows to build standard Unix interface to command line scripts:

#!/usr/bin/env ruby
require 'optparse'

# default options:
options = {lines: false, verbose: false}

OptionParser.new do |opts|
  # banner and separator are the usage description showed with '--help' or '-h'
  opts.banner = "Usage: wc.rb [options] [files]"
  opts.separator "Reads number of bytes or lines in the files"
  opts.separator "Options:"
  # options (switch - true/false)
  opts.on("-v", "--verbose", "Verbose mode") do |v|
    options[:verbose] = v
  end
  opts.on("-l", "--lines", "Count number of lines") do |l|
    options[:lines] = l
  end
end.parse!

files = ARGV.join(', ') # storing, beacuse ARGF.read* clears the ARGV

# ARGV now contains no options, only file
if options[:lines]
  number = "#{ARGF.readlines.count} lines"
else
  number = "#{ARGF.each_byte.count} bytes"
end

if options[:verbose]
  puts "Files #{files} contains #{number}."
else
  puts number
end

Script equipped with OptionParse shows the banner, separator and the short description of every option when running with –help or -h switch. Parser on(short, long, description) {block} method is used to define the switches and options. It passes the value of the option to the block; in the example above we have only switches (true/false), but you can pass the strings to the options by using –option STRING syntax in the second argument:

opts.on("-f", "--file FILE", "File to process") do |f|
  options[:file] = f   # f becames a filename given after -f or --file
end

There is more interesting options to construct switches: you can pass the list of arguments, define the argument type (other than default String). Please refer to the documentation: ri OptionParser.

Let’s now take a look how the OptionParser works:

$ ./wc.rb --help
Usage: wc.rb [options] [files]
Reads number of bytes or lines in the files
Options:
    -v, --verbose                    Verbose mode
    -l, --lines                      Count number of lines

$ ./wc.rb /etc/passwd /etc/hosts
9026 bytes

$ ./wc.rb -l /etc/passwd /etc/hosts
236 lines

$ ./wc.rb --lines --verbose /etc/passwd /etc/hosts
Files /etc/passwd, /etc/hosts contains 236 lines.

$ ./wc.rb -lv /etc/passwd /etc/hosts
Files /etc/passwd, /etc/hosts contains 236 lines.