Executable

Require Executable library.

require 'executable'

No Subcommmands

This example demonstrates using Executable::Command to create a simple command line interface without subcommands. (Note the Executable mixin could be used just as well).

class NoSubCommandCLI < Executable::Command

  attr :result

  def o?
    @o
  end

  def o=(flag)
    @o = flag
  end

  def call
    if o?
      @result = "with"
    else
      @result = "without"
    end
  end

end

Execute the CLI on an example command line.

cli = NoSubCommandCLI.run('')
cli.result.assert == 'without'

Execute the CLI on an example command line.

cli = NoSubCommandCLI.run('-o')
cli.result.assert == 'with'

There are two important things to notices heres. Frist, that #main is being called in each case. It is the method called with no other subcommands are defined. And second, the fact the a `o?` method is defined to compliment the `o=` writer, informs Executable that `-o` is an option flag, not taking any parameters.

Multiple Subcommmands

Setup an example CLI subclass.

class MyCLI < Executable::Command
  attr :result

  def initialize
    @result = []
  end

  def g=(value)
    @result << "g" if value
  end

  def g?
    @result.include?("g")
  end

  #
  class C1 < self
    def call
      @result << "c1"
    end

    def o1=(value)
      @result << "c1_o1 #{value}"
    end

    def o2=(value)
      @result << "c1_o2 #{value}"
    end
  end

  #
  class C2 < Executable::Command
    attr :result

    def initialize
      @result = []
    end

    def call
      @result << "c2"
    end

    def o1=(value)
      @result << "c2_o1 #{value}"
    end

    def o2=(value)
      @result << "c2_o2" if value
    end

    def o2?
      @result.include?("c2_o2")
    end
  end

end

Instantiate and run the class on an example command line.

Just a command.

cli = MyCLI.run('c1')
cli.result.assert == ['c1']

Command with global option.

cli = MyCLI.run('c1 -g')
cli.result.assert == ['g', 'c1']

Command with an option.

cli = MyCLI.run('c1 --o1 A')
cli.result.assert == ['c1_o1 A', 'c1']

Command with two options.

cli = MyCLI.run('c1 --o1 A --o2 B')
cli.result.assert == ['c1_o1 A', 'c1_o2 B', 'c1']

Try out the second command.

cli = MyCLI.run('c2')
cli.result.assert == ['c2']

Seoncd command with an option.

cli = MyCLI.run('c2 --o1 A')
cli.result.assert == ['c2_o1 A', 'c2']

Second command with two options.

cli = MyCLI.run('c2 --o1 A --o2')
cli.result.assert == ['c2_o1 A', 'c2_o2', 'c2']

Since C1#main takes not arguments, if we try to issue a command that will have left over arguments, then an ArgumentError will be raised.

expect ArgumentError do
  cli = MyCLI.run('c1 a')
end

How about a non-existenct subcommand.

expect NotImplementedError do
  cli = MyCLI.run('q')
  cli.result.assert == ['q']
end

How about an option only.

expect NotImplementedError do
  cli = MyCLI.run('-g')
  cli.result.assert == ['-g']
end

How about a non-existant options.

expect Executable::NoOptionError do
  MyCLI.run('c1 --foo')
end

Command Help

Executable Commands can generate help output. It does this by extracting the commenst associated with the option methods. A description of the command itself is taken from the comment on the `#call` method. Only the first line of a comment is used, so the reset of the comment can still be catered to documention tools such as YARD and RDoc.

Let’s setup an example CLI subclass to demonstrate this.

class MyCLI < Executable::Command

  # This is global option -g.
  # Yadda yadda yadda...
  def g=(bool)
    @g = bool
  end

  def g?; @g; end

  # Subcommand `c1`.
  class C1 < self

    # This does c1.
    def call(*args)
    end

    # This is option --o1 for c1.
    def o1=(value)
    end

    # This is option --o2 for c1.
    def o2=(value)
    end

  end

  # Subcommand `c2`.
  class C2 < self

    # This does c2.
    def call(*args)
    end

    # This is option --o1 for c2.
    def o1=(value)
    end

    # This is option --o2 for c2.
    def o2=(value)
    end

  end

end

Plain Text

The help output,

@out = MyCLI::C1.help.to_s

should be clearly laid out as follows:

Usage: mycli-c1 [options...] [subcommand]

This does c1.

OPTIONS
   -g          This is global option -g.
  --o1=VALUE   This is option --o1 for c1.
  --o2=VALUE   This is option --o2 for c1.

Copyright (c) 2012

Markdown

The help feature can also output ronn-style markdown,

@out = MyCLI::C1.help.markdown

should be clearly laid out as follows:

mycli-c1(1) - This does c1.
===========================

## SYNOPSIS

`mycli-c1` [options...] [subcommand]

## DESCRIPTION

This does c1.

## OPTIONS

  * `-g`:
    This is global option -g.

  * `--o1=VALUE`:
    This is option --o1 for c1.

  * `--o2=VALUE`:
    This is option --o2 for c1.

## COPYRIGHT

Copyright (c) 2012

Manpage

If a man page is available for a given command using the #show_help method will automatically find the manpage and display it.

sample = File.dirname(__FILE__) + '/samples'

load(sample + '/bin/hello')

manpage = Hello.cli.manpage

manpage.assert == sample + '/man/hello.1'

Subclass Example

Lets say we have a class that we would like to work with on the command line, but want to keep the class itself unchanaged without mixin.

