module CliGen

Defined in:

cligen.cr
cligen/annotations.cr
cligen/app.cr
cligen/app/generate.cr
cligen/arg.cr
cligen/command.cr
cligen/command/argument.cr
cligen/command/define_command_initializer.cr
cligen/command/define_singleton_init.cr
cligen/command/generate_gather_handler.cr
cligen/command/generate_register_command.cr
cligen/command/help_template.cr
cligen/command/resolve_value.cr
cligen/command/subcommand.cr
cligen/command/validate_command_tree.cr
cligen/command_node.cr
cligen/command_node/base.cr
cligen/command_node/command_meta.cr
cligen/command_node/subcommand_meta.cr
cligen/exceptions.cr
cligen/flag.cr
cligen/flag/base.cr
cligen/flag/meta.cr
cligen/global_flag.cr
cligen/global_flag/add_global_flag.cr
cligen/match_type.cr

Constant Summary

APPNAME = File.basename(PROGRAM_NAME)
GLOBAL_FLAGS = [] of BaseFlag
MAX_COMMAND_DEPTH = 32

CliGen::MAX_COMMAND_DEPTH

This exists to prevent the user from defining a command tree that extends past the compile-time configured max via the CliGen::MAX_COMMAND_DEPTH constant.

The reason this is a thing is because crystal macros don't allow for unbounded while's/until's in macros, meaning it always has to be deterministic. SO to deal with this and still allow for subcommand defining you need either go with the default (32 command depth) or define your own larger max (understand this will affect compile-time due to this directly affecting loops in the Command macros).

So to still support this I had to make bounded for-loops usng

{% for i in (1..CliGen::MAX_COMMAND_DEPTH) %}
  ...do checks...
{% end }
VERSION = "0.2.0"

Macro Summary

Macro Detail

macro add_global_flag(type, *, long, description, env_var = "", short = nil, validation = nil, default = nil, on_match = nil, options = nil, format = nil, internal = false) #

This macro provides a user-friendly way to define a global flag for your project.

What does this do?

This macro is used to help define & check a global flag to be used in the all levels of commands.

When provided it will parse your values & serialize them into a Flag(T) object & insert it in the CliGen::GLOBAL_FLAGS array after checking if a flag using it's --long is already in use. In the case that that long is already used it will raise at runtime and you'll need to choose another long.

Arguments

type: TypeNode

Required: true

This is the type of the flag (Bool, Int32, String, etc).

long: StringLiteral

Required: true

This is the long form of the flag that will be matched at the command-line

description: StringLiteral

Required: true

This is the full length description of the flag that will be presented in the help text provided to the user.

env_var: StringLiteral

Required: false

This is an ENV VAR that can be used to set this value without providing an argument via the CLI. By default it will (unless explicitly disbled by passing env_var: nil as an argument to disable the env_var entirely) will parse your long flag and set the ENV VAR to the un "--" portion of it

Warning: Incompatible ENV VAR formatting

When providing ENV VARs manually you cannot provide any whitespace or "-" characters internally to it. As thse are both incompatible with ENV VARs.

If you provide an ENV VAR with these the framework will raise at compile-time and tell you to change them.

Note: Auto Generates ENV VAR from flag long

If you did not provide a ENV VAR manually (or disable it via setting it to nil), the macro will use the long flag to create a ENV VAR that can be matched. In this case if the flag has any internal "-" chars they will be replaced with "_" so "--long--flag--name"/"--long-flag-name" -> "LONG_FLAG_NAME".

When you provide a long: with a trailing ARGUMENT (ex: "--item ITEM", "--item=ITEM") the flag will first be split on the whitespace or "=" prior to being used for the ENV_VAR.

short: StringLiteral

Required: false

This is the short form of a flag ("--filename" -> "-f") that can be matched during parsing.

Note: Alphabetic characters only

