How to Make a Ruby Script Using Trollop for Command Line Parsing

How do I make a Ruby script using Trollop for command line parsing?

A: Here is a self-contained example.

The business with if __FILE__ == $0 isn't specific to Trollop; that just means "run the following code if this file is executed as a script". This technique allows you use the file as a library module, separating the business logic from the command line parsing.

#!/usr/bin/env ruby
#
# This is a near-minimal ruby script to demonstrate trollop, a simple
# and elegant command-line parsing package. To run, copy this file to
# 'trollop_demo.rb' and make it executable by
# $ chmod +x trollop_demo.rb
# Then run it via:
# $ ./trollop_demo.rb <your args here>
# For more info on Trollop, see http://trollop.rubyforge.org/
require 'trollop'

# A trivial program...
def main_method(filename, options)
puts("filename = #{filename}, options = #{options}")
end

# Execute this part when the file is run as a script
if __FILE__ == $0
opts = Trollop::options do
version "#{$0} Version 3.14159 (C) 2041 Spacely Sprockets, Inc."
banner <<-EOS
#{$0} is a command-line program to demonstrate trollop
Usage:
#{$0} [options] <filename>
where [options] are zero or more of:
EOS
opt :verbose, "Print extra information."
opt :dry_run, "Don't actually do anything.", :short => "-n"
end
filename = ARGV.shift
if (filename.nil?) || (!File.exist?(filename))
Trollop::die "filename must be given and name an existing file"
else
main_method(filename, opts)
end
end

With just that bit of code, you can now try all these things:

$ ./trollop_demo.rb 
$ ./trollop_demo.rb a_file_that_doesn't_exist
$ ./trollop_demo.rb --version
$ ./trollop_demo.rb --help
$ ./trollop_demo.rb trollop_demo.rb
$ ./trollop_demo.rb --dry-run trollop_demo.rb
$ ./trollop_demo.rb --no-dry-run trollop_demo.rb
$ ./trollop_demo.rb --verbose trollop_demo.rb
$ ./trollop_demo.rb -v trollop_demo.rb

For more information, see http://trollop.rubyforge.org/

Really Cheap Command-Line Option Parsing in Ruby

Here's the standard technique I usually use:

#!/usr/bin/env ruby

def usage(s)
$stderr.puts(s)
$stderr.puts("Usage: #{File.basename($0)}: [-l <logfile] [-q] file ...")
exit(2)
end

$quiet = false
$logfile = nil

loop { case ARGV[0]
when '-q' then ARGV.shift; $quiet = true
when '-l' then ARGV.shift; $logfile = ARGV.shift
when /^-/ then usage("Unknown option: #{ARGV[0].inspect}")
else break
end; }

# Program carries on here.
puts("quiet: #{$quiet} logfile: #{$logfile.inspect} args: #{ARGV.inspect}")

Parse command line arguments in a Ruby script

Based on the answer by @MartinCortez here's a short one-off that makes a hash of key/value pairs, where the values must be joined with an = sign. It also supports flag arguments without values:

args = Hash[ ARGV.join(' ').scan(/--?([^=\s]+)(?:=(\S+))?/) ]

…or alternatively…

args = Hash[ ARGV.flat_map{|s| s.scan(/--?([^=\s]+)(?:=(\S+))?/) } ]

Called with -x=foo -h --jim=jam it returns {"x"=>"foo", "h"=>nil, "jim"=>"jam"} so you can do things like:

puts args['jim'] if args.key?('h')
#=> jam

While there are multiple libraries to handle this—including GetoptLong included with Ruby—I personally prefer to roll my own. Here's the pattern I use, which makes it reasonably generic, not tied to a specific usage format, and flexible enough to allow intermixed flags, options, and required arguments in various orders:

USAGE = <<ENDUSAGE
Usage:
docubot [-h] [-v] [create [-s shell] [-f]] directory [-w writer] [-o output_file] [-n] [-l log_file]
ENDUSAGE

