class Luban::CLI::Base

Constants

DefaultSummaryIndent
DefaultSummaryWidth
DefaultTitleIndent

Attributes

config_blk[R]
arguments[R]
commands[R]
default_argv[R]
description[R]
options[R]
parent[R]
prefix[R]
program_name[R]
result[R]
summary[R]
summary_indent[RW]
summary_width[RW]
title_indent[RW]
version[R]

Public Class Methods

configure(&blk) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 15
def configure(&blk); @config_blk = blk; end
inherited(subclass) click to toggle source
Calls superclass method
# File lib/luban/cli/base/dsl.rb, line 5
def inherited(subclass)
  super
  # Ensure configuration block from base class
  # got inherited to its subclasses
  blk = instance_variable_get('@config_blk')
  subclass.instance_variable_set('@config_blk', blk.clone) unless blk.nil?
end
new(parent, action_name, prefix: default_prefix, auto_help: true, &config_blk) click to toggle source
# File lib/luban/cli/base/core.rb, line 36
def initialize(parent, action_name, prefix: default_prefix, auto_help: true, &config_blk)
  @parent = parent
  @action_name = action_name
  @prefix = prefix
  @action_defined = false

  @program_name = default_program_name
  @options = {}
  @arguments = {}
  @summary = ''
  @description = ''
  @version = ''
  @default_argv = ARGV
  @result = { cmd: nil, argv: @default_argv, args: {}, opts: {} }
  @commands = {}

  @title_indent = DefaultTitleIndent
  @summary_width = DefaultSummaryWidth
  @summary_indent = DefaultSummaryIndent

  configure(&config_blk)
  setup_default_action unless @action_defined
  self.auto_help if auto_help
end

Protected Class Methods

define_class(class_name, base:, namespace:) click to toggle source
# File lib/luban/cli/base/core.rb, line 89
def self.define_class(class_name, base:, namespace:)
  mods = class_name.split('::')
  cmd_class = mods.pop
  mods.inject(namespace) do |ns, mod|
    ns.const_set(mod, Module.new) unless ns.const_defined?(mod, false)
    ns.const_get(mod, false)
  end.const_set(cmd_class, Class.new(base))
end

Public Instance Methods

action(method_name = nil, &blk) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 92
def action(method_name = nil, &blk)
  create_action(method_name, preserve_argv: true, &blk)
end
action!(method_name = nil, &blk) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 96
def action!(method_name = nil, &blk)
  create_action(method_name, preserve_argv: false, &blk)
end
action_method() click to toggle source
# File lib/luban/cli/base/core.rb, line 63
def action_method
  @action_method ||= "#{@prefix}#{@action_name.to_s.gsub(':', '_')}"
end
alter(&blk) click to toggle source
# File lib/luban/cli/base/core.rb, line 81
def alter(&blk)
  instance_eval(&blk)
  on_alter
  after_alter
end
argument(name, desc, **config, &blk) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 46
def argument(name, desc, **config, &blk)
  @arguments[name] = Argument.new(name, desc, **config, &blk)
end
auto_help(short: :h, desc: "Show this help message.", &blk)
Alias for: help
auto_help_command(**opts, &blk)
Alias for: help_command
command(cmd, base: Command, **opts, &blk) click to toggle source
# File lib/luban/cli/base/commands.rb, line 6
def command(cmd, base: Command, **opts, &blk)
  cmd = cmd.to_sym
  @commands[cmd] = command_class(cmd, base).new(self, cmd, **opts, &blk)
end
default_prefix() click to toggle source
# File lib/luban/cli/base/core.rb, line 61
def default_prefix; ''; end
default_program_name() click to toggle source
# File lib/luban/cli/base/core.rb, line 71
def default_program_name
  @default_program_name ||= File.basename($0, '.*')
end
desc(string) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 22
def desc(string)
  @summary = string.to_s unless string.nil?
end
has_command?(cmd) click to toggle source
# File lib/luban/cli/base/commands.rb, line 31
def has_command?(cmd)
  @commands.has_key?(cmd.to_sym)
end
has_commands?() click to toggle source
# File lib/luban/cli/base/commands.rb, line 35
def has_commands?
  !@commands.empty?
end
help(short: :h, desc: "Show this help message.", &blk) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 50
def help(short: :h, desc: "Show this help message.", &blk)
  switch :help, desc, short: short, &blk
