Committing before test
This commit is contained in:
+8
-75
@@ -1,11 +1,17 @@
|
||||
require "option_parser"
|
||||
require "./cligen/regex"
|
||||
require "./cligen/parsable"
|
||||
require "./cligen/annotations"
|
||||
require "./cligen/format"
|
||||
require "./cligen/regex"
|
||||
require "./cligen/flag"
|
||||
require "./cligen/command"
|
||||
require "./cligen/command_node"
|
||||
require "./cligen/app"
|
||||
|
||||
module CliGen
|
||||
VERSION = "0.1.0"
|
||||
|
||||
APPNAME = File.basename(PROGRAM_NAME)
|
||||
|
||||
record AdditionalDefaultFlag,
|
||||
short : String,
|
||||
long : String,
|
||||
@@ -26,78 +32,5 @@ module CliGen
|
||||
|
||||
annotation DefaultFlag
|
||||
end
|
||||
|
||||
macro define_section(section_name, parser)
|
||||
{{parser}}.separator ""
|
||||
{{parser}}.separator "{{section_name.id}}".colorize(:green)
|
||||
{{parser}}.separator "-------------------------------------------------------------------------------".colorize(:blue)
|
||||
end
|
||||
|
||||
|
||||
alias OnType = Tuple(String, String) | Tuple(String, String, String)
|
||||
macro define_default_flags(parser)
|
||||
CliGen.define_section("Misc Flags", {{parser}})
|
||||
{{parser}}.on("-h", "--help", "Print out this help output"){ abort {{parser}} }
|
||||
{{parser}}.invalid_option{|opt|
|
||||
abort "#{CliGen::APPNAME} : invalid_option : Inavlid option provided #{opt}"
|
||||
}
|
||||
{{parser}}.invalid_option{|opt|
|
||||
case opt
|
||||
when /^-+[a-z-]+/
|
||||
STDERR.puts "#{CliGen::APPNAME} : ERROR : {{@type.name}} : {{parser}} : invalid_option : Inavlid option provided #{opt}"
|
||||
else
|
||||
abort "#{CliGen::APPNAME} : ERROR : {{@type.name}} : {{parser}} : invalid_option : Inavlid option provided #{opt}"
|
||||
end
|
||||
}
|
||||
{{parser}}.missing_option{|opt|
|
||||
abort "ERROR : {{@type.name}} : {{parser}} : missing_option : Argument was not provided to #{opt}"
|
||||
}
|
||||
{{parser}}.unknown_args{|opt|
|
||||
unless opt.empty?
|
||||
case opt.first
|
||||
when /^-+[a-z-]+$/
|
||||
STDERR.puts "ERROR : {{@type.name}} : {{parser}} : unknown_args : #{opt} not a configured argument"
|
||||
else
|
||||
abort "ERROR : {{@type.name}} : {{parser}} : unknown_args : #{opt} not a configured argument"
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
CliGen::ADDITIONAL_DEFAULT_FLAGS.each do |flag|
|
||||
if [ flag.short, flag.long ].all?(&.!= "")
|
||||
{{parser}}.on(flag.short, flag.long, flag.description){|var| flag.work.call(var) }
|
||||
elsif flag.short != ""
|
||||
{{parser}}.on(flag.short, flag.description){|var| flag.work.call(var) }
|
||||
elsif flag.long != ""
|
||||
{{parser}}.on(flag.long, flag.description){|var| flag.work.call(var) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
macro define_root_parser
|
||||
ROOT_COMMAND = OptionParser.new do |parser|
|
||||
parser.banner = "#{CliGen::APPNAME} [command] [flags]"
|
||||
|
||||
{% commands = ::CliGen::Command.subclasses %}
|
||||
{% raise "ERROR : No commands defined (no subclasses of ::CliGen::Command were found)" if commands.empty? %}
|
||||
CliGen.define_section("Commands", parser)
|
||||
{% for command in commands %}
|
||||
{{command.name}}.make_parser(parser)
|
||||
{% end %}
|
||||
|
||||
CliGen.define_default_flags(parser)
|
||||
|
||||
abort parser if ARGV.empty?
|
||||
end
|
||||
end
|
||||
|
||||
macro finished
|
||||
define_root_parser
|
||||
end
|
||||
|
||||
def self.parse
|
||||
ROOT_COMMAND.parse
|
||||
end
|
||||
end
|
||||
|
||||
require "./cligen/command"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
module CliGen
|
||||
annotation ProxyCommand
|
||||
end
|
||||
|
||||
annotation CommandInfo
|
||||
end
|
||||
|
||||
annotation Argument
|
||||
end
|
||||
|
||||
annotation Trigger
|
||||
end
|
||||
|
||||
annotation Selection
|
||||
end
|
||||
|
||||
annotation SubCommand
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
require "./arg"
|
||||
require "./command_node"
|
||||
require "./flag"
|
||||
require "./app/generate"
|
||||
|
||||
module CliGen
|
||||
# Root entry point. Holds a flattened copy of all flags from every command
|
||||
# for global-flag matching, then hands off to the matched child CommandNode.
|
||||
class App < CliGen::BaseCommandNode
|
||||
@@instance : self?
|
||||
|
||||
def initialize(@name, flags : Array(BaseFlag), commands : Array(BaseCommandNode), pre_run_commands : Array(RunCommand), post_run_commands : Array(RunCommand))
|
||||
super(name: @name, flags: flags, commands: commands, pre_run_commands: pre_run_commands, post_run_commands: post_run_commands)
|
||||
@@instance = self
|
||||
end
|
||||
|
||||
def check! : Nil
|
||||
CliGen::GLOBAL_FLAGS.each(&.check!)
|
||||
# @flags is the amalgamation of all child flags — checked by the commands themselves
|
||||
check_for_duplicates!([CliGen::GLOBAL_FLAGS, @flags].flatten)
|
||||
@commands.each(&.check!)
|
||||
end
|
||||
|
||||
# Convenience entry point; defaults to ARGV
|
||||
def self.process(args : Array(String) = ARGV.to_a) : Nil
|
||||
generate if @@instance.nil?
|
||||
@@instance.not_nil!.process(args)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
module CliGen
|
||||
macro finished
|
||||
class App
|
||||
private def self.generate
|
||||
app_flags = [] of BaseFlag
|
||||
app_commands = [] of BaseCommandNode
|
||||
|
||||
{% for cmd in CliGen::Command.subclasses %}
|
||||
{% cmd_info = cmd.annotation(CliGen::CommandInfo) %}
|
||||
{% raise "ERROR : CliGen::App.generate : No CliGen::CommandInfo annotation made for #{cmd.name}" unless cmd_info %}
|
||||
{% raise "ERROR : CliGen::App.generate : Description must be provided for #{cmd.name}" unless cmd_info[:description].is_a? StringLiteral %}
|
||||
{% cmd_name = cmd.name.split("::").last.downcase %}
|
||||
{% cmd_flags = cmd.instance_vars.select{|v| v.annotation(CliGen::Argument) || v.annotation(CliGen::Selection) } %}
|
||||
|
||||
cmd_flags = [] of BaseFlag
|
||||
cmd_sub_commands = [] of BaseCommandNode
|
||||
cmd_pre_run_cmds = [] of Proc(Nil)
|
||||
cmd_post_run_cmds = [] of Proc(Nil)
|
||||
|
||||
{% for var in cmd_flags %}
|
||||
{% anno = var.annotation(CliGen::Argument) || var.annotation(CliGen::Selection) %}
|
||||
cmd_flags << Flag({{var.type}}).new(
|
||||
var: {{ var.name.stringify }},
|
||||
short: {% if anno[:short] %} {{anno[:short]}} {% else %} nil {% end %},
|
||||
long: {{ anno[:long] }},
|
||||
env_var: {% if anno[:env_var] %} {{anno[:env_var]}} {% else %} {{"#{cmd.name.upcase}_#{var.name.upcase}"}} {% end %},
|
||||
description: {{ anno[:description] }},
|
||||
default: {% if anno[:default] %} {{anno[:default]}} {% else %} nil {% end %},
|
||||
validate: {% if anno[:validate] %} {{anno[:validate]}} {% else %} nil {% end %},
|
||||
on_match: {% if anno[:on_match] %} {{anno[:on_match]}} {% else %} nil {% end %}
|
||||
)
|
||||
{% end %}
|
||||
|
||||
# Keep a copy of every flag on the root for global matching
|
||||
app_flags += cmd_flags
|
||||
|
||||
app_commands << CommandNode({{cmd}}).new(
|
||||
name: {{cmd_name}},
|
||||
flags: cmd_flags,
|
||||
commands: cmd_sub_commands,
|
||||
pre_run_commands: cmd_pre_run_cmds,
|
||||
post_run_commands: cmd_post_run_cmds,
|
||||
description: {{cmd_info[:description]}}
|
||||
)
|
||||
{% end %}
|
||||
|
||||
new(
|
||||
name: ::PROGRAM_NAME,
|
||||
flags: app_flags,
|
||||
commands: app_commands,
|
||||
pre_run_commands: [] of Proc(Nil),
|
||||
post_run_commands: [] of Proc(Nil)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
module CliGen
|
||||
# This class serves as a "argument wrapper" to force a fail-fast approach to
|
||||
# arg-parsing.
|
||||
#
|
||||
# It wraps around the argument + index of the argument to do state tracking
|
||||
# and ensure that each argument is only processed once (plus allows for
|
||||
# easier filtering of processed arguments to avoid having to do index math)
|
||||
#
|
||||
# args.reject(&.processed?) # returns the args that haven't been processed yet
|
||||
#
|
||||
# It expects each argument to only be processed once and will force a raise
|
||||
# if the argument has Arg#processed called a second time. This is to force
|
||||
# the developer (me) to fix any processing issues during the development of
|
||||
# this framework.
|
||||
class Arg
|
||||
|
||||
# The raw string argument provided from the user
|
||||
getter value : String
|
||||
# The index of the argument in the array it was in
|
||||
getter index : Int32
|
||||
# The "flag"/variable that tracks is the Arg has been processed yet
|
||||
getter? processed : Bool = false
|
||||
|
||||
def initialize(@value, @index)
|
||||
end
|
||||
|
||||
# This serves as a trigger that tells the object that it has been processed
|
||||
#
|
||||
# This will raise an exception if it is re-called after already having been
|
||||
# processed.
|
||||
def processed
|
||||
raise "ERROR : CliGen::Arg(index: #{@index}, value: #{@value})#processed : This arg was re-processed" if @processed
|
||||
@processed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
module CliGen::Coercable
|
||||
abstract def coerce(arg : String) : self
|
||||
end
|
||||
+21
-179
@@ -1,193 +1,35 @@
|
||||
require "colorize"
|
||||
require "time"
|
||||
|
||||
require "./command/parser"
|
||||
require "./annotations"
|
||||
require "./command/argument"
|
||||
require "./command/selection"
|
||||
require "./command/trigger"
|
||||
require "./command/subcommand"
|
||||
|
||||
module CliGen
|
||||
|
||||
annotation CommandPreRun
|
||||
end
|
||||
|
||||
annotation CommandInfo
|
||||
end
|
||||
|
||||
annotation CommandArgument
|
||||
end
|
||||
|
||||
annotation CommandSelection
|
||||
end
|
||||
|
||||
annotation SubCommand
|
||||
end
|
||||
|
||||
|
||||
class Command
|
||||
extend CliGen::Parser
|
||||
|
||||
|
||||
macro inherited
|
||||
{% verbatim do %}
|
||||
macro finished
|
||||
define_actions
|
||||
define_header
|
||||
{% anno = @type.annotation(::CliGen::CommandInfo) %}
|
||||
{% unless anno[:def_runner] == false %}
|
||||
define_runner
|
||||
{% end %}
|
||||
define_parser
|
||||
define_action_setter
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
macro define_actions
|
||||
@@action : String = ""
|
||||
ACTIONS = {{@type.class.methods.select(&.annotation(::CliGen::SubCommand)).map(&.name.stringify)}} of String
|
||||
end
|
||||
|
||||
macro define_header
|
||||
{% name = @type.name.split("::").last.downcase.id %}
|
||||
{% info_annos = @type.class.methods.select(&.annotation(::CliGen::SubCommand)).map(&.annotation(::CliGen::SubCommand)) %}
|
||||
{% info_annos += @type.class.methods.select(&.annotation(::CliGen::CommandSelection)).map(&.annotation(::CliGen::CommandSelection)) %}
|
||||
{% examples = [] of StringLiteral %}
|
||||
{% info_annos.select(&.[](:examples)).map(&.[](:examples).resolve).each(&.each{|example| examples << example}) %}
|
||||
HEADER = [
|
||||
"#{CliGen::APPNAME} {{name}} [[flags]]",
|
||||
"",
|
||||
{% unless examples.empty? %}
|
||||
"Examples:".colorize(:green),
|
||||
"-------------------------------------------------------------------------------".colorize(:blue),
|
||||
{% for example in examples %}
|
||||
{{example}},
|
||||
{% end %}
|
||||
{% end %}
|
||||
].join("\n")
|
||||
end
|
||||
|
||||
macro define_selection(selection_name, short = "", long = "", description = nil, default = "", values = [] of StringLiteral)
|
||||
{% if values.is_a? Path %}
|
||||
{% values = values.resolve %}
|
||||
{% end %}
|
||||
{% raise "ERROR : define_selection : You MUST provide a description" unless description %}
|
||||
{% raise "ERROR : define_selection : You must provide a valid set of values in Array(String) format" if values.empty? %}
|
||||
{% description = "#{description.id} (valid: #{values.join(", ").id})" %}
|
||||
{% unless default.empty? %}
|
||||
{% description = "#{description.id} (default: #{default.id})" %}
|
||||
{% end %}
|
||||
@@{{selection_name}} : String = {{default}}
|
||||
|
||||
@[::CliGen::CommandArgument(short: {{short}}, long: {{long}}, description: {{description}} )]
|
||||
def self.{{selection_name}}= (selection : String)
|
||||
raise "ERROR : {{selection_name}} : Invalid Selection(#{selection}) (valid: {{values.map(&.id).join(", ").id}})" unless {{values}}.includes?(selection)
|
||||
@@{{selection_name}} = selection
|
||||
end
|
||||
end
|
||||
|
||||
macro define_argument(arg_name, variable = nil, type = String, short = "", long = "", description = nil, subtype = Nil, format = nil, check = nil, def_getter = false, default = nil, logger = nil, &block?)
|
||||
{% raise "ERROR : define_argument : You must provide a format or set it to \"auto\" to use builtin formats when defining a Time argument" if type.id.stringify == "Time" && format == nil %}
|
||||
{% raise "ERROR : define_argument : You MUST provide a description" unless description %}
|
||||
{% variable = arg_name unless variable %}
|
||||
{% _type = type.id.stringify %}
|
||||
{% _subtype = subtype.id.stringify %}
|
||||
{% if _type == "String" %}
|
||||
@@{{variable}} : {{type}} = ""
|
||||
{% elsif _type == "Bool" %}
|
||||
@@{{variable}} : {{type}} = false
|
||||
{% elsif _type == "Int32" %}
|
||||
@@{{variable}} : {{type}} = 0
|
||||
{% elsif _type == "Array" %}
|
||||
{% raise "define_argument : subtype can only be Int32 or String" unless %w[ Int32 String ].includes?(_subtype) %}
|
||||
@@{{variable}} : {{type}}({{subtype}}) = [] of {{subtype}}
|
||||
{% elsif _type == "Time" %}
|
||||
@@{{variable}} : {{type}} = Time.unix(seconds: 0)
|
||||
{% end %}
|
||||
{% if def_getter %}
|
||||
def self.get_{{arg_name}} : {{type}}
|
||||
@@{{variable}}
|
||||
end
|
||||
{% end %}
|
||||
|
||||
@[::CliGen::CommandArgument(type: {{type}}, short: {{short}}, long: {{long}}, description: {{description}})]
|
||||
def self.{{arg_name}}(var : String) : Nil
|
||||
{% if check %}
|
||||
## If the check exists go ahead and call it
|
||||
{{check}}(var)
|
||||
{% end %}
|
||||
|
||||
## If a logger method has been provided by the user
|
||||
{% if logger %}
|
||||
{{logger.id}} "Command : subclass : argument_setter : {{arg_name}} : Entered with #{var}"
|
||||
{% end %}
|
||||
{% if _type == "Time" %}
|
||||
formats = [
|
||||
CliGen::Regex::INPUT_DATETIME_REGEX,
|
||||
CliGen::Regex::INPUT_DATE_REGEX
|
||||
]
|
||||
abort "ERROR : Command : {{arg_name}} : Incorrect format for provided data #{var}" unless formats.any?{|f| var.match(f)}
|
||||
if var =~ formats[0]
|
||||
@@{{variable}} = Time.parse_local(var, CliGen::Format::INPUT_DATETIME_FORMAT)
|
||||
elsif var =~ formats[1]
|
||||
@@{{variable}} = Time.parse_local(var, CliGen::Format::INPUT_DATE_FORMAT)
|
||||
end
|
||||
{% elsif _type == "Array" %}
|
||||
{% if _subtype == "Int32" %}
|
||||
var : Int32 = var.to_i
|
||||
{% elsif _subtype == "String" %}
|
||||
{% else %}
|
||||
var = {{subtype}}.new(var)
|
||||
def initialize(*, handler : CliGen::BaseCommandNode)
|
||||
{% verbatim do %}
|
||||
{% for var in @type.instance_vars %}
|
||||
{% anno = (var.annotation(CliGen::Argument) || var.annotation(CliGen::Selection)) %}
|
||||
{% if anno %}
|
||||
{% raise "ERROR : #{@type.name}#initialize : Argument '#{var.name}' cannot be a nilable type (#{var.type}) — flags always resolve to a concrete value" if var.type.union? %}
|
||||
if flg = handler.flags.find{|f| f.var == {{var.name.stringify}} && f.long == {{anno[:long]}}}
|
||||
@{{var.id}} = flg.as(CliGen::Flag({{var.type}})).value!
|
||||
else
|
||||
raise "ERROR : {{@type.name}}#{{@def.name}} : No flag found for \"{{var.name}}\"?"
|
||||
end
|
||||
{% else %}
|
||||
{% raise "ERROR : #{@type.name}#{@def.name} : Instance Variable(#{var.name}) is not handled by CliGen and does not have a default value" unless var.default %}
|
||||
@{{var.id}} = {{var.default}}
|
||||
{% end %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
@@{{variable}} << var
|
||||
{% elsif _type == "Bool" %}
|
||||
@@{{variable}} = true
|
||||
{% elsif _type == "Int32" %}
|
||||
@@{{variable}} = var.to_i
|
||||
{% elsif _type == "String" %}
|
||||
@@{{variable}} = var
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
|
||||
macro define_runner
|
||||
def self.run
|
||||
{% pre_run_commands = @type.class.methods.select(&.annotation(::CliGen::CommandPreRun)) %}
|
||||
{% unless pre_run_commands.empty? %}
|
||||
## Ensuring that all runs that are tagged with the the CommandPreRun annotation are run
|
||||
{% for method in pre_run_commands %}
|
||||
{{method.name}}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
{% selections = @type.class.methods.select(&.annotation(::CliGen::CommandSelection)) %}
|
||||
{% methods = @type.class.methods.select(&.annotation(::CliGen::SubCommand)) %}
|
||||
{% raise "ERROR : No commands or selections defined for #{@type.name}" if methods.empty? && selections.empty? %}
|
||||
{% raise "ERROR : Can't define both selections and commands for work in #{@type.name}" if ! methods.empty? && ! selections.empty? %}
|
||||
{% var = methods.empty? ? nil : "action" %}
|
||||
{% targets = methods.empty? ? selections : methods %}
|
||||
{% if targets == selections %}
|
||||
{% selectors = selections.map(&.annotation(::CliGen::CommandSelection)).map(&.[:selector]).uniq %}
|
||||
{% raise "ERROR : Can't provide more than a single selector for runner" if selectors.size > 1 %}
|
||||
{% var = selectors.first %}
|
||||
{% end %}
|
||||
{% begin %}
|
||||
case @@{{var.id}}
|
||||
{% for target in targets %}
|
||||
when {{target.name.stringify}}
|
||||
{{target.name}}
|
||||
{% end %}
|
||||
else
|
||||
abort "ERROR : No action provided to command {{@type.name}}"
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
|
||||
macro define_action_setter
|
||||
def self.action=(action : String)
|
||||
abort "ERROR : Action(#{action}) is not valid. Only #{ACTIONS.join(", ")} are acceptable" unless ACTIONS.includes?(action)
|
||||
@@action = action
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
module CliGen
|
||||
class Command
|
||||
macro argument(variable, short, long, description, validation = nil)
|
||||
{% raise "ERROR : CliGen::Command.argument : First argument (#{variable}) must be a TypeDeclaration (ex: '<var> : <type> [= val]')" unless variable.is_a? TypeDeclaration %}
|
||||
{% name = variable.name %}
|
||||
{% type = variable.type %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided short must be a string" unless short.is_a? StringLiteral || string == nil %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided long must be a string" unless long.is_a? StringLiteral || long == nil %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : You must provide a short or long" unless long || short %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : You must provide a description" unless description %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided description must be a String" unless description.is_a? StringLiteral %}
|
||||
{% unless validation.nil? %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation must be a Proc" unless validation.is_a? ProcLiteral %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation return type must be a Bool" unless validation.return_type == Bool %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation provided validation must have an input variable" if validation.args.empty? %}
|
||||
{% arg = validation.args.first %}
|
||||
{% unless arg.restriction == type %}
|
||||
{% example = "->(#{arg.name} : #{type}) : #{type} { #{validation.body} }" %}
|
||||
{% raise "ERROR : CliGen::Command.argument(#{name}) : Provided validation input value must be #{type}. EX: #{example}" %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
@[CliGen::Argument(short: {{short}}, long: {{long}}, description: {{description}}, validation: {{validation}}, on_match: {{on_match}})]
|
||||
@{{variable}}
|
||||
|
||||
def {{variable.name}}= (value : {{type}})
|
||||
{% unless validation.nil? %}
|
||||
raise "ERROR : #{@type.name}##{@def.name} : Provided value #{value} is not passing validation" unless {{validation}}.call(value)
|
||||
{% end %}
|
||||
@{{name}} = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,70 +0,0 @@
|
||||
module CliGen::Parser
|
||||
|
||||
macro extended
|
||||
{% verbatim do %}
|
||||
macro define_parser
|
||||
def self.make_parser(parent_parser : OptionParser) : Nil
|
||||
{% puts "#{@type.name} OptionParser is being generated" %}
|
||||
{% name = @type.name.split("::").last %}
|
||||
{% var = name.downcase %}
|
||||
{% methods = @type.class.methods %}
|
||||
{% info = @type.annotation(::CliGen::CommandInfo) %}
|
||||
{% raise "ERROR : No CommandInfo annotation provided to {{@type.name}}" unless info %}
|
||||
subparser = OptionParser.new do |parser|
|
||||
parser.banner = {{@type.name}}::HEADER
|
||||
|
||||
{% selections = methods.select(&.annotation(::CliGen::CommandSelection)) %}
|
||||
{% if selections.size > 0 %}
|
||||
CliGen.define_section("Selections", parser)
|
||||
{% for selection in selections %}
|
||||
{% selection_anno = selection.annotation(::CliGen::CommandSelection) %}
|
||||
parser.on({{selection.name.stringify}}, {{selection_anno[:description]}}){
|
||||
{{@type.name}}.{{selection_anno[:selector]}}= {{selection.name.stringify}}
|
||||
}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
{% subcommands = methods.select(&.annotation(::CliGen::SubCommand)) %}
|
||||
{% if subcommands.size > 0 %}
|
||||
CliGen.define_section("Subcommands", parser)
|
||||
{% for subcommand in subcommands %}
|
||||
{% subcommand_anno = subcommand.annotation(::CliGen::SubCommand) %}
|
||||
parser.on({{subcommand.name.stringify}}, {{subcommand_anno[:description]}}){
|
||||
{{@type.name}}.action= {{subcommand.name.stringify}}
|
||||
}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
{% arguments = methods.select(&.annotation(::CliGen::CommandArgument)) %}
|
||||
{% if arguments.size > 0 %}
|
||||
CliGen.define_section("Provide Arguments", parser)
|
||||
{% for argument in arguments %}
|
||||
{% argument_anno = argument.annotation(::CliGen::CommandArgument) %}
|
||||
{% if argument_anno[:short] == "" && argument_anno[:long] != "" %}
|
||||
parser.on({{argument_anno[:long]}}, {{argument_anno[:description]}}){|val|
|
||||
{% elsif argument_anno[:short] != "" && argument_anno[:long] == "" %}
|
||||
parser.on({{argument_anno[:short]}}, {{argument_anno[:description]}}){|val|
|
||||
{% else %}
|
||||
parser.on({{argument_anno[:short]}}, {{argument_anno[:long]}}, {{argument_anno[:description]}}){|val|
|
||||
{% end %}
|
||||
{{@type.name}}.{{argument.name}}(val)
|
||||
}
|
||||
{% end %}
|
||||
{% end %}
|
||||
CliGen.define_default_flags(parser)
|
||||
end
|
||||
parent_parser.on({{var}}, {{info[:description]}}){
|
||||
raise "ERROR : {{var.id}} not found in ARGV" unless i = ARGV.index({{var}})
|
||||
## Removing all preceeding arguments so that only the actual required
|
||||
## args for the next command are present
|
||||
ARGV.shift(i+1)
|
||||
abort subparser if ARGV.empty?
|
||||
subparser.parse
|
||||
{{@type.name}}.run
|
||||
}
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
module CliGen
|
||||
class Command
|
||||
macro selection(variable, short, long, description, options)
|
||||
{% raise "ERROR : CliGen::Command.selection : First selection must be a TypeDeclaration (ex: '<var> : <type> [= val]')" unless variable.is_a? TypeDeclaration %}
|
||||
{% raise "ERROR : CliGen::Command.selection : Provided short must be a string" unless short.is_a? StringLiteral || string == nil %}
|
||||
{% raise "ERROR : CliGen::Command.selection : Provided long must be a string" unless long.is_a? StringLiteral || long == nil %}
|
||||
{% raise "ERROR : CliGen::Command.selection : You must provide a short or long" unless long || short %}
|
||||
{% raise "ERROR : CliGen::Command.selection : You must provide a description" unless description %}
|
||||
{% raise "ERROR : CliGen::Command.selection : Provided description must be a String" unless description.is_a? StringLiteral %}
|
||||
{% raise "ERROR : CliGen::Command.selection : You must provide options" unless options %}
|
||||
{% raise "ERROR : CliGen::Command.selection : Provided options must be " unless options %}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
module CliGen
|
||||
class Command
|
||||
macro subcommand(func, description, examples = nil, &block)
|
||||
{% raise "ERROR : CliGen::Command.subcommand : First argument must be a TypeDeclaration (ex: '<var> : <type>')" unless variable.is_a? TypeDeclaration %}
|
||||
{% raise "ERROR : CliGen::Command.subcommand : You must provide a description" unless description %}
|
||||
{% raise "ERROR : CliGen::Command.subcommand : Provided description must be a String" unless description.is_a? StringLiteral %}
|
||||
{% unless examples.nil? %}
|
||||
{% examples = examples.resolve if examples.is_a? Path %}
|
||||
{% raise "ERROR : CliGen::Command.subcommand : Provided example must be an Array" unless examples.is_a? ArrayLiteral %}
|
||||
{% end %}
|
||||
{% raise "ERROR : CliGen::Command.subcommand : You MUST provide a function body" unless block %}
|
||||
|
||||
@[CliGen::SubCommand(description: {{description}}, examples: {{examples}})]
|
||||
def {{func}}
|
||||
{{block.body}}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
module CliGen
|
||||
class Command
|
||||
macro trigger(short, long, argument = nil, &on_match)
|
||||
{% raise "ERROR : CliGen::Command.trigger : Provided short must be a string" unless short.is_a? StringLiteral %}
|
||||
{% raise "ERROR : CliGen::Command.trigger : Provided long must be a string" unless long.is_a? StringLiteral %}
|
||||
{% raise "ERROR : CliGen::Command.trigger : Must provide a block for on_match trigger" unless on_match %}
|
||||
{% name = long.gsub(/--/, "") %}
|
||||
|
||||
@[CliGen::Trigger(short: {{short}}, long: {{long}}, argument: {{argument}})]
|
||||
{% if argument %}
|
||||
def self.__cligen_trigger__{{name}}__({{name}} : {{argument}}) : Nil
|
||||
{{on_match.body}}
|
||||
end
|
||||
{% else %}
|
||||
def self.__cligen_trigger__{{name}}__ : Nil
|
||||
{{on_match.body}}
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,235 @@
|
||||
require "./global_flag"
|
||||
require "./flag"
|
||||
require "./arg"
|
||||
require "ecr"
|
||||
|
||||
module CliGen
|
||||
record SubCommandInfo,
|
||||
name : String,
|
||||
description : String,
|
||||
examples : Array(String)?
|
||||
|
||||
alias RunCommand = Proc(Nil)
|
||||
|
||||
# Non-generic base that lets the tree hold heterogeneous CommandNode(T) children.
|
||||
# Everything that doesn't depend on T lives here.
|
||||
abstract class BaseCommandNode
|
||||
getter name : String
|
||||
getter flags : Array(BaseFlag)
|
||||
getter commands : Array(BaseCommandNode)
|
||||
getter description : String?
|
||||
@pre_run_commands : Array(RunCommand)
|
||||
@post_run_commands : Array(RunCommand)
|
||||
|
||||
def initialize(
|
||||
@name : String,
|
||||
flags : Array(BaseFlag),
|
||||
@commands : Array(BaseCommandNode),
|
||||
@pre_run_commands : Array(RunCommand),
|
||||
@post_run_commands : Array(RunCommand),
|
||||
@description : String? = nil
|
||||
)
|
||||
@flags = flags + CliGen::GLOBAL_FLAGS + @commands.flat_map(&.flags)
|
||||
end
|
||||
|
||||
def help : String
|
||||
ECR.render("src/cligen/template/cmd_help.ecr")
|
||||
end
|
||||
|
||||
def check_for_duplicates!(flags : Array(BaseFlag)) : Nil
|
||||
shorts = flags.compact_map(&.short)
|
||||
short_duplicates = [] of String
|
||||
longs = flags.compact_map { |f| f.long_key unless f.long_key.empty? }
|
||||
long_duplicates = [] of String
|
||||
|
||||
last_short : String = ""
|
||||
shorts.sort.each do |short|
|
||||
short_duplicates << short if last_short == short
|
||||
last_short = short
|
||||
end
|
||||
|
||||
last_long : String = ""
|
||||
longs.sort.each do |long|
|
||||
long_duplicates << long if last_long == long
|
||||
last_long = long
|
||||
end
|
||||
|
||||
unless long_duplicates.empty? && short_duplicates.empty?
|
||||
error_buffer = "ERROR : CommandNode(%s)#check! : Found Duplicates : %s"
|
||||
|
||||
message = ""
|
||||
unless long_duplicates.empty?
|
||||
message += "\nLong:\n%s\n" % long_duplicates.map { |f| "- #{f}" }.join("\n")
|
||||
end
|
||||
|
||||
unless short_duplicates.empty?
|
||||
message += "\nShort:\n%s" % short_duplicates.map { |f| "- #{f}" }.join("\n")
|
||||
end
|
||||
|
||||
raise error_buffer % [@name, message]
|
||||
end
|
||||
end
|
||||
|
||||
def get(flag_long : String) : BaseFlag?
|
||||
@flags.find{|f| f.long_key == flag_long}
|
||||
end
|
||||
|
||||
def find_match(arg : String)
|
||||
if subcommand?(arg)
|
||||
return CliGen::MatchType::SubCommand
|
||||
end
|
||||
case arg
|
||||
when "-h", "--help"
|
||||
CliGen::MatchType::Help
|
||||
when CliGen::Regex::FLAG_REGEX
|
||||
if flg = @flags.find(&.matches?(arg))
|
||||
flg
|
||||
else
|
||||
CliGen::MatchType::NoMatch
|
||||
end
|
||||
when CliGen::Regex::FLAG_WITH_ARG
|
||||
CliGen::MatchType::FlagWithArg
|
||||
when CliGen::Regex::SHORT_WITH_INLINE_ARG
|
||||
CliGen::MatchType::ShortWithInlineArg
|
||||
when CliGen::Regex::FLAG_MULTIPLE_SHORT
|
||||
CliGen::MatchType::FlagMultipleShort
|
||||
else
|
||||
if cmd = @commands.find { |c| c.name == arg }
|
||||
cmd
|
||||
else
|
||||
CliGen::MatchType::NoMatch
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def subcommands? : Bool
|
||||
subcommands.size > 0
|
||||
end
|
||||
|
||||
def subcommand?(arg : String) : Bool
|
||||
subcommands.any?{|f| f.name == arg}
|
||||
end
|
||||
|
||||
# Converts String array to Arg array and hands off to the typed process method
|
||||
def process(args : Array(String)) : Nil
|
||||
new_args = args.each_with_index.map { |arg, i| Arg.new(value: arg, index: i) }.to_a
|
||||
process(new_args)
|
||||
end
|
||||
|
||||
abstract def subcommands : Array(SubCommandInfo)
|
||||
abstract def check! : Nil
|
||||
abstract def process(args : Array(Arg)) : Nil
|
||||
end
|
||||
|
||||
class CommandNode(T) < BaseCommandNode
|
||||
def subcommands : Array(SubCommandInfo)
|
||||
{% subcmds = T.methods.select(&.annotation(CliGen::SubCommand)) %}
|
||||
{% if subcmds.empty? %}
|
||||
[] of SubCommandInfo
|
||||
{% else %}
|
||||
[
|
||||
{% for cmd in subcmds %}
|
||||
{% anno = cmd.annotation(CliGen::SubCommand) %}
|
||||
SubCommandInfo.new(
|
||||
name: {{cmd.name.stringify}},
|
||||
description: {{anno[:description]}},
|
||||
examples: {% if anno[:examples] %} {{anno[:examples]}} {% else %} nil {% end %}
|
||||
),
|
||||
{% end %}
|
||||
]
|
||||
{% end %}
|
||||
end
|
||||
|
||||
def check! : Nil
|
||||
@flags.each(&.check!)
|
||||
check_for_duplicates!(@flags)
|
||||
@commands.each(&.check!)
|
||||
raise "ERROR : CommandNode({{T}})#check! : {{T}} has no subcommands and no #main defined" \
|
||||
if subcommands.empty? && !{{T.has_method?(:main)}}
|
||||
end
|
||||
|
||||
def process(args : Array(Arg)) : Nil
|
||||
check!
|
||||
passed_execution = false
|
||||
matched_subcommand : String? = nil
|
||||
|
||||
@pre_run_commands.each(&.call)
|
||||
|
||||
args.each do |arg|
|
||||
next if arg.processed?
|
||||
arg.processed
|
||||
|
||||
case match = find_match(arg.value)
|
||||
when BaseCommandNode
|
||||
# Hand off remainder to the child; we're done at this level
|
||||
match.process(args.reject(&.processed?))
|
||||
passed_execution = true
|
||||
|
||||
when BaseFlag
|
||||
if match.requires_arg?
|
||||
match.process(args.reject(&.processed?))
|
||||
else
|
||||
match.process
|
||||
end
|
||||
|
||||
when MatchType::SubCommand
|
||||
raise "ERROR : CommandNode({{T}})#process : Subcommand(#{matched_subcommand}) was already matched" if matched_subcommand
|
||||
matched_subcommand = arg.value
|
||||
|
||||
when MatchType::Help
|
||||
abort help
|
||||
|
||||
when MatchType::FlagWithArg
|
||||
if regex_match = CliGen::Regex::FLAG_WITH_ARG.match(arg.value)
|
||||
case flag_match = find_match(regex_match["flag"])
|
||||
when BaseFlag
|
||||
flag_match.process([Arg.new(value: regex_match["arg"], index: arg.index)])
|
||||
else
|
||||
raise "ERROR : CommandNode(#{@name}).run : No flag matched '#{regex_match["flag"]}'"
|
||||
end
|
||||
else
|
||||
raise "Oh good, you broke regex. How the hell did it match in find_match but not above? What the hell is going on"
|
||||
end
|
||||
|
||||
when MatchType::ShortWithInlineArg
|
||||
abort "#{CliGen::APPNAME}: invalid flag '#{arg.value}' — inline values are not supported. Did you mean '#{arg.value[0..1]} #{arg.value[2..]}' ?"
|
||||
|
||||
when MatchType::FlagMultipleShort
|
||||
arg.value.gsub(/^-/, "").chars.map { |c| "-#{c}" }.each do |flag|
|
||||
case match = find_match(flag)
|
||||
when BaseFlag
|
||||
abort "#{CliGen::APPNAME}: cannot bundle flag that requires an argument: #{flag}" if match.requires_arg?
|
||||
match.process
|
||||
when MatchType::NoMatch
|
||||
raise "ERROR : CommandNode(#{@name}).run : No match for '#{flag}'"
|
||||
end
|
||||
end
|
||||
|
||||
when MatchType::NoMatch
|
||||
raise "ERROR : CommandNode(#{@name}).run : No match for '#{arg.value}'"
|
||||
end
|
||||
end
|
||||
|
||||
@post_run_commands.each(&.call)
|
||||
|
||||
unless passed_execution
|
||||
cls = T.new(self)
|
||||
{% subcmds = T.methods.select(&.annotation(CliGen::SubCommand)) %}
|
||||
{% begin %}
|
||||
case matched_subcommand
|
||||
{% for cmd in subcmds %}
|
||||
when {{cmd.name.stringify}}
|
||||
cls.{{cmd.name}}
|
||||
{% end %}
|
||||
else
|
||||
{% if T.has_method?(:main) %}
|
||||
cls.main
|
||||
{% else %}
|
||||
raise "ERROR : CommandNode({{T}})#{{@def.name}} : No subcommand matched and no #main defined"
|
||||
{% end %}
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,203 @@
|
||||
require "./arg"
|
||||
|
||||
module CliGen
|
||||
abstract class BaseFlag
|
||||
getter var : String
|
||||
getter short : String?
|
||||
getter long : String
|
||||
getter long_key : String
|
||||
getter env_var : String
|
||||
getter description : String
|
||||
|
||||
def initialize(
|
||||
@var : String,
|
||||
@short : String?,
|
||||
@long : String,
|
||||
@env_var : String,
|
||||
@description : String
|
||||
)
|
||||
# if the user provides just a "--long" I want the @long_key to match it
|
||||
if @long.includes(" ")
|
||||
@long_key = @long.split(" ").first
|
||||
else
|
||||
@long_key = @long
|
||||
end
|
||||
end
|
||||
|
||||
def matches?(token : String) : Bool
|
||||
token == @short || (!@long_key.empty? && token == @long_key)
|
||||
end
|
||||
|
||||
abstract def satisfied? : Bool
|
||||
abstract def validate! : Nil
|
||||
abstract def raw_value : String?
|
||||
abstract def check! : Nil
|
||||
end
|
||||
|
||||
class Flag(T) < BaseFlag
|
||||
@value : T?
|
||||
@default : T?
|
||||
@options : Array(T)?
|
||||
@validate : (T -> Bool)?
|
||||
@on_match : Proc(Nil)?
|
||||
|
||||
def initialize(
|
||||
var : String,
|
||||
short : String?,
|
||||
long : String,
|
||||
env_var : String,
|
||||
description : String,
|
||||
@default : T? = nil,
|
||||
@options : Array(T)? = nil,
|
||||
@validate : (T -> Bool)? = nil,
|
||||
@on_match : Proc(Nil)? = nil
|
||||
)
|
||||
super(var, short, long, env_var, description)
|
||||
end
|
||||
|
||||
def requires_arg? : Bool
|
||||
{{T}} != Bool
|
||||
end
|
||||
|
||||
def process(argv : Array(Arg) = [] of Arg) : Nil
|
||||
if requires_arg?
|
||||
raise "ERROR : Flag({{T}}) : Array requires an argument but provided array is empty" if argv.empty?
|
||||
end
|
||||
|
||||
{% if T == Bool %}
|
||||
@value = true
|
||||
{% elsif T <= Array %}
|
||||
{% raise "ERROR : Flag(#{T}) : You cannot define multiple types of array entries" if T.type_vars.size > 1 %}
|
||||
{% elem = T.type_vars.first %}
|
||||
argv.each do |arg|
|
||||
break if arg.value.starts_with('-')
|
||||
{% if elem == Int32 %}
|
||||
(@value ||= [] of Int32) << arg.value.to_i
|
||||
{% elsif elem == String %}
|
||||
(@value ||= [] of String) << arg.value
|
||||
{% elsif elem < CliGen::Coercable %}
|
||||
if arg.value.includes?(",")
|
||||
(@value ||= [] of {{elem}}) += arg.value.split(",").map{|i| {{elem}}.coerce(i)}
|
||||
else
|
||||
(@value ||= [] of {{elem}}) << {{elem}}.coerce(arg.value)
|
||||
end
|
||||
{% else %}
|
||||
{% raise "ERROR : Flag(#{T}) : #{elem} is not a coercable type. If you wish to coerce it from a bare string include CliGen::Coercable & implement the class method" %}
|
||||
{% end %}
|
||||
arg.processed
|
||||
end
|
||||
{% elsif T == Int32 %}
|
||||
@value = argv.first.value.to_i
|
||||
argv.first.processed
|
||||
{% elsif T == Time %}
|
||||
@value = parse_time(argv.first.value)
|
||||
argv.first.processed
|
||||
{% elsif T == String %} # String
|
||||
@value = argv.first.value
|
||||
argv.first.processed
|
||||
{% elsif T < CliGen::Parsable %}
|
||||
processed = argv.select(&.processed?)
|
||||
@value = T.parse_args(argv)
|
||||
post_processed = argv.select(&.processed?)
|
||||
if processed == post_processed
|
||||
raise "ERROR : Flag({{T}}, long: #{@long_key})#process : Your {{T}}#process_args did not mark processed args as processed. Please check your code"
|
||||
end
|
||||
{% else %}
|
||||
{% raise "ERROR : Flag({{T}}, long: #{@long_key})#process : Generic Type #{T} is not supported. To add support you must include CliGen::Parsable & implement the class method" %}
|
||||
{% end %}
|
||||
|
||||
validate!
|
||||
@on_match.try(&.call)
|
||||
end
|
||||
|
||||
def value! : T
|
||||
v = @value
|
||||
|
||||
# Priority: provided arg → env var → default → abort
|
||||
if v.nil?
|
||||
if raw = ENV[@env_var]?
|
||||
v = coerce(raw)
|
||||
end
|
||||
end
|
||||
|
||||
v ||= @default
|
||||
|
||||
raise "#{CliGen::APPNAME}: required flag #{@long_key} was not provided" if v.nil?
|
||||
v.not_nil!
|
||||
end
|
||||
|
||||
def raw_value : String?
|
||||
{% if T == Bool %}
|
||||
@value.try(&.to_s)
|
||||
{% elsif T <= Array %}
|
||||
@value.try(&.join(","))
|
||||
{% else %}
|
||||
@value.try(&.to_s)
|
||||
{% end %}
|
||||
end
|
||||
|
||||
def satisfied? : Bool
|
||||
return true if !@value.nil?
|
||||
return true if @env_var && ENV[@env_var]?
|
||||
return true if !@default.nil?
|
||||
false
|
||||
end
|
||||
|
||||
def validate! : Nil
|
||||
v = value!
|
||||
|
||||
if opts = @options
|
||||
abort "#{CliGen::APPNAME}: '#{v}' is not a valid value for #{@long_key} (valid: #{opts.join(", ")})" unless opts.includes?(v)
|
||||
end
|
||||
|
||||
if check = @validate
|
||||
abort "#{CliGen::APPNAME}: validation failed for #{@long_key}" unless check.call(v)
|
||||
end
|
||||
end
|
||||
|
||||
def check!
|
||||
raise "ERROR : Flag({{T}}, long: #{@long})#check! : -h is reserved for internal help usage" if @short == "-h"
|
||||
raise "ERROR : Flag({{T}}, long: #{@long})#check! : --help is reserved for internal help usage" if @long_key == "--help"
|
||||
end
|
||||
|
||||
private def coerce(raw : String) : T
|
||||
{% if T == Bool %}
|
||||
raw == "true" || raw == "1"
|
||||
{% elsif T == Int32 %}
|
||||
raw.to_i
|
||||
{% elsif T == Time %}
|
||||
parse_time(raw)
|
||||
{% elsif T <= Array %}
|
||||
{% elem = T.type_vars.first %}
|
||||
{% if elem == Int32 %}
|
||||
raw.split(',').map(&.to_i)
|
||||
{% elsif elem == String %}
|
||||
raw.split(',')
|
||||
{% elsif elem < CliGen::Coercable %}
|
||||
raw.split(',').map{|i| {{elem}}.coerce(i)}
|
||||
{% else %}
|
||||
{% raise "ERROR : Flag(#{T}) : #{elem} is not a coercable type. If you wish to coerce it from a bare string include CliGen::Coercable & implement the class method" %}
|
||||
{% end %}
|
||||
{% elsif T == String %} # String
|
||||
raw
|
||||
{% elsif T < CliGen::Coercable %}
|
||||
T.coerce(raw)
|
||||
{% else %}
|
||||
{% raise "ERROR : Flag(#{T}) : #{T} is not a coercable type. If you wish to coerce it from a bare string include CliGen::Coercable & implement the class method" %}
|
||||
{% end %}
|
||||
end
|
||||
|
||||
private def parse_time(raw : String) : Time
|
||||
formats = [
|
||||
CliGen::Regex::INPUT_DATETIME_REGEX,
|
||||
CliGen::Regex::INPUT_DATE_REGEX
|
||||
]
|
||||
abort "#{CliGen::APPNAME}: invalid date/time format '#{raw}' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)" unless formats.any? { |f| raw.match(f) }
|
||||
if raw.match(formats[0])
|
||||
Time.parse_local(raw, CliGen::Format::INPUT_DATETIME_FORMAT)
|
||||
else
|
||||
Time.parse_local(raw, CliGen::Format::INPUT_DATE_FORMAT)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
require "./flag"
|
||||
|
||||
module CliGen
|
||||
GLOBAL_FLAGS = [] of BaseFlag
|
||||
|
||||
macro add_global_flag(type, long, description, env_var = nil, short = nil, validation = nil, &on_match)
|
||||
{% raise "ERROR : CliGen.add_global_flag : type must be a TypeNode" unless type.is_a? TypeNode %}
|
||||
{% raise "ERROR : CliGen.add_global_flag : long must begin a StringLiteral" unless long.is_a? StringLiteral %}
|
||||
{% raise "ERROR : CliGen.add_global_flag : long must begin with --" unless long =~ /^--/ %}
|
||||
{% if short %}
|
||||
{% raise "ERROR : CliGen.add_global_flag : Short must be a StringLiteral" unless short.is_a? StringLiteral %}
|
||||
{% raise "ERROR : CliGen.add_global_flag : Short must be a - with single char (ex: -a)" unless short =~ /^-[a-zA-Z]/ %}
|
||||
{% end %}
|
||||
{% if validation %}
|
||||
{% raise "ERROR : CliGen.add_global_flag : validation must be a Proc" unless validation.is_a? ProcLiteral %}
|
||||
{% raise "ERROR : CliGen.add_global_flag : validation proc return type must be nil \"->(...) : Nil {...}\"" unless validation.is_a? ProcLiteral %}
|
||||
{% raise "ERROR : CliGen.add_global_flag : validation proc must have a single input variable" unless validation.args.size == 1%}
|
||||
{% arg = validation.args.first %}
|
||||
{% raise "ERROR : CliGen.add_global_flag : validation proc input variable MUST be typed to match the flag type \"->(#{arg.name} : #{type}) : Nil { ... }\"" unless arg.restriction == type %}
|
||||
{% end %}
|
||||
{% raise "ERROR : CliGen.add_global_flag : decription must be a StringLiteral" unless description.is_a? StringLiteral %}
|
||||
{% if env_var %}
|
||||
{% raise "ERROR : CliGen.add_global_flag : env_var must be a StringLiteral" unless env_var.is_a? StringLiteral %}
|
||||
{% else %}
|
||||
{% env_var = long.gsub(/--/, "").upcase %}
|
||||
{% end %}
|
||||
CliGen::GLOBAL_FLAGS << CliGen::Flag({{type}}).new(
|
||||
var: "",
|
||||
short: {{short}},
|
||||
long: {{long}},
|
||||
description: {{description}},
|
||||
env_var: {{env_var}},
|
||||
on_match: {% if on_match %} ->() : Nil { {{on_match.body}} } {% else %} nil {% end %},
|
||||
validation: {% if validation %} {{validation}} {% else %} nil {% end %}
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
module CliGen
|
||||
enum MatchType
|
||||
FlagWithArg
|
||||
FlagMultipleShort
|
||||
ShortWithInlineArg
|
||||
SubCommand
|
||||
Help
|
||||
NoMatch
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
module CliGen::Parseable
|
||||
abstract def parse_args(args : Array(CliGen::Arg)) : self
|
||||
end
|
||||
@@ -1,4 +1,9 @@
|
||||
module CliGen::Regex
|
||||
FLAG_REGEX=/^(-[a-zA-Z]|--[a-zA-Z-_]+)$/
|
||||
FLAG_WITH_ARG=/^(?<flag>(-[a-zA-Z]|--[a-zA-Z-_]+))="?(?<arg>\S+?)"?$/
|
||||
FLAG_MULTIPLE_SHORT=/^-[a-zA-Z]+$/
|
||||
SHORT_WITH_INLINE_ARG=/^-[a-zA-Z][a-zA-Z0-9]+$/
|
||||
|
||||
INPUT_DATETIME_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/
|
||||
INPUT_DATE_REGEX = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/
|
||||
end
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
Command: <% @name %>
|
||||
<%- unless @description.nil? -%>
|
||||
Description: <%= @description %>
|
||||
<%- end -%>
|
||||
|
||||
Flags:
|
||||
---------------------------------------------------------------
|
||||
<%- @flags.each do |flag| -%>
|
||||
<%- unless flag.short.nil? -%>
|
||||
<%= "%-10s %s" % ["#{flag.short},#{flag.long}", flag.description] %>
|
||||
<%- else -%>
|
||||
<%= "%-10s %s" % [flag.long, flag.description] %>
|
||||
<%- end -%>
|
||||
|
||||
<%- end -%>
|
||||
<%- unless @commands.empty? -%>
|
||||
Other Commands
|
||||
---------------------------------------------------------------
|
||||
<%- @commands.each do |command| -%>
|
||||
<%= "%-10s %s" % [ command.name, command.description ] %>
|
||||
<%- end -%>
|
||||
|
||||
<%- end -%>
|
||||
<%- if subcommands? -%>
|
||||
SubCommands of <%= @name %>:
|
||||
---------------------------------------------------------------
|
||||
<%- subcommands.each do |cmd| -%>
|
||||
<%= "%-10 %s" % [cmd.name, cmd.description] %>
|
||||
<%- end -%>
|
||||
|
||||
<%- cmds = subcommands.select{|c| ! c.examples.nil? } -%>
|
||||
<%- unless cmds.empty? -%>
|
||||
Examples:
|
||||
---------------------------------------------------------------
|
||||
<%- cmds.map(&.examples).flatten.each do |example| -%>
|
||||
<%= example %>
|
||||
<%- end -%>
|
||||
|
||||
<%- end -%>
|
||||
<%- end -%>
|
||||
|
||||
Note: When wanting help output of any command you can provide the -h/--help flags or help command
|
||||
Reference in New Issue
Block a user