HELP = <<ENDHELP
-h, --help Show this help.
-v, --version Show the version number (#{DocuBot::VERSION}).
create Create a starter directory filled with example files;
also copies the template for easy modification, if desired.
-s, --shell The shell to copy from.
Available shells: #{DocuBot::SHELLS.join(', ')}
-f, --force Force create over an existing directory,
deleting any existing files.
-w, --writer The output type to create [Defaults to 'chm']
Available writers: #{DocuBot::Writer::INSTALLED_WRITERS.join(', ')}
-o, --output The file or folder (depending on the writer) to create.
[Default value depends on the writer chosen.]
-n, --nopreview Disable automatic preview of .chm.
-l, --logfile Specify the filename to log to.

ENDHELP

ARGS = { :shell=>'default', :writer=>'chm' } # Setting default values
UNFLAGGED_ARGS = [ :directory ] # Bare arguments (no flag)
next_arg = UNFLAGGED_ARGS.first
ARGV.each do |arg|
case arg
when '-h','--help' then ARGS[:help] = true
when 'create' then ARGS[:create] = true
when '-f','--force' then ARGS[:force] = true
when '-n','--nopreview' then ARGS[:nopreview] = true
when '-v','--version' then ARGS[:version] = true
when '-s','--shell' then next_arg = :shell
when '-w','--writer' then next_arg = :writer
when '-o','--output' then next_arg = :output
when '-l','--logfile' then next_arg = :logfile
else
if next_arg
ARGS[next_arg] = arg
UNFLAGGED_ARGS.delete( next_arg )
end
next_arg = UNFLAGGED_ARGS.first
end
end

puts "DocuBot v#{DocuBot::VERSION}" if ARGS[:version]

if ARGS[:help] or !ARGS[:directory]
puts USAGE unless ARGS[:version]
puts HELP if ARGS[:help]
exit
end

if ARGS[:logfile]
$stdout.reopen( ARGS[:logfile], "w" )
$stdout.sync = true
$stderr.reopen( $stdout )
end

# etc.

How do I make posititional arguments with trollop?

Trollop is about parsing options -- things which may or may not be present.

A required positional argument is not an option. To access your positional arguments, just look at argv. If you use a mix of options and required positional arguments, trollop will take care of stripping out the optional stuff, and leave ARGV in a state where it contains only the stuff you care about.

If you run this example code:

#!/usr/bin/env ruby

require 'trollop'

opts = Trollop::options do
opt :monkey, "Use monkey mode" # flag --monkey, default false
opt :name, "Monkey name", :type => :string # string --name <s>, default nil
opt :num_limbs, "Number of limbs", :default => 4 # integer --num-limbs <i>, default to 4
end

puts "opts: #{opts}"
puts "ARGV: #{ARGV}"

Here is what you will get:

[~/tbear/bin]$ ./t.rb
opts: {:monkey=>false, :name=>nil, :num_limbs=>4, :help=>false}
ARGV: []

[~/tbear/bin]$ ./t.rb arg1
opts: {:monkey=>false, :name=>nil, :num_limbs=>4, :help=>false}
ARGV: ["arg1"]

[~/tbear/bin]$ ./t.rb -m arg1
opts: {:monkey=>true, :name=>nil, :num_limbs=>4, :help=>false, :monkey_given=>true}
ARGV: ["arg1"]

Notice how ARGV is the same in example 2 and example 3.

Parse multiple command line options in Ruby using OptionParser

OptionParser doesn't support that; It could be patched to do so, but I'm not sure it's worth the trouble.

Consider this code:

require 'optparse'

options = {}
OptionParser.new do |opt|
opt.on('-m', '--move') { |o| options[:move] = o }
end.parse!

from_name, to_name = ARGV

puts "Should move: #{ options.key?(:move) }"
puts "From: #{ from_name }"
puts "To: #{ to_name }"

Saving it and running it with various combinations of the parameters returns:

> ruby test.rb --move from to
Should move: true
From: from
To: to

> ruby test.rb from to
Should move: false
From:
To:

If the code is supposed to move files by default then don't bother with the --move flag, simply use:

test.rb from to

and consider removing the OptionParser block entirely.

If the code is supposed to normally copy with the option to move, then --move becomes more sensible to act as a flag that moving is desired.

ruby test.rb --move from to

I'd have code that tests for options[:move] and run the code to move instead of copy at that point.

In either case, the filenames shouldn't be tied to the flag, they should be supplied separately and retrieved from ARGV after OptionParser has finished parsing the command-line and removing entries it's handled.

Passing string with dash as parameter into Trollop

Trollop's job is to parse command line options. If you had an option defined as '-W', how would it differentiate between that option and an argument that happened to begin with '-W'?

So even if there were a Trollop option to ignore unknown options and let them pass through as arguments to your program, if you defined any options at all you would still have the problem when a string began with a hyphen followed by the defined option's letter.

One thing you could do would be to require users who want to begin an argument with a hyphen to precede it with a backslash. That would succeed in hiding it from Trollop, but then before using it you would need to remove the backslash. As long as backslash would never be a legitimate character in the id string this should be ok.

By the way you might want to add the short option:

require 'trollop'
opts = Trollop::options do
opts = Trollop::options do
opt :id, 'Video Id', type: String, short: :i
opt :title, 'Video Title', type: String, short: :t
end
end

p opts
p ARGV

You could try running it like this, and then observing the result:

➜  stack_overflow git:(master) ✗   ./trollop.rb -i 3 '\-i1'
{:id=>"3", :title=>nil, :help=>false, :id_given=>true}
["\\-i1"]

Parsing arguments with defaults and flags

You can modify ARGV before Trollop processes it. Your best bet would probably be to scan the input arguments, apply some basic transformations, and then run Trollop.

For example:

args = ARGV.split
idx = args.index '--scrape'
if idx != nil
if idx < args.length
if args[idx + 1][0..1] == '--'
args=args[0..idx] + ['.'] + args[idx+1..-1]
end
else
if args[idx + 1][0..1] == '--'
args << '.'
end
end
end

This snippet should check for --scrape with no parameter following it and add in a '.' in that case. You can do something similar to check for the omitted --run parameter. When you are done making your modifications, use args.join(' ') to put the arguments back together into a string. Assign this new string to ARGV, and then set Trollop loose.

Using ruby's OptionParser to parse sub-commands

Figured it out. I need to use OptionParser#order!. It will parse all the options from the start of ARGV until it finds a non-option (that isn't an option argument), removing everything it processes from ARGV, and then it will quit.

So I just need to do something like:

global = OptionParser.new do |opts|
# ...
end
subcommands = {
'foo' => OptionParser.new do |opts|
# ...
end,
# ...
'baz' => OptionParser.new do |opts|
# ...
end
}

global.order!
subcommands[ARGV.shift].order!


Related Topics



Leave a reply



Submit