end
Also aliased as: auto_help
help_command(**opts, &blk) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 65
def help_command(**opts, &blk)
  if block_given?
    command(**opts, &blk)
  else
    validator = method(:has_command?)
    command(:help, **opts) do
      desc "List all commands or help for one command"
      argument :command, "Command to help for", 
                type: :symbol, required: false,
               assure: validator
      action :show_help_for_command
    end
  end
end
Also aliased as: auto_help_command
list_commands() click to toggle source
# File lib/luban/cli/base/commands.rb, line 27
def list_commands
  @commands.keys
end
long_desc(string) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 26
def long_desc(string)
  @description = string.to_s unless string.nil?
end
option(name, desc, nullable: false, **config, &blk) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 30
def option(name, desc, nullable: false, **config, &blk)
  @options[name] = if nullable
                     NullableOption.new(name, desc, **config, &blk)
                   else
                     Option.new(name, desc, **config, &blk)
                   end
end
parse(argv=default_argv) click to toggle source
# File lib/luban/cli/base/parse.rb, line 4
def parse(argv=default_argv)
  argv = argv.dup
  parse!(argv)
end
parse!(argv=default_argv) click to toggle source
# File lib/luban/cli/base/parse.rb, line 9
def parse!(argv=default_argv)
  if commands.empty?
    parse_without_commands(argv)
  else
    parse_with_commands(argv)
  end
  update_result(argv)
end
parser() click to toggle source
# File lib/luban/cli/base/core.rb, line 67
def parser
  @parser ||= create_parser
end
program(name) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 18
def program(name)
  @program_name = name.to_s unless name.nil? 
end
reset() click to toggle source
# File lib/luban/cli/base/core.rb, line 75
def reset
  @options.each_value { |o| o.reset }
  @arguments.each_value { |a| a.reset }
  @result = { cmd: nil, argv: @default_argv, args: {}, opts: {} }
end
show_help() click to toggle source
# File lib/luban/cli/base/dsl.rb, line 55
def show_help; puts parser.help; end
show_help_for_command(args:, **params) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 57
def show_help_for_command(args:, **params)
  if args[:command].nil?
    show_help
  else
    commands[args[:command]].show_help
  end
end
show_version() click to toggle source
# File lib/luban/cli/base/dsl.rb, line 90
def show_version; puts parser.ver; end
switch(name, desc, negatable: false, **config, &blk) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 38
def switch(name, desc, negatable: false, **config, &blk)
  @options[name] = if negatable
                     NegatableSwitch.new(name, desc, **config, &blk)
                   else
                     Switch.new(name, desc, **config, &blk)
                   end
end
task(cmd, **opts, &blk) click to toggle source
# File lib/luban/cli/base/commands.rb, line 39
def task(cmd, **opts, &blk)
  command(cmd, **opts, &blk).tap do |t|
    add_common_task_options(t)
    if !t.summary.nil? and t.description.empty?
      t.long_desc "#{t.summary} in #{self.class.name}"
    end
  end
end
undef_command(cmd) click to toggle source
# File lib/luban/cli/base/commands.rb, line 11
def undef_command(cmd)
  c = @commands.delete(cmd.to_sym)
  if c.nil?
    raise RuntimeError, "Command #{cmd.inspect} is NOT defined."
  else
    undef_singleton_method(c.action_method)
  end
end
Also aliased as: undef_task
undef_task(cmd)
Alias for: undef_command
use_commands(module_name, **opts, &blk) click to toggle source
# File lib/luban/cli/base/commands.rb, line 20
def use_commands(module_name, **opts, &blk)
  module_class = Object.const_get(module_name.camelcase, false)
  module_class.constants(false).map { |c| module_class.const_get(c, false) }.each do |c|
    command(c.name.snakecase, base: c, **opts, &blk) if c < Command
  end
end

Protected Instance Methods

action_abort() click to toggle source
# File lib/luban/cli/base/core.rb, line 129
def action_abort
  name = @action_name
  action do
    abort "Aborted! Action is NOT defined for #{name} in #{self.class.name}."
  end
end
action_noops() click to toggle source
# File lib/luban/cli/base/core.rb, line 125
def action_noops
  action { } # NOOPS
end
add_common_task_options(task) click to toggle source
# File lib/luban/cli/base/commands.rb, line 73
def add_common_task_options(task)
  if @parent != self and @parent.respond_to?(__method__, true)
    @parent.send(__method__, task)
  end
