Initial commit

This commit is contained in:
Tristan Ancelet
2026-02-23 10:30:03 -06:00
commit b91ecad7e5
10 changed files with 339 additions and 0 deletions

157
src/command.cr Normal file
View File

@@ -0,0 +1,157 @@
require "colorize"
require "time"
require "./command/parser"
module CliGenerator
annotation CommandPreRun
end
annotation CommandInfo
end
annotation CommandArgument
end
annotation SubCommand
end
class Command
extend CliGenerator::Parser
@@action : String = ""
macro inherited
macro finished
define_actions
define_header
define_runner
define_parser
end
end
macro define_actions
ACTIONS : Array(String) = {{@type.class.methods.select(&.annotation(::CliGenerator::SubCommand)).map(&.name.stringify)}}
end
macro define_header
{% name = @type.name.split("::").last.downcase.id %}
{% info_annos = @type.class.methods.select(&.annotation(::CliGenerator::SubCommand)).map(&.annotation(::CliGenerator::SubCommand)) %}
HEADER = [
"#{PROGRAM_NAME} {{name}} [[flags]]",
"",
{% unless info_annos.empty? %}
"Examples:".colorize(:green),
"-------------------------------------------------------------------------------".colorize(:blue),
{% for anno in info_annos %}
{% examples = anno[:examples].resolve %}
{% for example in examples %}
{{example}},
{% end %}
{% end %}
{% end %}
].join("\n")
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)
{% raise "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 "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 %}
@[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 format %}
abort "ERROR : Action : {{arg_name}} : Incorrect format for provided data #{var}" unless var =~ {{format}}
{% 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 = [
CliGenerator::Regex::INPUT_DATETIME_REGEX,
CliGenerator::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, CliGenerator::Format::INPUT_DATETIME_FORMAT)
elsif var =~ formats[1]
@@{{variable}} = Time.parse_local(var, CliGenerator::Format::INPUT_DATE_FORMAT)
end
{% elsif _type == "Array" %}
{% if _subtype == "Int32" %}
var : Int32 = var.to_i
{% elsif _subtype == "String" %}
{% else %}
var = {{subtype}}.new(var)
{% 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(::CliGenerator::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 %}
{% methods = @type.class.methods.select(&.annotation(::CliGenerator::SubCommand)) %}
{% raise "ERROR : No commands defined for #{@type.name}" if methods.empty? %}
{% begin %}
case @@action
{% for method in methods %}
when {{method.name.stringify}}
{{method.name}}
{% end %}
else
abort "ERROR : No action provided to command #{@type.name}"
end
{% end %}
end
end
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