class Hello
  attr_accessor :name

  def initialize(name="World")
    @name = name
  end

  def hello
    @output = "Hello, #{name}!"
  end

  def output
    @output
  end
end

Rather then including Exectuable in the class directly, we can create a subclass and use it instead.

class HelloCommand < Hello
  include Executable

  def call(*args)
    hello
  end
end

Now we can execute the command perfectly well.

cmd = HelloCommand.execute(['hello', '--name=Fred'])
cmd.output.assert == "Hello, Fred!"

And the original class remains undisturbed.

README Example

This is the example used in the documentation.

class Example
  include Executable

  attr_switch :quiet

  def bread(*args)
    ["bread", quiet?, *args]
  end

  def butter(*args)
    ["butter", quiet?, *args]
  end

  # Route call to methods.
  def call(name, *args)
    meth = public_method(name)
    meth.call(*args)
  end
end

Use a subcommand and an argument.

c, a = Example.parse(['butter', 'yum'])
r = c.call(*a)
r.assert == ["butter", nil, "yum"]

A subcommand and a boolean option.

c, a = Example.parse(['bread', '--quiet'])
r = c.call(*a)
r.assert == ["bread", true]

Legacy/Dispath

The Dispatch mixin, which is also called Legacy b/c this is how older version of Executable worked, provides Executable with a `#call` method that automatically routes the to a method given by the first argument.

class DispatchExample < Executable::Command
  include Legacy

  attr :result

  def foo
    @result = :foo
  end

  def bar
    @result = :bar
  end

end

Now when we invoke the command, the

eg = DispatchExample.run('foo')
eg.result.assert == :foo

eg = DispatchExample.run('bar')
eg.result.assert == :bar

OptionParser Example

This example mimics the one given in optparse.rb documentation.

require 'ostruct'
require 'time'

class ExampleCLI < Executable::Command

  CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary]
  CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }

  attr :options

  def initialize
    super
    reset
  end

  def reset
    @options = OpenStruct.new
    @options.library = []
    @options.inplace = false
    @options.encoding = "utf8"
    @options.transfer_type = :auto
    @options.verbose = false
  end

  # Require the LIBRARY before executing your script
  def require=(lib)
    options.library << lib
  end
  alias :r= :require=

  # Edit ARGV files in place (make backup if EXTENSION supplied)
  def inplace=(ext)
    options.inplace = true
    options.extension = ext
    options.extension.sub!(/\A\.?(?=.)/, ".")  # ensure extension begins with dot.
  end
  alias :i= :inplace=

  # Delay N seconds before executing
  # Cast 'delay' argument to a Float.
  def delay=(n)
    options.delay = n.to_float
  end

  # Begin execution at given time
  # Cast 'time' argument to a Time object.
  def time=(time)
    options.time = Time.parse(time)
  end
  alias :t= :time=

  # Specify record separator (default \\0)
  # Cast to octal integer.
  def irs=(octal)
    options.record_separator = octal.to_i(8)
  end
  alias :F= :irs=

  # Example 'list' of arguments
  # List of arguments.
  def list=(args)
    options.list = list.split(',')
  end

  # Keyword completion.  We are specifying a specific set of arguments (CODES
  # and CODE_ALIASES - notice the latter is a Hash), and the user may provide
  # the shortest unambiguous text.
  CODE_LIST = (CODE_ALIASES.keys + CODES)

  help.option(:code, "Select encoding (#{CODE_LIST})")

  # Select encoding
  def code=(code)
    codes = CODE_LIST.select{ |x| /^#{code}/ =~ x }
    codes = codes.map{ |x| CODE_ALIASES.key?(x) ? CODE_ALIASES[x] : x }.uniq
    raise ArgumentError unless codes.size == 1
    options.encoding = codes.first
  end

  # Select transfer type (text, binary, auto)
  # Optional argument with keyword completion.
  def type=(type)
    raise ArgumentError unless %w{text binary auto}.include(type.downcase)
    options.transfer_type = type.downcase
  end

  # Run verbosely
  # Boolean switch.
  def verbose=(bool)
    options.verbose = bool
  end
  def verbose?
    @options.verbose
  end
  alias :v= :verbose=
  alias :v? :verbose?

  # Show this message
  # No argument, shows at tail.  This will print an options summary.
  def help!
    puts help_text
    exit
  end
  alias :h! :help!

  # Show version
  # Another typical switch to print the version.
  def version?
    puts Executor::VERSION
    exit
  end

  #
  def call
    # ... main procedure here ...
  end
end

We will run some scenarios on this example to make sure it works.

cli = ExampleCLI.execute('-r=facets')
cli.options.library.assert == ['facets']

Make sure time option parses.

cli = ExampleCLI.execute('--time=2010-10-10')
cli.options.time.assert == Time.parse('2010-10-10')

Make sure code lookup words and is limted to the selections provided.

cli = ExampleCLI.execute('--code=ji')
cli.options.encoding.assert == 'iso-2022-jp'

expect ArgumentError do
  ExampleCLI.execute('--code=xxx')
end

Ensure irs is set to an octal number.

cli = ExampleCLI.execute('-F 32')
cli.options.record_separator.assert == 032

Ensure extension begins with dot and inplace is set to true.

cli = ExampleCLI.execute('--inplace txt')
cli.options.extension.assert == '.txt'
cli.options.inplace.assert == true