end
add_parser_arguments() click to toggle source
# File lib/luban/cli/base/core.rb, line 251
def add_parser_arguments
  add_section("Arguments") do |rows|
    @arguments.each_value do |arg|
      rows.concat(summarize(arg.display_name, arg.description))
    end
  end
end
add_parser_commands() click to toggle source
# File lib/luban/cli/base/core.rb, line 271
def add_parser_commands
  add_section("Commands") do |rows|
    commands.each_value do |cmd|
      rows.concat(summarize(cmd.name, cmd.summary, summary_width, summary_width * 1.5))
    end
  end
end
add_parser_defaults() click to toggle source
# File lib/luban/cli/base/core.rb, line 267
def add_parser_defaults
  add_section("Defaults", options.values.map(&:default_str).reject { |d| d.empty? })
end
add_parser_description() click to toggle source
# File lib/luban/cli/base/core.rb, line 263
def add_parser_description
  add_section("Description", wrap(description, summary_width * 2))
end
add_parser_options() click to toggle source
# File lib/luban/cli/base/core.rb, line 243
def add_parser_options
  add_section("Options") do
    @options.each_value do |option|
      parser.on(*option.specs) { |v| option.value = v }
    end
  end
end
add_parser_summary() click to toggle source
# File lib/luban/cli/base/core.rb, line 259
def add_parser_summary
  add_section("Summary", wrap(summary, summary_width * 2))
end
add_parser_usage() click to toggle source
# File lib/luban/cli/base/core.rb, line 196
def add_parser_usage
  parser.banner = compose_banner
  text
end
add_parser_version() click to toggle source
# File lib/luban/cli/base/core.rb, line 239
def add_parser_version
  parser.version = @version
end
add_section(title, rows = []) { |rows| ... } click to toggle source
# File lib/luban/cli/base/core.rb, line 279
def add_section(title, rows = [], &blk)
  text compose_title(title)
  yield rows if block_given?
  rows.each { |row| text compose_row(row) }
  text
end
after_alter() click to toggle source
# File lib/luban/cli/base/core.rb, line 115
def after_alter; end
after_configure() click to toggle source
# File lib/luban/cli/base/core.rb, line 113
def after_configure; end
command_class(cmd, base) click to toggle source
# File lib/luban/cli/base/commands.rb, line 52
def command_class(cmd, base)
  class_name = cmd.camelcase
  if command_class_defined?(class_name)
    get_command_class(class_name)
  else
    define_command_class(class_name, base)
  end
end
command_class_defined?(class_name) click to toggle source
# File lib/luban/cli/base/commands.rb, line 61
def command_class_defined?(class_name)
  self.class.const_defined?(class_name, false)
end
compose_banner() click to toggle source
# File lib/luban/cli/base/core.rb, line 201
def compose_banner
  "Usage: #{program_name} #{compose_synopsis}"
end
compose_row(string, indent = ' ' * summary_indent) click to toggle source
# File lib/luban/cli/base/core.rb, line 290
def compose_row(string, indent = ' ' * summary_indent)
  "#{indent}#{string}"
end
compose_summary(item, summary, width) click to toggle source
# File lib/luban/cli/base/core.rb, line 304
def compose_summary(item, summary, width)
  "%-#{width}s %-#{width}s" % [item, summary]
end
compose_synopsis() click to toggle source
# File lib/luban/cli/base/core.rb, line 205
def compose_synopsis
  if has_commands?
    compose_synopsis_with_commands
  else
    compose_synopsis_without_commands
  end
end
compose_synopsis_with_arguments() click to toggle source
# File lib/luban/cli/base/core.rb, line 225
def compose_synopsis_with_arguments
  synopsis = ''
  @arguments.each_value do |arg|
    synopsis += arg.required? ? arg.display_name : "[#{arg.display_name}]"
    synopsis += "[ #{arg.display_name}]*" if arg.multiple?
    synopsis += ' '
  end
  synopsis
end
compose_synopsis_with_commands() click to toggle source
# File lib/luban/cli/base/core.rb, line 213
def compose_synopsis_with_commands
  "[options] command [command options] [arguments ...]"
end
compose_synopsis_with_options() click to toggle source
# File lib/luban/cli/base/core.rb, line 221
def compose_synopsis_with_options
  options.empty?  ? '' : '[options] '
