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