Module: Methadone::Main

Includes:
ARGVParser, ExitNow
Defined in:
lib/methadone/main.rb

Overview

Include this module to gain access to the “canonical command-line app structure” DSL. This is a very lightweight layer on top of what you might normally write that gives you just a bit of help to keep your code structured in a sensible way. You can use as much or as little as you want, though you must at least use #main to get any benefits.

Further, you must provide access to a logger via a method named #logger. If you include Methadone::CLILogging, this will be done for you

You also get a more expedient interface to OptionParser as well as checking for required arguments to your app. For example, if we want our app to accept a negatable switch named “switch”, a flag named “flag”, and two arguments “needed” (which is required) and “maybe” which is optional, we can do the following:

#!/usr/bin/env ruby

require 'methadone'

class App
  include Methadone::Main
  include Methadone::CLILogging

  main do |needed, maybe|
    options[:switch] => true or false, based on command line
    options[:flag] => value of flag passed on command line
  end

  # Proxy to an OptionParser instance's on method
  on("--[no]-switch")
  on("--flag VALUE")

  arg :needed
  arg :maybe, :optional

  defaults_from_env_var SOME_VAR
  defaults_from_config_file '.my_app.rc'

  go!
end

Our app then acts as follows:

$ our_app 
# => parse error: 'needed' is required
$ our_app foo
# => succeeds; "maybe" in main is nil
$ our_app --flag foo
# => options[:flag] has the value "foo"
$ SOME_VAR='--flag foo' our_app
# => options[:flag] has the value "foo"
$ SOME_VAR='--flag foo' our_app --flag bar
# => options[:flag] has the value "bar"

Note that we’ve done all of this inside a class that we called App. This isn’t strictly necessary, and you can just include Methadone::Main and Methadone::CLILogging at the root of your bin file if you like. This is somewhat unsafe, because self inside the bin file is Object, and any methods you create (or cause to be created via include) will be present on every object. This can cause odd problems, so it’s recommended that you not do this.

Subcommands


In order to promote modularity and maintainability, complex command line applications should be broken up into subcommands. Subcommands are just like regular Methadone applications, except you don’t put a go! call in it. It will be run in by the base methadone app class. Likewise, subcommands can have subcommands of their own.

In order to tell a Methadone app class that it has subcommands, use the command method, which takes a hash with the command name as a key and the command class as the value. Multiple subcommands can be specified in a single call, or as separate calls.

#!/usr/bin/env ruby

require 'methadone'

class MySubcommand
  include Methadone::Main
  include Methadone::CLILogging

  on '-f','--foo BAR', 'Some option'
  arg 'something', :required, "Description","defaults: value"

  main do |something|
   # stuff
  end
end

class App
  include Methadone::Main
  include Methadone::CLILogging

  command "do" => MySubcommand

  go!
end

Apps that have subcommands (currently) don’t support arguments and don’t need to supply a main, as it doesn’t get called. This may change in a future version of Methadone. Options to the app can modify the options contents will impactful to the subcommand as it receives those option values as the base for its options.

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ExitNow

#exit_now!, #help_now!

Class Method Details

.included(k) ⇒ Object



121
122
123
# File 'lib/methadone/main.rb', line 121

def self.included(k)
  k.extend(self)
end

Instance Method Details

#arg(arg_name, *options) ⇒ Object

Sets the name of an arguments your app accepts.

arg_name

name of the argument to appear in documentation This will be converted into a String and used to create the banner (unless you have overridden the banner)

options

list (not Hash) of options:

:required

this arg is required (this is the default)

:optional

this arg is optional

:one

only one of this arg should be supplied (default)

:many

many of this arg may be supplied, but at least one is required

:any

any number, include zero, may be supplied

A string

if present, this will be documentation for the argument and appear in the help. Multiple strings will be listed on multiple lines

A Regexp

Argument values must match the regexp, or an error will be raised.

An Array

Argument values must be found in the array, or an error will be raised.

As of version 2.0, best effort is made to ensure values are assigned to
your arguments as needed.  :required and :many options will take one
value if possible, and the first greedy argument (:many or :any) will
consume any unallocated count of values remaining in ARGV.  Value
assignment still goes left to right, but allocation counts are determined
by needs of each argument.  Filtering rules do not play a part in
determining if a value can be allocated to an argument.

Greedy arguments that do not receive any values will hold an empty
array, while non-greedy arguments that do not receive a value will be
nil.