Unlike some other frameworks that might support numeric flags, due to the issues around supporting them & being able to discern if these are arguments (-1/signed int's) or short flags ("--one" -> "-1"), I've determined that I will not be supporting numeric flags as this causes a number of complications/complexities around ARGV parsing.

default: T

Required: ?false?

This is the default value of the flag (String -> "abc", Int32 -> 0, etc) that will be returned if no direct (via parsing CLI args) or indirect (by parsing ENV VAR values) arguments are provided.

While not technically required, it's advised to always set a default when creating flags as if you don't and nothing is parsed/provided when Flag(T)#value! is called it will raise a CliGen::MissingRequiredFlagError exception at the call site.

options: ArrayLiteral(T)|Call

Required: false

This argument sets a static list of accepted arguments to a specific subset of values.

EX: Output format

CliGen.add_global_flag(String,
                       default: "ecr",
                       short: "-f",
                       long: "--format",
                       description: "Provide the preferred output format",
                       options: %w[ json yaml ecr ]
)

Note: Support for runtime resolution

While the primary value of this is static arrays of values, you can also delegate the discovery of values to a global method or helper method in your codebase.

HOWEVER, when doing so ALWAYS ensure that you are providing a full path to your method, as the the macro has no way of determining relative paths in your modules. While, provided you are doing this in the same context as the method you are running, this shouldn't be an issue, however best practices dictate you provide a full path just to be careful.

EX: Delegated resolution

module ABC
  def self.items
    %w[ a b c d e f g taco ]
  end
end

CliGen.add_global_flag(String,
                       default: "a",
                       short: "-i",
                       long: "--item",
                       description: "Provide an item to print",
                       options: ::ABC.items
)

format: RegexLiteral

Required: false

This exists to handle (for String & Custom Data Types) filtering & checking that an argument being provided by a user is being given in a specific format.

This is something you use when you're only wanting to validate formatting, if you plan to do more specific/extensive validation you should use the validation: field.

EX: Hostname matching

CliGen.add_global_flag(Array(String),
                       default: [] of String,
                       short: "-H",
                       long: "--hostname",
                       description: "Provide a hostname to do remote work on",
                       format: /^[a-zA-Z]{3}[0-9]+node[0-9]$/
)

validation: ProcLiteral(T, Bool)

Required: false

Here you can provide a ad-hoc proc for doing validations of a provided argument that can't easily be done by providing a static options: value.

Note: Explicit input & return type requirement

The explicit input : T & return : Bool turn types are required as the macros I setup are trying to enforce that both the input & return types are explicity to avoid truthy & falsey semantics.

EX: checking int range

CliGen.add_global_flag(Int32,
                       short: "-p",
                       long: "--port",
                       description: "Provide a single port to test against",
                       validation: ->(port : Int32) : Bool do
                         (UInt16::MIN..UInt16::MAX).includes?(port)
                       end
)

EX: file existance check

CliGen.add_global_flag(String,
                       short: "-i",
                       long: "--filename",
                       description: "Provide a file that will serve as the input for this program",
                       validation: ->(file : String) : Bool do
                         if File.exists?(file)
                           true
                         else
                           STDERR.puts "ERROR : --filename : Provided file (#{file}) does not exist"
                           false
                         end
                       end
)

on_match: ProcLiteral(T, Nil)

Required: false

This option is where you provide the proc for handling ad-hoc

EX: Configuring the stdlib log level

CliGen.add_global_flag(String,
                       long: "--log-level LEVEL",
                       short: "-l",
                       description: "Set the current log level of the stdlib Log library",
                       options: %w[ trace debug notice info warn error fatal ],
                       on_match: ->(level : String) do
                         ::Log.setup(level: ::Log::Severity.parse(level))
                       end
)

EX: Collecting arguments in a global array

module MyModule
  MY_ARRAY = [] of String 
  CliGen.add_global_flag(String,
                         long: "--filename FILE",
                         short: "-i",
                         description: "Provide a single file to check against (repeatable)",
                         validation: ->(file : String) : Bool do
                            if File.exists?(file)
                              true
                            else
                              STDERR.puts "ERROR : --filename : #{file} does not exist"
                              false
                            end
                         end,
                         on_match: ->(file : String) do
                           ::MyModule::MY_ARRAY << file
                         end
  )   
end

For more detailed documentation please visit the wiki in the repo. All topics are covered there in much greater detail than inline documentation here


macro override_help_template(filepath) #