end
compose_synopsis_without_commands() click to toggle source
# File lib/luban/cli/base/core.rb, line 217
def compose_synopsis_without_commands
  "#{compose_synopsis_with_options}#{compose_synopsis_with_arguments}"
end
compose_title(title, indent = ' ' * title_indent, suffix = ':') click to toggle source
# File lib/luban/cli/base/core.rb, line 286
def compose_title(title, indent = ' ' * title_indent, suffix = ':')
  "#{indent}#{title}#{suffix}"
end
configure(&blk) click to toggle source
# File lib/luban/cli/base/core.rb, line 104
def configure(&blk)
  [self.class.config_blk, blk].each do |callback|
    instance_eval(&callback) unless callback.nil?
  end
  on_configure
  after_configure
end
create_action(method_name = nil, preserve_argv: true, &blk) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 102
def create_action(method_name = nil, preserve_argv: true, &blk)
  handler = if method_name
              lambda { |**opts| invoke_action(method_name, **opts) }
            elsif block_given?
              blk
            end
  if handler.nil?
    raise ArgumentError, "Code block to execute command #{@action_name} is MISSING."
  end
  _base = self
  parse_method = preserve_argv ? :parse : :parse!
  define_action_method do |argv = _base.default_argv|
    _base.send(:process, parse_method, argv) do |params|
      instance_exec(**params, &handler)
    end
  end
  @action_defined = true
end
create_parser() click to toggle source
# File lib/luban/cli/base/core.rb, line 183
def create_parser
  @parser = OptionParser.new
  add_parser_usage
  add_parser_version unless @version.empty?
  add_parser_options unless options.empty?
  add_parser_arguments unless arguments.empty?
  add_parser_summary unless summary.empty?
  add_parser_description unless description.empty?
  add_parser_defaults unless options.values.all? { |o| o.default_str.empty? }
  add_parser_commands if has_commands?
  @parser
end
define_action_method(&action_blk) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 121
def define_action_method(&action_blk)
  @parent.send(:define_singleton_method, action_method, &action_blk)
end
define_command_class(class_name, base) click to toggle source
# File lib/luban/cli/base/commands.rb, line 69
def define_command_class(class_name, base)
  self.class.send(:define_class, class_name, base: base, namespace: self.class)
end
dispatch_command(cmd:, argv:) click to toggle source
# File lib/luban/cli/base/core.rb, line 136
def dispatch_command(cmd:, argv:)
  validate_command(cmd)
  send(commands[cmd].action_method, argv)
end
find_action_handler(action_method) click to toggle source
# File lib/luban/cli/base/core.rb, line 150
def find_action_handler(action_method)
  if respond_to?(action_method, true)
    self
  elsif @parent != self and @parent.respond_to?(:find_action_handler, true)
    @parent.send(:find_action_handler, action_method)
  else
    nil
  end
end
get_command_class(class_name) click to toggle source
# File lib/luban/cli/base/commands.rb, line 65
def get_command_class(class_name)
  self.class.const_get(class_name, false)
end
invoke_action(action_method, **opts) click to toggle source
# File lib/luban/cli/base/core.rb, line 141
def invoke_action(action_method, **opts)
  handler = find_action_handler(action_method)
  if handler.nil?
    raise RuntimeError, "Action handler #{action_method} is MISSING."
  else
    handler.send(action_method, **opts)
  end
end
on_alter() click to toggle source
# File lib/luban/cli/base/core.rb, line 114
def on_alter; end
on_configure() click to toggle source
# File lib/luban/cli/base/core.rb, line 112
def on_configure; end
on_parse_error(error) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 144
def on_parse_error(error)
  show_error_and_exit(error)
end
parse_arguments(argv) click to toggle source
# File lib/luban/cli/base/parse.rb, line 38
def parse_arguments(argv)
  @arguments.each_value do |arg|
    break if argv.empty?
    arg.value = arg.multiple? ? argv.slice!(0..-1) : argv.shift
  end
end
parse_command(argv) click to toggle source
# File lib/luban/cli/base/parse.rb, line 26
def parse_command(argv)
  cmd = argv.shift
  cmd = cmd.to_sym unless cmd.nil?
  @result[:cmd] = cmd