370
371
372
# File 'lib/methadone/main.rb', line 370

def arg(arg_name,*options)
  opts.arg(arg_name,*options)
end

#command(*args) ⇒ Object

Calls the command method of #opts with the given arguments (see RDoc for #opts for the additional help provided). Commands are special args that take their own options and other arguments.



339
340
341
# File 'lib/methadone/main.rb', line 339

def command(*args)
  opts.command(*args)
end

#defaults_from_config_file(filename, options = {}) ⇒ Object

Set the path to the file where defaults can be configured.

The format of this file can be either a simple string of options, like what goes in the environment variable (see #defaults_from_env_var), or YAML, in which case it should be a hash where keys are the option names, and values their defaults.

Relative paths will be expanded relative to the user’s home directory.

filename

path to the file. If relative, will look in user’s HOME directory. If absolute, this is the absolute path to where the file should be.



202
203
204
# File 'lib/methadone/main.rb', line 202

def defaults_from_config_file(filename,options={})
  @rc_file = File.expand_path(filename, ENV['HOME'])
end

#defaults_from_env_var(env_var) ⇒ Object

Set the name of the environment variable where users can place default options for your app. Omit this to disable the feature.



188
189
190
# File 'lib/methadone/main.rb', line 188

def defaults_from_env_var(env_var)
  @env_var = env_var
end

#description(desc = nil) ⇒ Object

Set the description of your app for inclusion in the help output.

desc

a short, one-line description of your app



376
377
378
# File 'lib/methadone/main.rb', line 376

def description(desc=nil)
  opts.description(desc)
end

#global_optionsObject



405
406
407
408
409
# File 'lib/methadone/main.rb', line 405

def global_options
  (@parent.nil? ? {} : @parent.global_options).merge(
    opts.global_options
  )
end

#go!(parent = nil) ⇒ Object

Start your command-line app, exiting appropriately when complete.

This will exit your program when it completes. If your #main block evaluates to an integer, that value will be sent to Kernel#exit, otherwise, this will exit with 0

If the command-line options couldn’t be parsed, this will exit with 64 and whatever message OptionParser provided.

If a required argument (see #arg) is not found, this exits with 64 and a message about that missing argument.



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/methadone/main.rb', line 219

def go!(parent=nil)

  if @default_help and ARGV.empty?
    puts opts.to_s
    exit 64 # sysexits.h exit code EX_USAGE
  end

  # Get stuff from parent, if there
  set_parent(parent)

  setup_defaults
  opts.post_setup

  if opts.commands.empty?
    opts.parse!
    opts.check_args!
    opts.check_option_usage!
    result = call_main
  else
    opts.parse_to_command! # Leaves unknown args and options in once it encounters a non-option.
    opts.check_option_usage!
    if opts.selected_command
      result = call_provider
    else
      logger.error "You must specify a command"
      puts ""
      puts opts.help
      exit 64
    end
  end

  if result.kind_of? Fixnum
    exit result
  else
    exit 0
  end
rescue OptionParser::ParseError => ex
  logger.error ex.message
  puts
  puts opts.help
  exit 64 # Linux standard for bad command line
end

#help_if_bareObject

Print the usage help if the command is run without any options or arguments.



182
183
184
# File 'lib/methadone/main.rb', line 182

def help_if_bare
  @default_help = true
end

#leak_exceptions(leak) ⇒ Object

Configure the auto-handling of StandardError exceptions caught from calling go!.

leak

if true, go! will not catch StandardError exceptions, but instead allow them to bubble up. If false, they will be caught and handled as normal. This does not affect Methadone::Error exceptions; those will NOT leak through.

leak_exceptions only needs to be set once; since it is stored as a class variable, all classes that include this module will handle exceptions the same way.



176
177
178
# File 'lib/methadone/main.rb', line 176

def leak_exceptions(leak)
  @@leak_exceptions = leak
end

#main(&block) ⇒ Object

Declare the main method for your app. This allows you to specify the general logic of your app at the top of your bin file, but can rely on any methods or other code that you define later.

For example, suppose you want to process a set of files, but wish to determine that list from another method to keep your code clean.

#!/usr/bin/env ruby -w

require 'methadone'

include Methadone::Main

main do
  files_to_process.each do |file|
    # process file
  end
end

def files_to_process
  # return list of files
end

go!

The block can accept any parameters, and unparsed arguments from the command line will be passed.

Note: #go! will modify ARGV to remove any known options and arguments. If there are any values left over, they will remain available in ARGV. This behaviour is different from 1.x versions of Methadone, which emptied ARGV completely

To run this method, call #go!



161
162
163
# File 'lib/methadone/main.rb', line 161

def main(&block)
  @main_block = block
end

#on(*args, &block) ⇒ Object

Calls the on method of #opts with the given arguments (see RDoc for #opts for the additional help provided).



332
333
334
# File 'lib/methadone/main.rb', line 332

def on(*args,&block)
  opts.on(*args,&block)
end

#optionsObject

Returns a Hash that you can use to store or retrieve options parsed from the command line. When you put values in here, if you do so before you’ve declared your command-line interface via #on, the value will be used in the docstring to indicate it is the default. You can use either a String or a Symbol and, after #go! is called and the command-line is parsed, the values will be available as both a String and a Symbol.

Example

main do
  puts options[:foo] # put the value of --foo that the user provided
end

options[:foo] = "bar" # set "bar" as the default value for --foo, which
                      # will cause us to include "(default: bar)" in the
                      # docstring

on("--foo FOO","Sets the foo")
go!


401
402
403
# File 'lib/methadone/main.rb', line 401

def options
  @options ||= {}
end

#optsObject

Returns an OptionParser that you can use to declare your command-line interface. Generally, you won’t use this and will use #on directly, but this allows you to have complete control of option parsing.

The object returned has an additional feature that implements typical use of OptionParser.

opts.on("--flag VALUE")

Does this under the covers:

opts.on("--flag VALUE") do |value|
  options[:flag] = value
end

Since, most of the time, this is all you want to do, this makes it more expedient to do so. The key that is is set in #options will be a symbol and string of the option name, without the leading dashes. Note that if you use multiple option names, a key will be generated for each. Further, if you use the negatable form, only the positive key will be set, e.g. for --[no-]verbose, only :verbose will be set (to true or false).

As an example, this declaration:

opts.on("-f VALUE", "--flag")

And this command-line invocation:

$ my_app -f foo

Will result in all of these forms returning the String “foo”:

  • options['f']

  • options[:f]

  • options['flag']

  • options[:flag]

Further, any one of those keys can be used to determine the default value for the option.

Playing well with others


Sometimes you need the user to specify groups of options, or sometimes one option cannot be used in conjunction with another option. While OptionParser does not natively support this, options defined with Methadone’s on method does so by using the following hash arguments:

:excludes => <optID>
:requires => <optID>

The optID can be any of the keys that an option would create in the options hash. You can even specify multiple options by using an array of optIDs:

:excludes => [:f, "another-option"]

If you specify both an option and another option that excludes that option, an error is logged. Only one side of an exclusion needs to be specified.

If you use an option, but do not use an option it requires, an error will be logged. Order of the options do not matter.



326
327
328
# File 'lib/methadone/main.rb', line 326

def opts
  @option_parser ||= OptionParserProxy.new(OptionParser.new,options)
end

#version(version, version_options = {}) ⇒ Object

Set the version of your app so it appears in the banner. This also adds –version as an option to your app which, when used, will act just like –help (see version_options to control this)

version

the current version of your app. Should almost always be YourApp::VERSION, where the module YourApp should’ve been generated by the bootstrap script

version_options

controls how the version option behaves. If this is a string, then the string will be used as documentation for the –version flag. If a Hash, more configuration is available:

custom_docs

the string to document the –version flag if you don’t like the default

compact

if true, –version will just show the app name and version - no help

format

if provided, this can give limited control over the format of the compact version string. It should be a printf-style string and will be given two options: the first is the CLI app name, and the second is the version string



426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/methadone/main.rb', line 426

def version(version,version_options={})
  opts.version(version)
  if version_options.kind_of?(Symbol)
    case version_options
    when :terse
      version_options = {
        :custom_docs => "Show version",
        :format => '%0.0s%s',
        :compact => true
      }
    when :basic
      version_options = {
        :custom_docs => "Show version info",
        :compact => true
      }
    else
      version_options = version_options.to_s
    end
  end

  if version_options.kind_of?(String)
    version_options = { :custom_docs => version_options }
  end
  version_options[:custom_docs] ||= "Show help/version info"
  version_options[:format] ||= "%s version %s"
  opts.on("--version",version_options[:custom_docs]) do 
    if version_options[:compact]
      puts version_options[:format] % [::File.basename($0),version]
    else
      puts opts.to_s
    end
    exit 0
  end
end