Benry-CmdApp
($Release: 1.1.2 $)
What's This?
Benry-CmdApp is a framework to create command-line application.
If you want create command-line application which takes sub-commands
like git
, docker
, or npm
, Benry-CmdApp is the solution.
Basic idea:
- Action (= sub-command) is defined as a method in Ruby.
- Commnad-line arguments are passed to action method as positional arguments.
- Command-line options are passed to action method as keyword arguments.
For example:
<command> hello
in command-line invokes action methodhello()
in Ruby.<command> hello arg1 arg2
invokeshello("arg1", "arg2")
.<command> hello arg --opt=val
invokeshello("arg", opt: "val")
.
Links:
- Document: https://kwatch.github.io/benry-ruby/benry-cmdapp.html
- GitHub: https://github.com/kwatch/benry-ruby/tree/main/benry-cmdapp
- Changes: https://github.com/kwatch/benry-ruby/tree/main/benry-cmdapp/CHANGES.md
Benry-CmdApp requires Ruby >= 2.3.
Table of Contents
- What's This?
- Install
- Basic Usage
- Advanced Feature
- Configuratoin and Customization
- Q & A
- Q: How to show all backtraces of exception?
- Q: How to specify description to arguments of actions?
- Q: How to append some tasks to an existing action?
- Q: How to delete an existing action/alias?
- Q: How to re-define an existing action?
- Q: How to show entering into or exitting from actions?
- Q: How to enable/disable color mode?
- Q: How to define
-vvv
style option? - Q: How to show global option
-L <topic>
in help message? - Q: How to specify detailed description of options?
- Q: How to list only aliases (or actions) excluding actions (or aliases) ?
- Q: How to change the order of options in help message?
- Q: How to add metadata to actions or options?
- Q: How to remove common help option from all actions?
- Q: Is it possible to show details of actions and aliases?
- Q: How to make error messages I18Ned?
- License and Copyright
Install
$ gem install benry-cmdapp
Basic Usage
Action
How to define actions:
- (1) Inherit action class.
- (2) Define action methods with
@action.()
. - (3) Create an application object and run it.
Note:
- Use
@action.()
, not@action()
. - Command-line arguments are passed to action method as positional arguments.
- An action class can have several action methods.
- It is ok to define multiple action classes.
File: ex01.rb
# coding: utf-8 require 'benry/cmdapp' ## (1) Inherit action class. class MyAction < Benry::CmdApp::Action # !!!! ## (2) Define action methods with `@action.()`. @action.("print greeting message") # !!!! def hello(name="world") # !!!! puts "Hello, #{name}!" end end ## (3) Create an application object and run it. status_code = Benry::CmdApp.main("sample app", "1.0.0") exit status_code ## or: #config = Benry::CmdApp::Config.new("sample app", "1.0.0") #app = Benry::CmdApp::Application.new(config) #status_code = app.main() #exit status_code
Output:
[bash]$ ruby ex01.rb hello # action Hello, world! [bash]$ ruby ex01.rb hello Alice # action + argument Hello, Alice!
Help message of command:
[bash]$ ruby ex01.rb -h # or `--help` ex01.rb (1.0.0) --- sample app Usage: $ ex01.rb [<options>] <action> [<arguments>...] Options: -h, --help : print help message (of action if specified) -V, --version : print version -l, --list : list actions and aliases -a, --all : list hidden actions/options, too Actions: hello : print greeting message help : print help message (of action if specified)
Help message of action:
[bash]$ ruby ex01.rb -h hello # or: ruby ex01.rb --help hello ex01.rb hello --- print greeting message Usage: $ ex01.rb hello [<name>]
- Benry-CmdApp adds
-h
and--help
options to each action automatically. Output ofruby ex01.rb hello -h
andruby ex01.rb -h hello
will be the same.
[bash]$ ruby ex01.rb hello -h # or: ruby ex01.rb helo --help ex01.rb hello --- print greeting message Usage: $ ex01.rb hello [<name>]
Method Name and Action Name
Rules between method name and action name:
- Method name
print_
results in action nameprint
. This is useful to define actions which name is same as Ruby keyword or popular functions. - Method name
foo_bar_baz
results in action namefoo-bar-baz
. - Method name
foo__bar__baz
results in action namefoo:bar:baz
.
File: ex02.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action ## 'print_' => 'print' @action.("sample #1") def print_() # !!!! puts __method__ end ## 'foo_bar_baz' => 'foo-bar-baz' @action.("sample #2") def foo_bar_baz() # !!!! puts __method__ end ## 'foo__bar__baz' => 'foo:bar:baz' @action.("sample #3") def foo__bar__baz() # !!!! puts __method__ end end status_code = Benry::CmdApp.main("test app") exit status_code ## or: #config = Benry::CmdApp::Config.new("test app") #app = Benry::CmdApp::Application.new(config) #status_code = app.main() #exit status_code
Help message:
[bash]$ ruby ex02.rb --help ex02.rb --- test app Usage: $ ex02.rb [<options>] <action> [<arguments>...] Options: -h, --help : print help message (of action if specified) -l, --list : list actions and aliases -a, --all : list hidden actions/options, too Actions: foo-bar-baz : sample #2 foo:bar:baz : sample #3 help : print help message (of action if specified) print : sample #1
Output:
[bash]$ ruby ex02.rb print # `print_` method print_ [bash]$ ruby ex02.rb foo-bar-baz # `foo_bar_baz` method foo_bar_baz [bash]$ ruby ex02.rb foo:bar:baz # `foo__bar__baz` method foo__bar__baz
Parameter Name in Help Message of Action
In help message of an action, positional parameters of action methods are printed under the name conversion rule.
- Parameter
foo
is printed as<foo>
. - Parameter
foo_bar_baz
is printed as<foo-bar-baz>
. - Parameter
foo_or_bar_or_baz
is printed as<foo|bar|baz>
. - Parameter
foobar__xxx
is printed as<foobar.xxx>
.
In addition, positional parameters are printed in different way according to its kind.
- If parameter
foo
is required (= doesn't have default value), it will be printed as<foo>
. - If parameter
foo
is optional (= has default value), it will be printed as[<foo>]
. - If parameter
foo
is variable length (=*foo
style), it will be printed as[<foo>...]
. - If parameter
foo
is required or optional andfoo_
is variable length, it will be printed as<foo>...
.
File: ex03.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("name conversion test") def test1(file_name, file_or_dir, file__html) # !!!! # ... end @action.("parameter kind test") def test2(aaa, bbb, ccc=nil, ddd=nil, *eee) # !!!! # ... end @action.("parameter combination test") def test3(file, *file_) # !!!! files = [file] + file_ # ... end end status_code = Benry::CmdApp.main("sample app", "1.0.0") exit status_code
Help message:
[bash]$ ruby ex03.rb -h test1 ex03.rb test1 --- name conversion test Usage: $ ex03.rb test1 <file-name> <file|dir> <file.html> # !!!! [bash]$ ruby ex03.rb -h test2 ex03.rb test2 --- parameter kind test Usage: $ ex03.rb test2 <aaa> <bbb> [<ccc> [<ddd> [<eee>...]]] # !!!! [bash]$ ruby ex03.rb -h test3 ex03.rb test3 --- parameter combination test Usage: $ ex03.rb test3 <file>... # !!!!
Options
- Action can take command-line options.
- Option values specified in command-line are passed to action method as keyword arguments.
File: ex04.rb
# coding: utf-8 require 'benry/cmdapp' class MyAction < Benry::CmdApp::Action @action.("print greeting message") @option.(:lang, "-l, --lang=<en|fr|it>", "language") # !!!! def hello(user="world", lang: "en") # !!!! case lang when "en" ; puts "Hello, #{user}!" when "fr" ; puts "Bonjour, #{user}!" when "it" ; puts "Ciao, #{user}!" else raise "#{lang}: Unknown language." end end end exit Benry::CmdApp.main("sample app", "1.0.0")
Output:
[bash]$ ruby ex04.rb hello Hello, world! [bash]$ ruby ex04.rb hello -l fr # !!!! Bonjour, world! [bash]$ ruby ex04.rb hello --lang=it # !!!! Ciao, world!
- An action can have multiple options.
- Option format can have indentation spaces, for example
' --help'
.
File: ex05.rb
# coding: utf-8 require 'benry/cmdapp' class MyAction < Benry::CmdApp::Action @action.("print greeting message") @option.(:lang , "-l, --lang=<en|fr|it>", "language") @option.(:repeat, " --repeat=<N>", "repeat <N> times") # !!!! def hello(user="world", lang: "en", repeat: "1") #p repeat.class #=> String # !!!! repeat.to_i.times do # !!!! case lang when "en" ; puts "Hello, #{user}!" when "fr" ; puts "Bonjour, #{user}!" when "it" ; puts "Ciao, #{user}!" else raise "#{lang}: Unknown language." end end end end exit Benry::CmdApp.main("sample app", "1.0.0")
Output:
[bash]$ ruby ex05.rb hello Alice -l fr --repeat=3 Bonjour, Alice! Bonjour, Alice! Bonjour, Alice!
Help message:
[bash]$ ruby ex05.rb -h hello ex05.rb hello --- print greeting message Usage: $ ex05.rb hello [<options>] [<user>] Options: -l, --lang=<en|fr|it> : language --repeat=<N> : repeat <N> times # !!!!
- If an option defined but the corresponding keyword argument is missing, error will be raised.
- If an action method accepts any keyword arguments (such as
**kwargs
), nothing will be raised.
### ERROR: option `:repeat` is defined but keyword arg `repeat:` is missing. @action.("greeting message") @option.(:lang , "-l <lang>", "language") @option.(:repeat, "-n <N>" , "repeat N times") def hello(user="world", lang: "en") .... end ### NO ERROR: `**kwargs` accepts any keyword arguments. @action.("greeting message") @option.(:lang , "-l <lang>", "language") @option.(:repeat, "-n <N>" , "repeat N times") def hello(user="world", lang: "en", **kwargs) .... end
For usability reason, Benry-CmdApp supports --lang=<val>
style of long option
but doesn't support --lang <val>
style.
Benry-CmdApp regards --lang <val>
as 'long option without argument'
and 'argument for command'.
[bash]$ ruby ex05.rb hello --lang fr # ``--lang fr`` != ``--lang=fr`` [ERROR] --lang: Argument required.
Option Definition Format
There are 9 option definition formats.
- When the option takes no value:
-q
--- Short style.--quiet
--- Long style.-q, --quiet
--- Short and long style.
- When the option takes a required value:
-f <path>
--- Short style.--file=<path>
--- Long style.-f, --file=<path>
--- Short and long style.
- When the option takes an optional value:
-i[<N>]
--- Short style.--indent[=<N>]
--- Long style.-i, --indent[=<N>]
--- Short and long style.
File: ex06.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action ## short options @action.("short options") @option.(:quiet , "-q" , "quiet mode") # none @option.(:file , "-f <file>" , "filename") # required @option.(:indent , "-i[<N>]" , "indent width") # optional def test1(quiet: false, file: nil, indent: nil) puts "quiet=#{quiet.inspect}, file=#{file.inspect}, indent=#{indent.inspect}" end ## long options @action.("long options") @option.(:quiet , "--quiet" , "quiet mode") # none @option.(:file , "--file=<file>" , "filename") # required @option.(:indent , "--indent[=<N>]" , "indent width") # optional def test2(quiet: false, file: nil, indent: nil) puts "quiet=#{quiet.inspect}, file=#{file.inspect}, indent=#{indent.inspect}" end ## short and long options @action.("short and long options") @option.(:quiet , "-q, --quiet" , "quiet mode") # none @option.(:file , "-f, --file=<file>" , "filename") # required @option.(:indent , "-i, --indent[=<N>]" , "indent width") # optional def test3(quiet: false, file: nil, indent: nil) puts "quiet=#{quiet.inspect}, file=#{file.inspect}, indent=#{indent.inspect}" end end exit Benry::CmdApp.main("test app")
Output:
[bash]$ ruby ex06.rb test1 -q -f readme.txt -i4 quiet=true, file="readme.txt", indent="4" [bash]$ ruby ex06.rb test2 --quiet --file=readme.txt --indent=4 quiet=true, file="readme.txt", indent="4" [bash]$ ruby ex06.rb test3 -q -f readme.txt -i4 quiet=true, file="readme.txt", indent="4" [bash]$ ruby ex06.rb test3 --quiet --file=readme.txt --indent=4 quiet=true, file="readme.txt", indent="4"
Optional argument example:
[bash]$ ruby ex06.rb test1 -i # ``-i`` results in ``true`` quiet=false, file=nil, indent=true [bash]$ ruby ex06.rb test1 -i4 # ``-i4`` results in ``4`` quiet=false, file=nil, indent="4" [bash]$ ruby ex06.rb test2 --indent # ``--indent`` results in ``true`` quiet=false, file=nil, indent=true [bash]$ ruby ex06.rb test2 --indent=4 # ``--indent=4`` results in ``4`` quiet=false, file=nil, indent="4"
Help message:
[bash]$ ruby ex06.rb -h test1 ex06.rb test1 --- short options Usage: $ ex06.rb test1 [<options>] Options: -q : quiet mode -f <file> : filename -i[<N>] : indent width [bash]$ ruby ex06.rb -h test2 ex06.rb test2 --- long options Usage: $ ex06.rb test2 [<options>] Options: --quiet : quiet mode --file=<file> : filename --indent[=<N>] : indent width [bash]$ ruby ex06.rb -h test3 ex06.rb test3 --- short and long options Usage: $ ex06.rb test3 [<options>] Options: -q, --quiet : quiet mode -f, --file=<file> : filename -i, --indent[=<N>] : indent width
Option Value Validation
@option.()
can validate option value via keyword argument.
type: <class>
specifies option value class. Currently supportsInteger
,Float
,TrueClass
, andDate
.rexp: <rexp>
specifies regular expression of option value.enum: <array>
specifies available values as option value.range: <range>
specifies range of option value.
File: ex07.rb
# coding: utf-8 require 'benry/cmdapp' class MyAction < Benry::CmdApp::Action @action.("print greeting message") @option.(:lang , "-l, --lang=<en|fr|it>", "language", enum: ["en", "fr", "it"], # !!!! rexp: /\A\w\w\z/) # !!!! @option.(:repeat, " --repeat=<N>", "repeat <N> times", type: Integer, range: 1..10) # !!!! def hello(user="world", lang: "en", repeat: 1) #p repeat.class #=> Integer repeat.times do case lang when "en" ; puts "Hello, #{user}!" when "fr" ; puts "Bonjour, #{user}!" when "it" ; puts "Ciao, #{user}!" else raise "#{lang}: Unknown language." end end end end exit Benry::CmdApp.main("sample app", "1.0.0")
Output:
[bash]$ ruby ex07.rb hello -l japan [ERROR] -l japan: Pattern unmatched. [bash]$ ruby ex07.rb hello -l ja [ERROR] -l ja: Expected one of en/fr/it. [bash]$ ruby ex07.rb hello --repeat=abc [ERROR] --repeat=abc: Integer expected. [bash]$ ruby ex07.rb hello --repeat=100 [ERROR] --repeat=100: Too large (max: 10).
Callback for Option Value
@option.()
can take a block argument which is a callback for option value.
Callback can:
- Do custom validation of option value.
- Convert option value into other value.
File: ex08.rb
# coding: utf-8 require 'benry/cmdapp' class MyAction < Benry::CmdApp::Action @action.("print greeting message") @option.(:lang , "-l, --lang=<en|fr|it>", "language", enum: ["en", "fr", "it", "EN", "FR", "IT"], rexp: /\A\w\w\z/) {|v| v.downcase } # !!!! @option.(:repeat, " --repeat=<N>", "repeat <N> times", type: Integer) {|v| # !!!! v > 0 or raise "Not positive value." # !!!! v # !!!! } # !!!! def hello(user="world", lang: "en", repeat: 1) repeat.times do case lang when "en" ; puts "Hello, #{user}!" when "fr" ; puts "Bonjour, #{user}!" when "it" ; puts "Ciao, #{user}!" else raise "#{lang}: Unknown language." end end end end exit Benry::CmdApp.main("sample app", "1.0.0")
Output:
[bash]$ ruby ex08.rb hello -l FR # converted into lowercase Bonjour, world! [bash]$ ruby ex08.rb hello --repeat=0 [ERROR] --repeat=0: Not positive value.
Boolean (On/Off) Option
Benry-CmdApp doesn't support --[no-]foobar
style option.
Instead, define boolean (on/off) option.
- Specify
type: TrueClass
to@option.()
. - Option value
true
,yes
, andon
are converted into true. - Option value
false
,no
, andoff
are converted into false.
File: ex09.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("flag test") @option.(:verbose, "--verbose[=<on|off>]", # !!!! "verbose mode", type: TrueClass) # !!!! def flagtest(verbose: false) # !!!! puts "verbose=#{verbose}" end end exit Benry::CmdApp.main("sample app", "1.0.0")
Output:
[bash]$ ruby ex09.rb flagtest --verbose=on # on verbose=true [bash]$ ruby ex09.rb flagtest --verbose=off # off verbose=false [bash]$ ruby ex09.rb flagtest --verbose=true # on verbose=true [bash]$ ruby ex09.rb flagtest --verbose=false # off verbose=false [bash]$ ruby ex09.rb flagtest --verbose=yes # on verbose=true [bash]$ ruby ex09.rb flagtest --verbose=no # off verbose=false [bash]$ ruby ex09.rb flagtest --verbose=abc # error [ERROR] --verbose=abc: boolean expected.
If you want default value of flag to true
, use value:
keyword argument.
value:
keyword argument in@option.()
specifies the substitute value instead oftrue
when no option value specified in command-line.
File: ex10.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("flag test") @option.(:verbose, "-q, --quiet", "quiet mode", value: false) # !!!! def flagtest2(verbose: true) # !!!! puts "verbose=#{verbose.inspect}" end end exit Benry::CmdApp.main("git helper")
Output:
[bash]$ ruby ex10.rb flagtest2 # true if '--quiet' NOT specified verbose=true [bash]$ ruby ex10.rb flagtest2 --quiet # false if '--quiet' specified verbose=false [bash]$ ruby ex10.rb flagtest2 --quiet=on # error [ERROR] --quiet=on: Unexpected argument.
In above example, --quiet=on
will be error because option is defined as
@option.(:verbose, "-q, --quiet", ...)
which means that this option takes no arguments.
If you want to allow --quiet=on
, specify option argument and type: TrueClass
.
...(snip)... @action.("flag test") @option.(:verbose, "-q, --quiet[=<on|off>]", "quiet mode", # !!!! type: TrueClass, value: false) # !!!! def flagtest2(verbose: true) puts "verbose=#{verbose.inspect}" end ...(snip)...
Option Set
Option set handles multiple options as a object. Option set will help you to define same options into multiple actions.
File: ex11.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action optset1 = optionset() { # !!!! @option.(:host , "-H, --host=<host>" , "host name") @option.(:port , "-p, --port=<port>" , "port number", type: Integer) } optset2 = optionset() { # !!!! @option.(:user , "-u, --user=<user>" , "user name") } @action.("connect to postgresql server") @optionset.(optset1, optset2) # !!!! def postgresql(host: nil, port: nil, user: nil) puts "psql ...." end @action.("connect to mysql server") @optionset.(optset1, optset2) # !!!! def mysql(host: nil, port: nil, user: nil) puts "mysql ...." end end exit Benry::CmdApp.main("Sample App")
Help message:
[bash]$ ruby ex11.rb -h postgresql # !!!! ex11.rb postgresql --- connect to postgresql Usage: $ ex11.rb postgresql [<options>] Options: -H, --host=<host> : host name # !!!! -p, --port=<port> : port number # !!!! -u, --user=<user> : user name # !!!! [bash]$ ruby ex11.rb -h mysql # !!!! ex11.rb mysql --- connect to mysql Usage: $ ex11.rb mysql [<options>] Options: -H, --host=<host> : host name # !!!! -p, --port=<port> : port number # !!!! -u, --user=<user> : user name # !!!!
Option set object has the following methods.
OptionSet#select(:key1, :key2, ...)
--- Creates new OptionSet object with copying options which are filtered by the keys specified.OptionSet#exclude(:key1, :key2, ...)
--- Creates new OptionSet object with copying options which are filtered by dropping the options that key is included in specified keys.
@action.("connect to postgresql server") @optionset.(optset1.select(:host, :port)) # !!!! def postgresql(host: nil, port: nil) .... end @action.("connect to mysql server") @optionset.(optset1.exclude(:port)) # !!!! def mysql(host: nil) .... end
Copy Options
@copy_options.()
copies options from other action.
File: ex12.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("connect to postgresql") @option.(:host , "-H, --host=<host>" , "host name") @option.(:port , "-p, --port=<port>" , "port number", type: Integer) @option.(:user , "-u, --user=<user>" , "user name") def postgresql(host: nil, port: nil, user: nil) puts "psql ...." end @action.("connect to mysql") @copy_options.("postgresql") # !!!!! def mysql(host: nil, port: nil, user: nil) puts "mysql ...." end end exit Benry::CmdApp.main("Sample App")
Help message:
[bash]$ ruby ex12.rb -h mysql # !!!! ex12.rb mysql --- connect to mysql Usage: $ ex12.rb mysql [<options>] Options: -H, --host=<host> : host name -p, --port=<port> : port number -u, --user=<user> : user name
If you want to exclude some options from copying, specify exlude:
keyword argument.
For example, @copy_options.("hello", exclude: [:help, :lang])
copies all options of hello
action excluding :help
and :lang
options.
Option Error and Action Error
option_error()
returns (not raise)Benry::CmdApp::OptionError
object.action_error()
returns (not raise)Benry::CmdApp::ActionError
object.- These are available in action method.
File: ex13.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("invoke openssl command") @option.(:encrypt, "--encrypt", "encrypt a file") @option.(:decrypt, "--decrypt", "decrypt a file") def openssl(filename, encrypt: false, decrypt: false) if encrypt == false && decrypt == false raise option_error("Required '--encrypt' or '--decrypt' option.") # !!!! end opt = encrypt ? "enc" : "dec" command = "openssl #{opt} ..." result = system command if result == false raise action_error("Command failed: #{command}") # !!!! end end end exit Benry::CmdApp.main("Sample App")
Output:
[bash]$ ruby ex13.rb openssl file.txt [ERROR] Required '--encrypt' or '--decrypt' option. #<== option_error() [bash]$ ruby ex13.rb openssl --encrypt file.txt enc: Use -help for summary. [ERROR] Command failed: openssl enc ... #<== action_error() From ex13.rb:17:in `openssl' raise action_error("Command failed: #{command}") From ex13.rb:25:in `<main>' exit app.main()
If you want to show all stacktrace, add --debug
global option.
[bash]$ ruby ex13.rb --debug openssl --encrypt file.txt enc: Use -help for summary. ex13.rb:17:in `openssl': Command failed: openssl enc ... (Benry::CmdApp::ActionError) from /home/yourname/cmdapp.rb:988:in `_invoke_action' from /home/yourname/cmdapp.rb:927:in `start_action' from /home/yourname/cmdapp.rb:1794:in `start_action' from /home/yourname/cmdapp.rb:1627:in `handle_action' from /home/yourname/cmdapp.rb:1599:in `run' from /home/yourname/cmdapp.rb:1571:in `main'
Advanced Feature
Category of Action
category "foo:bar:"
in action class adds prefixfoo:bar:
to each action name.- Category name should be specified as a prefix string ending with
:
. For example,category "foo:"
is OK butcategory "foo"
will be error. - Symbol is not allowed. For example,
category :foo
will be error. - Method name
def baz__test()
withcategory "foo:bar:"
results in the action namefoo:bar:baz:test
.
File: ex21.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action category "foo:bar:" # !!!! @action.("test action #1") def test1() # action name: 'foo:bar:test1' puts __method__ #=> test1 puts methods().grep(/test1/) #=> foo__bar__test1 end @action.("test action #2") def baz__test2() # action name: 'foo:bar:baz:test2' puts __method__ #=> baz__test2 puts methods().grep(/test2/) #=> foo__bar__baz__test2 end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex21.rb -l Actions: foo:bar:baz:test2 : test action #2 foo:bar:test1 : test action #1 help : print help message (of action if specified) [bash]$ ruby ex21.rb foo:bar:test1 test1 # <== puts __method__ foo__bar__test1 # <== puts methods().grep(/test1/) [bash]$ ruby ex21.rb foo:bar:baz:test2 baz__test2 # <== puts __method__ foo__bar__baz__test2 # <== puts methods().grep(/test1/)
(INTERNAL MECHANISM):
As shown in the above output, Benry-CmdApp internally renames test1()
and baz__test2()
methods within category foo:bar:
to foo__bar__test1()
and foo__bar__baz__test2()
respectively.
__method__
seems to keep original method name, but don't be fooled, methods are renamed indeed.
Due to this mechanism, it is possible to define the same name methods in different categories with no confliction.
category()
can take a description text of category. For example,category "foo:", "Bla bla"
registers"Bla bla
as a description of categoryfoo:
. Description of category is displayed in list of category list. See Action List and Prefix List section for details.
Nested Category
category()
can take a block which represents sub-category.
File: ex22.rb
# coding: utf-8 require 'benry/cmdapp' class GitAction < Benry::CmdApp::Action category "git:" # top level category @action.("show current status in compact format") def status(path=".") puts "git status -sb #{path}" end category "commit:" do # sub level category @action.("create a new commit") def create(message: nil) puts "git commit" end end category "branch:" do # sub level category @action.("create a new branch") def create(branch) puts "git checkout -b #{branch}" end end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex22.rb -l Actions: git:branch:create : create a new branch git:commit:create : create a new commit git:status : show current status in compact format help : print help message (of action if specified)
Block of category()
is nestable.
File: ex23.rb
# coding: utf-8 require 'benry/cmdapp' class GitAction < Benry::CmdApp::Action category "git:" do # top level category @action.("show current status in compact format") def status(path=".") puts "git status -sb #{path}" end category "commit:" do # sub level category @action.("create a new commit") def create(message: nil) puts "git commit" end end category "branch:" do # sub level category @action.("create a new branch") def create(branch) puts "git checkout -b #{branch}" end end end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex23.rb -l Actions: git:branch:create : create a new branch git:commit:create : create a new commit git:status : show current status in compact format help : print help message (of action if specified)
Category Action or Alias
category "foo:bar:", action: "blabla"
definesfoo:bar
action (instead offoo:bar:blabla
) withblabla()
method.
File: ex24.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action category "foo:bar:", action: "test3" # !!!! @action.("test action #1") def test1() # action name: 'foo:bar:test1' puts __method__ end @action.("test action #3") def test3() # action name: 'foo:bar' puts __method__ end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex24.rb -l Actions: foo:bar : test action #3 # !!!! not 'foo:bar:test3' foo:bar:test1 : test action #1 help : print help message (of action if specified) [bash]$ ruby ex24.rb foo:bar:test1 test1 [bash]$ ruby ex24.rb foo:bar:test3 # !!!! not available because renamed [ERROR] foo:bar:test3: Action not found. [bash]$ ruby ex24.rb foo:bar # !!!! available because renamed test3
- Action name (and also category name) should be specified as a string. Symbol is not allowed.
## Symbol is not allowed category :foo #=> error category "foo:", action: :blabla #=> error
Invoke Other Action
run_action()
invokes other action.run_once()
invokes other action only once. This is equivarent to 'prerequisite task' feature in task runner application.
File: ex25.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("create build dir") def prepare() puts "rm -rf build" puts "mkdir build" end @action.("build something") def build() run_once("prepare") # !!!! run_once("prepare") # skipped because already invoked puts "echo 'README' > build/README.txt" puts "zip -r build.zip build" end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex25.rb build rm -rf build # invoked only once!!!! mkdir build # invoked only once!!!! echo 'README' > build/README.txt zip -r build.zip build
- Action name should be a string. Symbol is not allowed.
## Error because action name is not a string. run_once(:prepare)
- When looped action is detected, Benry-CmdApp aborts action.
File: ex26.rb
require 'benry/cmdapp' class LoopedAction < Benry::CmdApp::Action @action.("test #1") def test1() run_once("test2") end @action.("test #2") def test2() run_once("test3") end @action.("test #3") def test3() run_once("test1") # !!!! end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex26.rb test1 [ERROR] test1: Looped action detected. [bash]$ ruby ex26.rb test3 [ERROR] test3: Looped action detected.
Cleaning Up Block
at_end { ... }
registers a clean-up block that is invoked at end of process (not at end of action).- This is very useful to register clean-up blocks in preparation action.
- Registered blocks are invoked in reverse order of registration.
For example,
at_end { puts "A" }; at_end { puts "B" }; at_end { puts "C" }
prints "C", "B", and "A" at end of process.
File: ex27.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("create build dir") def prepare() puts "mkdir -p build" ## register cleaning up block in preparation task at_end { puts "rm -rf build" } # !!!! end @action.("build something") def build() run_once("prepare") puts "echo 'README' > build/README.txt" puts "zip -r build.zip build" end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex27.rb build mkdir -p build echo 'README' > build/README.txt zip -r build.zip build rm -rf build # !!!! clean-up block invoked at the end of process !!!!
Alias for Action
Alias provides alternative short name of action.
define_alias()
in action class defines an alias with taking action category into account.Benry::CmdApp.define_alias()
defines an alias, without taking category into account.
File: ex28.rb
# coding: utf-8 require 'benry/cmdapp' class GitAction < Benry::CmdApp::Action category "git:" # !!!! @action.("show current status in compact mode") def status() puts "git status -sb" end define_alias "st", "status" # !!!! ## or: #Benry::CmdApp.define_alias "st", "git:status" # !!!! category "staging:" do # !!!! @action.("show changes in staging area") def show() puts "git diff --cached" end define_alias "staged" , "show" # !!!! ## or: #Benry::CmdApp.define_alias "staged", "git:staging:show" # !!!! end end Benry::CmdApp.define_alias "git", "git:status" # !!!! exit Benry::CmdApp.main("sample app")
Help message:
[bash]$ ruby ex28.rb -h ex28.rb --- sample app Usage: $ ex28.rb [<options>] <action> [<arguments>...] Options: -h, --help : print help message (of action if specified) -l, --list : list actions and aliases -a, --all : list hidden actions/options, too Actions: git : alias for 'git:status' # !!!! git:staging:show : show changes in staging area git:status : show current status in compact mode help : print help message (of action if specified) st : alias for 'git:status' # !!!! staged : alias for 'git:staging:show' # !!!!
Output:
[bash]$ ruby ex28.rb st # alias name git status -sb [bash]$ ruby ex28.rb git:status # original action name git status -sb [bash]$ ruby ex28.rb staged # alias name git diff --cached [bash]$ ruby ex28.rb git:staging:show # original action name git diff --cached [bash]$ ruby ex28.rb git # alias name git status -sb
- Aliases are printed in the help message of action (if defined).
[bash]$ ruby ex28.rb git:status -h ex28.rb git:status --- show current status in compact mode Usage: $ ex28.rb git:status Aliases: # !!!! git : alias for 'git:status' # !!!! st : alias for 'git:status' # !!!!
- Both alias and action names should be string. Symbol is not allowed.
## Error because alias name is a Symbol. Benry::CmdApp.define_alias :test, "hello" ## Error because action name is a Symbol. Benry::CmdApp.define_alias "test", :hello
- Target action (second argument of `define_alias()`) can be an array of string which contains action name and options.
File: ex29.rb
# coding: utf-8 require 'benry/cmdapp' class MyAction < Benry::CmdApp::Action @action.("print greeting message") @option.(:lang, "-l, --lang=<lang>", "language", enum: ["en", "fr", "it"]) def hello(user="world", lang: "en") case lang when "en" ; puts "Hello, #{user}!" when "fr" ; puts "Bonjour, #{user}!" when "it" ; puts "Ciao, #{user}!" else raise "#{lang}: Unknown language." end end end Benry::CmdApp.define_alias("bonjour", ["hello", "--lang=fr"]) # !!!! Benry::CmdApp.define_alias("ciao" , ["hello", "-l", "it", "Bob"]) # !!!! exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex29.rb hello Hello, world! [bash]$ ruby ex29.rb bonjour # !!!! Bonjour, world! [bash]$ ruby ex29.rb bonjour Alice # !!!! Bonjour, Alice! [bash]$ ruby ex29.rb ciao # !!!! Ciao, Bob!
- It is not allowed to define an alias for other alias.
## define an alias Benry::CmdApp.define_alias("hello-it" , ["hello", "-l", "it"]) ## ERROR: define an alias for other alias Benry::CmdApp.define_alias("ciao" , "hello-it") # !!!!
- Global option
-L alias
lists all aliases. This option is hidden in default, therefore not shown in help message but available in default (for debug purpose).
[bash]$ ruby ex30.rb -L alias Aliases: git : alias for 'git:status'
Abbreviation of Category
Abbreviation of category is a shortcut of category prefix.
For example, when b:
is an abbreviation of a category prefix git:branch:
, you can invoke git:branch:create
action by b:create
.
File: ex31.rb
# coding: utf-8 require 'benry/cmdapp' class GitAction < Benry::CmdApp::Action category "git:" do category "branch:" do @action.("create a new branch") def create(branch) puts "git checkout -b #{branch}" end end end end ## define abbreviation 'b:' of category prefix 'git:branch:' Benry::CmdApp.define_abbrev("b:", "git:branch:") # !!!! exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex31.rb b:create topic1 # invokes 'git:branch:create' !!!! git checkout -b topic1
Global option -L abbrev
lists all abbreviations.
This option is hidden in default, therefore not shown in help message but available in default (for debug purpose).
[bash]$ ruby ex31.rb -L abbrev Abbreviations: b: => git:branch:
Default Action
config.default_action = "test1"
defines default action. In this case, actiontest1
will be invoked if action name not specified in command-line.- Default action name is shown in help message.
File: ex32.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("test action #1") def test1() puts __method__ end end exit Benry::CmdApp.main("sample app", "1.0.0", default_action: "test1") # !!!! ## or: #config = Benry::CmdApp::Config.new("sample app", "1.0.0") #config.default_action = "test1" # !!!! #app = Benry::CmdApp::Application.new(config) #exit app.main()
Output:
[bash]$ ruby ex32.rb test1 test1 [bash]$ ruby ex32.rb # no action name!!!! test1
Help message:
[bash]$ ruby ex32.rb -h ex32.rb --- sample app Usage: $ ex32.rb [<options>] <action> [<arguments>...] Options: -h, --help : print help message (of action if specified) -l, --list : list actions and aliases -a, --all : list hidden actions/options, too Actions: (default: test1) # !!!! help : print help message (of action if specified) test1 : test action #1
Action List and Category List
When config.default_action
is not specified, Benry-CmdAction lists action names if action name is not specified in command-line.
File: ex33.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("test action #1") def test1() end category "foo:" do @action.("test action #2") def test2() end end category "bar:" do @action.("test action #3") def test3() end category "baz:" do @action.("test action #4") def test4() end end end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex33.rb # no action name!!!! Actions: bar:baz:test4 : test action #4 bar:test3 : test action #3 foo:test2 : test action #2 help : print help message (of action if specified) test1 : test action #1
Command-line option -l, --list
also prints the same result of the above example.
This is useful if you specify default action name wit config.default_action
.
Action name list contains alias names, too.
If you want to list only action names (or alias names), specify -L action
or -L alias
option.
See Q: How to list only aliases (or actions) excluding actions (or aliases) ? for details.
If category prefix (such as xxx:
) is specified instead of action name,
Benry-CmdApp lists action names which have that category prefix.
Output:
[bash]$ ruby ex33.rb foo: # !!!! Actions: foo:test2 : test action #2 [bash]$ ruby ex33.rb bar: # !!!! Actions: bar:baz:test4 : test action #4 bar:test3 : test action #3
If :
is specified instead of action name, Benry-CmdApp lists top-level category prefixes of action names and number of actions under the each category prefix.
Outuput:
[bash]$ ruby ex33.rb : # !!!! Categories: (depth=1) bar: (2) # !!! two actions ('bar:test3' and 'bar:baz:test4') foo: (1) # !!! one action ('foo:text2')
In the above example, only top-level category prefixes are displayed.
If you specified ::
instead of :
, second-level category prefixes are displayed,
for example foo:xxx:
and foo:yyy:
.
Of course, :::
displays more level category prefixes.
File: ex34.rb
# coding: utf-8 require 'benry/cmdapp' class GitAction < Benry::CmdApp::Action category "git:" category "staging:" do @action.("..."); def add(); end @action.("..."); def show(); end @action.("..."); def delete(); end end category "branch:" do @action.("..."); def list(); end @action.("..."); def switch(name); end end category "repo:" do @action.("..."); def create(); end @action.("..."); def init(); end category "config:" do @action.("..."); def add(); end @action.("..."); def delete(); end @action.("..."); def list(); end end category "remote:" do @action.("..."); def list(); end @action.("..."); def set(); end end end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex34.rb : Categories: (depth=1) git: (12) [bash]$ ruby ex34.rb :: # !!!! Categories: (depth=2) git: (0) git:branch: (2) git:repo: (7) git:staging: (3) [bash]$ ruby ex34.rb ::: # !!!! Categories: (depth=3) git: (0) git:branch: (2) git:repo: (2) git:repo:config: (3) git:repo:remote: (2) git:staging: (3)
category()
can take a description of category as second argument.
Descriptions of category are displayed in the category prefix list.
File: ex35.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action category "foo:", "description of Foo" do @action.("test action #2") def test2() end end category "bar:", "description of Bar" do @action.("test action #3") def test3() end category "baz:", "description fo Baz" do @action.("test action #4") def test4() end end end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex35.rb : # !!!! Categories: (depth=1) bar: (2) : description of Bar # !!!! foo: (1) : description of Foo # !!!!
Important Actions or Options
It is possible to mark actions or options as important or not.
- Actions or options marked as important are emphasized in help message.
- Actions or options marked as not important are weaken in help message.
File: ex38.rb
require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("important action", important: true) # !!!! def test1() end @action.("not important action", important: false) # !!!! def test2() end @action.("sample") @option.(:foo, "--foo", "important option", important: true) @option.(:bar, "--bar", "not important option", important: false) def test3(foo: nil, bar: nil) end end exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex38.rb -l Actions: help : print help message (of action if specified) test1 : important action # !!!! bold font !!!! test2 : not important action # !!!! gray color !!!! test3 : sample [bash]$ ruby ex38.rb -h test3 ex38.rb test3 --- sample Usage: $ ex38.rb test3 [<options>] Options: --foo : important option # !!!! bold font !!!! --bar : not important option # !!!! gray color !!!!
Multiple Option
If you need multiple options like -I
option of Ruby,
pass multiple: true
to @option.()
.
File: ex39.rb
require 'benry/cmdapp' class TestAction < Benry::CmdApp::Action @action.("multiple option test") @option.(:path, "-I <path>", "path", multiple: true) def test_(path: []) puts "path=#{path.inspect}" #=> path=["/tmp", "/var/tmp"] end end exit Benry::CmdApp.main("test app")
Output:
[bash]$ ruby ex39.rb test -I /tmp -I /var/tmp # !!!! path=["/tmp", "/var/tmp"] # !!!!
Configuratoin and Customization
Application Configuration
Benry::CmdApp::Config
class configures application behaviour.
config.app_desc = "..."
sets command description which is shown in help message. (required)config.app_version = "1.0.0"
enables-V
and--version
option, and prints version number if-V
or--version
option specified. (default:nil
)config.app_command = "<command>"
sets command name which is shown in help message. (default:File.basname($0)
)config.app_name = "<string>"
sets application name which is shown in help message. (default: same asconfig.app_command
)config.app_usage = "<text>" (or
["<text1>", "<text2>", ...]) sets usage string in help message. (default:
" <action> [<arguments>...]"``)config.app_detail = "<text>"
sets detailed description of command which is showin in help message. (default:nil
)config.backtrace_ignore_rexp = /.../
sets regular expression to ignore backtrace when error raised. (default:nil
)config.help_description = "<text>"
sets text of 'Description:' section in help message. (default:nil
)config.help_postamble = {"<Title>:" => "<text>"}
sets postamble of help message, such as 'Example:' or 'Tips:'. (default:nil
)config.default_action = "<action>"
sets default action name. (default:nil
)config.option_help = true
enables-h
and--help
options. (default:true
)config.option_version = true
enables-V
and--version
options. (default:true
ifapp_version
provided,false
if else)config.option_list = true
enables-l
and--list
options. (default:true
)config.option_topic = true
enables-L <topic>
option. (default::hidden
)config.option_all = true
enables-a
and--all
options which shows private (hidden) actions and options into help message. (default:true
)config.option_verbose = true
enables-v
and--verbose
options which sets$QUIET_MODE = false
. (default:false
)config.option_quiet = true
enables-q
and--quiet
options which sets$QUIET_MODE = true
. (default:false
)config.option_color = true
enables--color[=<on|off>]
option which sets$COLOR_MODE = true/false
. This affects to help message colorized or not. (default:false
)config.option_debug = true
enables-D
and--debug
options which sets$DEBUG_MODE = true
. (default::hidden
)config.option_trace = true
enables-T
and--trace
options. Entering into and exitting from action are reported when trace mode is on. (default:false
)config.option_dryrun = true
enables-X
and--dryrun
options which sets$DRYRUN_MODE = true
. (default:false
)config.format_option = " %-18s : %s"
sets format of options in help message. (default:" %-18s : %s"
)config.format_action = " %-18s : %s"
sets format of actions in help message. (default:" %-18s : %s"
)config.format_usage = " $ %s"
sets format of usage in help message. (default:" $ %s"
)config.format_avvrev = " %-10s => %s"
sets format of abbreviations in output of-L abbrev
option. (default:" %-10s => %s"
)config.format_usage = " $ %s"
sets format of usage in help message. (default:" $ %s"
)config.format_category = " $-18s : %s""
sets format of category prefixes in output of-L category
option. (default:nil
which means to use value ofconfig.format_action
)
File: ex41.rb
# coding: utf-8 require 'benry/cmdapp' config = Benry::CmdApp::Config.new("sample app", "1.0.0", app_name: "Sample App") config.each(sort: false) do |name, val| puts "config.%-20s = %s" % [name, val.inspect] end
Output:
[bash]$ ruby ex41.rb config.app_desc = "sample app" config.app_version = "1.0.0" config.app_name = "Sample App" config.app_command = "ex41.rb" # == File.basename($0) config.app_usage = nil config.app_detail = nil config.default_action = nil config.help_description = nil config.help_postamble = nil config.format_option = " %-18s : %s" config.format_action = " %-18s : %s" config.format_usage = " $ %s" config.format_category = nil config.deco_command = "\e[1m%s\e[0m" # bold config.deco_header = "\e[1;34m%s\e[0m" # bold, blue config.deco_extra = "\e[2m%s\e[0m" # gray color config.deco_strong = "\e[1m%s\e[0m" # bold config.deco_weak = "\e[2m%s\e[0m" # gray color config.deco_hidden = "\e[2m%s\e[0m" # gray color config.deco_debug = "\e[2m%s\e[0m" # gray color config.deco_error = "\e[31m%s\e[0m" # red config.option_help = true config.option_version = true config.option_list = true config.option_topic = :hidden config.option_all = true config.option_verbose = false config.option_quiet = false config.option_color = false config.option_debug = :hidden config.option_trace = false config.option_dryrun = false config.backtrace_ignore_rexp = nil config.trace_mode = nil
You may notice that the value of config.option_debug
is :hidden
.
If value of config.option_xxxx
is :hidden
, then corresponding global option is enabled as hidden option.
Therefore you can see --debug
option in help message if you add -h
and -a
(or --all
) option.
Help message:
$ ruby ex37.rb -h -a # !!!! ex37.rb --- sample app Usage: $ ex37.rb [<options>] <action> [<arguments>...] Options: -h, --help : print help message (of action if specified) -l, --list : list actions and aliases -a, --all : list hidden actions/options, too --debug : debug mode # !!!! Actions: help : print help message (of action if specified) test1 : test action
Customization of Global Options
To add custom global options:
- (1) Create a global option schema object.
- (2) Add custom options to it.
- (3) Pass it to
Application.new()
.
File: ex42.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("test action") def test1() puts __method__ end end ## (1) create global option shema config = Benry::CmdApp::Config.new("sample app") schema = Benry::CmdApp::GlobalOptionSchema.new(config) # !!!! ## (2) add custom options to it schema.add(:logging, "--logging", "enable logging") # !!!! ## (3) pass it to ``Application.new()`` app = Benry::CmdApp::Application.new(config, schema) # !!!! exit app.main()
Help message:
[bash]$ ruby ex42.rb -h ex42.rb --- sample app Usage: $ ex42.rb [<options>] <action> [<arguments>...] Options: -h, --help : print help message (of action if specified) -l, --list : list actions and aliases -a, --all : list hidden actions/options, too --logging : enable logging # !!!! Actions: help : print help message (of action if specified) test1 : test action
To customize global options entirely:
- (1) Create empty
GlobalOptionSchema
object. - (2) Add global options as you want.
- (3) Create and execute Application object with it.
File: ex43.rb
# coding: utf-8 require 'benry/cmdapp' ## (1) Create empty ``GlobalOptionSchema`` object. schema = Benry::CmdApp::GlobalOptionSchema.new(nil) # !!!! ## (2) Add global options as you want. schema.add(:help , "-h, --help" , "print help message") schema.add(:version, "-V, --version", "print version") schema.add(:list , "-l, --list" , "list actions and aliases") schema.add(:all , "-a, --all" , "list hidden actions/options, too") schema.add(:verbose, "-v, --verbose", "verbose mode") schema.add(:quiet , "-q, --quiet" , "quiet mode") schema.add(:color , "--color[=<on|off>]", "enable/disable color mode", type: TrueClass) schema.add(:debug , "-D, --debug" , "set $DEBUG_MODE to true") schema.add(:trace , "-T, --trace" , "report enter into and exit from action") ## (3) Create and execute Application object with it. config = Benry::CmdApp::Config.new("sample app", "1.0.0") app = Benry::CmdApp::Application.new(config, schema) # !!!! exit app.main()
Help message:
[bash]$ ruby ex43.rb -h ex43.rb (1.0.0) --- sample app Usage: $ ex43.rb [<options>] <action> [<arguments>...] Options: -h, --help : print help message -V, --version : print version -l, --list : list actions and aliases -a, --all : list hidden actions/options, too -v, --verbose : verbose mode -q, --quiet : quiet mode --color[=<on|off>] : enable/disable color mode -D, --debug : set $DEBUG_MODE to true -T, --trace : report enter into and exit from action Actions: help : print help message (of action if specified)
Customization of Global Option Behaviour
- (1) Define subclass of
Application
class. - (2) Override
#toggle_global_options()
method. - (3) Create and execute subclass object of
Application
.
File: ex44.rb
# coding: utf-8 require 'benry/cmdapp' ## (1) Define subclass of ``Application`` class. class MyApplication < Benry::CmdApp::Application ## (2) Override ``#toggle_global_options()`` method. def toggle_global_options(global_opts) status_code = super return status_code if status_code # `return 0` means "stop process successfully", # `return 1` means "stop process as failed". if global_opts[:logging] require 'logger' $logger = Logger.new(STDOUT) end return nil # `return nil` means "continue process". end end ## (3) Create and execute subclass object of ``Application``. config = Benry::CmdApp::Config.new("sample app") app = MyApplication.new(config) # !!!! exit app.main()
Of course, prepending custom module to Application class is also effective way.
File: ex45.rb
# coding: utf-8 require 'benry/cmdapp' module MyApplicationMod def toggle_global_options(global_opts) # .... end end Benry::CmdApp::Application.prepend(MyApplicationMod) # !!!! config = Benry::CmdApp::Config.new("sample app") app = Benry::CmdApp::Application.new(config) exit app.main()
Custom Hook of Application
- (1) Define subclass of Application class.
- (2) Override
#handle_action()
method. - (3) Create and execute custom application object.
File: ex46.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("test action") def test1() $logger.info("logging message") if $logger end end ## (1) Define subclass of Application class class MyApplication < Benry::CmdApp::Application # !!!! ## (2) Override method def handle_action(action, args) # !!!! #p @config $logger.debug("action=#{action}, args=#{args.inspect}") if $logger super # !!!! end end ## (3) create and execute custom application object config = Benry::CmdApp::Config.new("sample app") schema = Benry::CmdApp::GlobalOptionSchema.new(config) schema.add(:logging, "--logging", "enable logging") app = MyApplication.new(config, schema) # !!!! exit app.main()
Customization of Application Help Message
If you want to just add more text into application help message, set the followings:
.* config.app_detail = <text>
--- print text before 'Usage:' section.
.* config.help_description = <text>
--- print text after 'Usage:' section as 'Description:' section.
.* config.help_postamble = {<head> => <text>}
--- print at end of help message.
File: ex47.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("test action #1") def hello(user="world") puts "Hello, #{user}!" end end config = Benry::CmdApp::Config.new("sample app", "1.0.0") config.app_detail = "See https://...." # !!!! config.help_description = " Bla bla bla" # !!!! config.help_postamble = [ # !!!! {"Example:" => " $ <command> hello Alice\n"}, # !!!! "(Tips: ....)", # !!!! ] # !!!! app = Benry::CmdApp::Application.new(config) exit app.main()
Help message:
[bash]$ ruby ex47.rb -h ex47.rb --- sample app See https://.... # !!!! Usage: $ ex47.rb [<options>] <action> [<arguments>...] Description: # !!!! Bla bla bla # !!!! Options: -h, --help : print help message (of action if specified) -l, --list : list actions and aliases -a, --all : list hidden actions/options, too Actions: hello : test action #1 Example: # !!!! $ <command> hello Alice # !!!! (Tips: ....) # !!!!
If you want to change behaviour of building command help message:
- (1) Define subclass of
Benry::CmdApp::ApplicationHelpBuilder
class. - (2) Override methods.
- (3) Create an instance object of the class.
- (4) Pass it to Application object.
File: ex48.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("print greeting message") def hello(user="world") puts "Hello, #{user}!" end end ## (1) Define subclass of ``Benry::CmdApp::ApplicationHelpBuilder`` class. class MyAppHelpBuilder < Benry::CmdApp::ApplicationHelpBuilder ## (2) Override methods. def build_help_message(gschema, all: false) super end def section_preamble() super end def section_usage() super end def section_options(global_opts_schema, all: false) super end def section_actions(include_aliases=true, all: false) super end def section_postamble() super end ### optional (for `-L <topic>` option) #def section_candidates(prefix, all: false); super; end #def section_aliases(all: false); super; end #def section_abbrevs(all: false); super; end #def section_categories(depth=0, all: false); super; end end ## (3) Create an instance object of the class. config = Benry::CmdApp::Config.new("sample app") schema = Benry::CmdApp::GlobalOptionSchema.new(config) schema.add(:logging, "--logging", "enable logging") app_help_builder = MyAppHelpBuilder.new(config) # !!!! ## (4) Pass it to Application object. app = Benry::CmdApp::Application.new(config, schema, app_help_builder) # !!!! exit app.main()
More simple way:
- (1) Create a module and override methods of
Benry::CmdApp::ApplicationHelpBuilder
class. - (2) Prepend it to
Benry::CmdApp::ApplicationHelpBuilder
class. - (3) Create and execute Application object.
File: ex49.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("print greeting message") def hello(user="world") puts "Hello, #{user}!" end end ## (1) Create a module and override methods of ``ApplicationHelpBuilder`` class. module MyHelpBuilderMod def build_help_message(gschema, all: false) super end def section_preamble() super end def section_usage() super end def section_options(global_opts_schema, all: false) super end def section_actions(include_aliases=true, all: false) super end def section_postamble() super end ### optional (for `-L <topic>` option) #def section_candidates(prefix, all: false); super; end #def section_aliases(all: false); super; end #def section_abbrevs(all: false); super; end #def section_categories(depth=0, all: false); super; end end ## (2) Prepend it to ``Benry::CmdApp::ApplicationHelpBuilder`` class. Benry::CmdApp::ApplicationHelpBuilder.prepend(MyHelpBuilderMod) ## (3) Run application. exit Benry::CmdApp.main("sample app")
Customization of Action Help Message
If you want to just add more text into action help message,
pass the following keyword arguments to @action.()
.
detail: <text>
--- printed before 'Usage:' section.description: <text>
--- printed after 'Usage:' section as 'Description:' section, likeman
command in UNIX.postamble: {<header> => <text>}
--- printed at end of help message as a dedicated section.
File: ex50.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("test action #1", detail: "See https://....", # !!!! description: " Bla bla bla", # !!!! postamble: {"Example:" => " ...."}) # !!!! def hello(user="world") puts "Hello, #{user}!" end end exit Benry::CmdApp.main("sample app")
Help message:
[bash]$ ruby ex50.rb -h hello ex50.rb hello --- test action #1 See https://.... # !!!! Usage: $ ex50.rb hello [<user>] Description: # !!!! Bla bla bla # !!!! Example: .... # !!!!
If you want to change behaviour of building action help message:
- (1) Define subclass of
ActionHelpBuilder
class. - (2) Override methods.
- (3) Create an instance object of the class.
- (4) Pass it to Application object.
File: ex51.rb
require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("print greeting message") def hello(user="world") puts "Hello, #{user}!" end end ## (1) Define subclass of ``ActionHelpBuilder`` class. class MyActionHelpBuilder < Benry::CmdApp::ActionHelpBuilder ## (2) Override methods. def build_help_message(metadata, all: false) super end def section_preamble(metadata) super end def section_usage(metadata, all: false) super end def section_description(metadata) super end def section_options(metadata, all: false) super end def section_postamble(metadata) super end end ## (3) Create an instance object of the class. config = Benry::CmdApp::Config.new("sample app") action_help_builder = MyActionHelpBuilder.new(config) ## (4) Pass it to Application object. schema = Benry::CmdApp::GlobalOptionSchema.new(config) app = Benry::CmdApp::Application.new(config, schema, nil, action_help_builder) exit app.main()
Another way:
- (1) Create a module and override methods of
Benry::CmdApp::ActionHelpBuilder
class. - (2) Prepend it to
Benry::CmdApp::ActionHelpBuilder
class. - (3) Run application.
File: ex52.rb
# coding: utf-8 require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("print greeting message") def hello(user="world") puts "Hello, #{user}!" end end ## (1) Create a module and override methods of ``ActionHelpBuilder`` class. module MyActionHelpBuilderMod def build_help_message(metadata, all: false) super end def section_preamble(metadata) super end def section_usage(metadata, all: false) super end def section_description(metadata) super end def section_options(metadata, all: false) super end def section_postamble(metadata) super end end ## (2) Prepend it to ``Benry::CmdApp::ActionHelpBuilder`` class. Benry::CmdApp::ActionHelpBuilder.prepend(MyActionHelpBuilderMod) # !!!! ## (3) Run application. exit Benry::CmdApp::main("sample app")
Customization of Section Title in Help Message
If you want to change section titles such as 'Options:' or 'Actions:' in the help message, override the constants representing section titles.
The following constants are defined in BaseHelperBuilder
class.
module Benry::CmdApp class BaseHelpBuilder HEADER_USAGE = "Usage:" HEADER_DESCRIPTION = "Description:" HEADER_OPTIONS = "Options:" HEADER_ACTIONS = "Actions:" HEADER_ALIASES = "Aliases:" HEADER_ABBREVS = "Abbreviations:" HEADER_CATEGORIES = "Categories:"
You can override them in ApplicationHelpBuilder
or ActionHelpBuilder
classes which are subclass of BaseHandlerBuilder
class.
## for example Benry::CmdApp::ApplicationHelpBuilder::HEADER_ACTIONS = "ACTIONS:" Benry::CmdApp::ActionHelpBuilder::HEADER_OPTIONS = "OPTIONS:"
If you want to change just decoration of section titles,
set config.deco_header
.
config = Benry::CmdApp::Config.new("Test App", "1.0.0") config.deco_header = "\e[1;34m%s\e[0m" # bold, blue #config.deco_header = "\e[1;4m%s\e[0m" # bold, underline
Q & A
Q: How to show all backtraces of exception?
A: Add --deubg
option.
Benry-CmdApp catches exceptions and handles their backtrace
automatically in default, but doesn't catch them when --debug
option is specified.
Q: How to specify description to arguments of actions?
A: Can't. It is possible to specify description to actions or options, but not possible to arguments of actions.
Q: How to append some tasks to an existing action?
A: (a) Use method alias, or (b) use prepend.
File: ex61.rb
require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("test action #1") def hello(user="world") puts "Hello, #{user}!" end @action.("test action #2") def hi(user="world") puts "Hi, #{user}!" end end ## (a) use method alias class SampleAction # open existing class alias __old_hello hello # alias for existing method def hello(user="world") # override existing method puts "---- >8 ---- >8 ----" __old_hello(user) # call original method puts "---- 8< ---- 8< ----" end end ## (b) use prepend module SampleMod # define new module def hi(user="world") # override existing method puts "~~~~ >8 ~~~~ >8 ~~~~" super # call original method puts "~~~~ 8< ~~~~ 8< ~~~~" end end SampleAction.prepend(SampleMod) # prepend it to existing class exit Benry::CmdApp.main("sample app")
Output:
[bash]$ ruby ex61.rb hello ---- >8 ---- >8 ---- Hello, world! ---- 8< ---- 8< ---- [bash]$ ruby ex61.rb hi Alice ~~~~ >8 ~~~~ >8 ~~~~ Hi, Alice! ~~~~ 8< ~~~~ 8< ~~~~
Q: How to delete an existing action/alias?
A: Call Benry::CmdApp.undef_action("<action>")
or Benry::CmdApp.undef_alias("<alias>")
.
Q: How to re-define an existing action?
A: First remove the existing action, then re-define the action.
File: ex62.rb
require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("sample action") def hello() # !!!! puts "Hello, world!" end end Benry::CmdApp.undef_action("hello") # !!!! class OtherAction < Benry::CmdApp::Action @action.("other action") # !!!! def hello() # !!!! puts "Ciao, world!" end end exit Benry::CmdApp.main("sample app")
Help message:
[bash]$ ruby ex62.rb -h ex62.rb --- sample app Usage: $ ex62.rb [<options>] <action> [<arguments>...] Options: -h, --help : print help message (of action if specified) -l, --list : list actions and aliases -a, --all : list hidden actions/options, too Actions: hello : other action # !!!! help : print help message (of action if specified)
Q: How to show entering into or exitting from actions?
A: Set config.option_trace = true
and pass -T
(or --trace
) option.
File: ex63.rb
require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("preparation") def prepare() puts "... prepare something ..." end @action.("build") def build() run_once("prepare") puts "... build something ..." end end exit Benry::CmdApp.main("sample app", "1.0.0", option_trace: true) # !!!! (or `:hidden`)
Output:
[bash]$ ruby ex63.rb --trace build # !!!! ### enter: build ### enter: prepare ... prepare something ... ### exit: prepare ... build something ... ### exit: build
Q: How to enable/disable color mode?
A: Set config.option_color = true
and pass --color=on
or --color=off
option.
File: ex64.rb
require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("print greeting message") def hello(user="world") puts "Hello, #{user}!" end end exit Benry::CmdApp.main("sample app", option_color: true) # !!!!
Help message:
[bash]$ ruby ex64.rb -h ex64.rb --- sample app Usage: $ ex64.rb [<options>] <action> [<arguments>...] Options: -h, --help : print help message (of action if specified) -l, --list : list actions and aliases -a, --all : list hidden actions/options, too --color[=<on|off>] : enable/disable color # !!!! Actions: hello : print greeting message [bash]$ ruby ex64.rb -h --color=off # !!!! [bash]$ ruby ex64.rb -h --color=on # !!!! [bash]$ ruby ex64.rb -h --color # !!!!
Q: How to define -vvv
style option?
A: Provide block parameter on @option.()
.
File: ex65.rb
require 'benry/cmdapp' class TestAction < Benry::CmdApp::Action @action.("set verbose level") @option.(:verbose, "-v", "verbose level") {|opts, key, val| # !!!! opts[key] ||= 0 # !!!! opts[key] += 1 # !!!! } # !!!! def test_(verbose: 0) puts "verbose=#{verbose}" end end exit Benry::CmdApp.main("test app")
Output:
[bash]$ ruby ex65.rb test -v # !!!! verbose=1 [bash]$ ruby ex65.rb test -vv # !!!! verbose=2 [bash]$ ruby ex65.rb test -vvv # !!!! verbose=3
Q: How to show global option -L <topic>
in help message?
A: Set config.option_topic = true
(default: :hidden
).
Q: How to specify detailed description of options?
A: Add detail:
keyword argument to @option.()
.
File: ex66.rb
require 'benry/cmdapp' class TestAction < Benry::CmdApp::Action @action.("detailed description test") @option.(:mode, "-m <mode>", "output mode", detail: <<"END") v, verbose: print many output q, quiet: print litte output c, compact: print summary output END def test_(mode: nil) puts "mode=#{mode.inspect}" end end exit Benry::CmdApp.main("test app")
Help message:
[bash]$ ruby ex66.rb -h test ex66.rb test --- detailed description test Usage: $ ex66.rb test [<options>] Options: -m <mode> : output mode v, verbose: print many output q, quiet: print litte output c, compact: print summary output
Q: How to list only aliases (or actions) excluding actions (or aliases) ?
A: Specify global option -L alias
or -L action
.
[bash]$ ruby gitexample.rb -l Actions: git : alias for 'git:status' git:stage : put changes of files into staging area git:staged : show changes in staging area git:status : show status in compact format git:unstage : remove changes from staging area stage : alias for 'git:stage' staged : alias for 'git:staged' unstage : alias for 'git:unstage' ### list only aliases (ordered by action name automatically) [bash]$ ruby gitexample.rb -L alias # !!!! Aliases: stage : alias for 'git:stage' staged : alias for 'git:staged' git : alias for 'git:status' unstage : alias for 'git:unstage' ### list only actions [bash]$ ruby gitexample.rb -L action # !!!! Actions: git:stage : put changes of files into staging area git:staged : show changes in staging area git:status : show status in compact format git:unstage : remove changes from staging area
Notice that -L alias
sorts aliases by action names.
This is the intended behaviour.
Q: How to change the order of options in help message?
A: Call GlobalOptionSchema#reorder_options()
.
File: ex68.rb
require 'benry/cmdapp' config = Benry::CmdApp::Config.new("sample app", "1.0.0", option_verbose: true, option_quiet: true, option_color: true, ) schema = Benry::CmdApp::GlobalOptionSchema.new(config) keys = [:verbose, :quiet, :color, :help, :version, :all, :target, :list] # !!!! schema.reorder_options(*keys) # !!!! app = Benry::CmdApp::Application.new(config, schema) ## or: #app = Benry::CmdApp::Application.new(config) #app.schema.reorder_options(*keys) # !!!! exit app.main()
Help message:
[bash]$ ruby ex68.rb -h ex68.rb (1.0.0) --- sample app Usage: $ ex68.rb [<options>] <action> [<arguments>...] Options: -v, --verbose : verbose mode -q, --quiet : quiet mode --color[=<on|off>] : color mode -h, --help : print help message (of action if specified) -V, --version : print version -a, --all : list hidden actions/options, too -L <topic> : topic list (actions|aliases|categories|abbrevs) -l, --list : list actions and aliases Actions: help : print help message (of action if specified)
Q: How to add metadata to actions or options?
A: Pass tag:
keyword argument to @action.()
or @option.()
.
tag:
keyword argument accept any type of value such as symbol, string, array, and so on.- Currenty, Benry-CmdApp doesn't provide the good way to use it effectively. This feature may be used by command-line application or framework based on Benry-CmdApp.
File: ex69.rb
require 'benry/cmdapp' class SampleAction < Benry::CmdApp::Action @action.("print greeting message", tag: :important) # !!!! @option.(:repeat, "-r <N>", "repeat N times", tag: :important) # !!!! def hello(user="world", repeat: nil) (repeat || 1).times do puts "Hello, #{user}!" end end end exit Benry::CmdApp.main("sample app")
Q: How to remove common help option from all actions?
A: Clears Benry::CmdApp::ACTION_SHARED_OPTIONS
which is an array of option item.
File: ex70.rb
require 'benry/cmdapp' arr = Benry::CmdApp::ACTION_SHARED_OPTIONS arr.clear()
Q: Is it possible to show details of actions and aliases?
A: Try global option -L metadata
.
It prints detailed data of actions and aliases in YAML format.
Q: How to make error messages I18Ned?
A: Currently not supported. May be supported in a future release.
License and Copyright
- $License: MIT License $
- $Copyright: copyright(c) 2023 kwatch@gmail.com $