end
parse_permutationally(argv)
parse_posixly_correct(argv)
Alias for: parse_with_commands
parse_with_commands(argv) click to toggle source
# File lib/luban/cli/base/parse.rb, line 20
def parse_with_commands(argv)
  parser.order!(argv)
  parse_command(argv)
end
Also aliased as: parse_posixly_correct
parse_without_commands(argv) click to toggle source
# File lib/luban/cli/base/parse.rb, line 32
def parse_without_commands(argv)
  parser.permute!(argv)
  parse_arguments(argv)
end
Also aliased as: parse_permutationally
process(parse_method, argv) { |args: result, opts: result| ... } click to toggle source
# File lib/luban/cli/base/dsl.rb, line 125
def process(parse_method, argv)
  send(parse_method, argv)
  if result[:opts][:help]
    show_help
  elsif !@version.empty? and result[:opts][:version]
    show_version
  else
    validate_required_options
    validate_required_arguments
    result[:opts][:__remaining__] = result[:argv]
    yield args: result[:args], opts: result[:opts]
    if has_commands?
      dispatch_command(cmd: result[:cmd], argv: result[:argv])
    end
  end
rescue OptionParser::ParseError, Error => e
  on_parse_error(e)
end
setup_default_action() click to toggle source
# File lib/luban/cli/base/core.rb, line 117
def setup_default_action
  if has_commands?
    action_noops
  else
    action_abort
  end
end
show_error(error) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 154
def show_error(error)
  puts "#{error.message} (#{error.class.name})"
  puts
end
show_error_and_exit(error) click to toggle source
# File lib/luban/cli/base/dsl.rb, line 148
def show_error_and_exit(error)
  show_error(error)
  show_help
  exit 64 # Linux standard for bad command line
end
summarize(item, summary, width = summary_width, max_width = width - 1) click to toggle source
# File lib/luban/cli/base/core.rb, line 294
def summarize(item, summary, width = summary_width, max_width = width - 1)
  item_rows = wrap(item.to_s, max_width)
  summary_rows = wrap(summary, max_width)
  num_of_rows = [item_rows.size, summary_rows.size].max
  rows = (0...num_of_rows).collect do |i|
           compose_summary(item_rows[i], summary_rows[i], width)
         end
  return rows
end
text(string = nil) click to toggle source
# File lib/luban/cli/base/core.rb, line 235
def text(string = nil)
  @parser.separator(string)
end
undef_singleton_method(method_name) click to toggle source
# File lib/luban/cli/base/core.rb, line 98
def undef_singleton_method(method_name)
  # Undefine methods that are defined in the eigenclass
  singleton_class.send(:undef_method, method_name)
  #(class << self; self; end).send(:undef_method, method_name)
end
update_result(argv) click to toggle source
# File lib/luban/cli/base/parse.rb, line 45
def update_result(argv)
  @result[:argv] = argv
  @result[:opts] = options.values.inject({}) { |r, o| r[o.name] = o.value; r }
  @result[:args] = arguments.values.inject({}) { |r, a| r[a.name] = a.value; r }
  @result
end
validate_command(cmd) click to toggle source
# File lib/luban/cli/base/core.rb, line 160
def validate_command(cmd)
  if cmd.nil?
    raise MissingCommand, "Please specify a command to execute."
  end
  unless has_command?(cmd)
    raise InvalidCommand, "Invalid command: #{cmd}"
  end
end
validate_required_arguments() click to toggle source
# File lib/luban/cli/base/core.rb, line 176
def validate_required_arguments
  missing_args = @arguments.each_value.select(&:missing?).collect(&:display_name)
  unless missing_args.empty?
    raise MissingRequiredArguments, "Missing required argument(s): #{missing_args.join(', ')}"
  end
end
validate_required_options() click to toggle source
# File lib/luban/cli/base/core.rb, line 169
def validate_required_options
  missing_opts = @options.each_value.select(&:missing?).collect(&:display_name)
  unless missing_opts.empty?
    raise MissingRequiredOptions, "Missing required option(s): #{missing_opts.join(', ')}"
  end
end
wrap(string, width) click to toggle source
# File lib/luban/cli/base/core.rb, line 308
def wrap(string, width)
  rows = []
  row = ''
  string.split(/\s+/).each do |word|
    if row.size + word.size >= width
      rows << row
      row = word
    elsif row.empty?
      row = word
    else
      row << ' ' << word
    end
  end
  rows << row if row
  rows
end