Comandante

A CLI toolkit including an option parser and some helper commands to make life easier.

Features

Installation

  1. Add the dependency to your shard.yml:
dependencies:
  comandante:
    github: tghaleb/comandante
  1. Run shards install

Examples

In Commandante project directory:

make examples
./examples/prog1 -h
./examples/prog2 --help
./examples/prog1-config --debug ./examples/prog1-config.yaml

Take a look at the code in examples/ and optionally in spec/.

Usage

require "comandante"

OptParser

OptParser can be used to parse arguments for interfaces with simple arguments as well as those that have sub commands. For a very basic CLI create a new OptParser.

@opts = OptParser.new(NAME, LABEL,
  arguments_string: "FILE",
  arguments_range: 1..1)

arguments_range for argument count checking.

Add an option, unless you specify the option type it is a Swtich

@opts.append_option(
  OptParser::OptionConfig.new(
   name: "debug",
   label: "debug mode",
   simple_action: OptParser::OptProc.new { |v| Cleaner.debug = v.as(Bool) }
 ))
@opts.append_option(
  OptParser::OptionConfig.new(
   name: "verbose",
   label: "verbose mode",
   simple_action: OptParser::OptProc.new { |v| Cleaner.verbose = v.as(Bool) }
 ))

The simple_action takes a proc that sets debug mode. Another option that you can use is action which takes a class derived from OptionAction. Use it when simple_action is not enough.

@opts.parse

Helper.debug_inspect(@opts.args)
Helper.debug_inspect(@opts.options)

debug_inspect will display debug messages if the user uses the --debug switch.

For a full example take a look at examples/

There is also debug and debug_pretty macros, which are wrappers around the above but will not be included when compiled in --release mode. These are the ones to use when maximum performance is required, to avoid any overhead of the debug calls. Just require:

require "comandante/macros"

and include Macros where you want to use them.

For subcommands you create a class derived from CommandAction.

class Eval < CommandAction
  def run(
    global_opts : OptionsHash,
    opts : OptionsHash,
    args : Array(String)
  ) : Nil
    Helper.debug_inspect global_opts
    Helper.debug_inspect opts
    Helper.debug_inspect args
    opts["src"].as(Array(String)).each do |f|
      File.open(f) do |io|
        "will read " + f
      end
    end
  end
end

Create configuration for the sub command

EVAL_OPTS = CommandConfig.new(
  name: "eval",
  label: "Evaluates some sources.",
  description: <<-E.to_s,
            Will evaluate something.
            And can accept repeating --src options

            Example:
              #{NAME} eval --src one --src two
            E
  action: Commands::Eval.new,
  arguments_range: 0..0,
  options: [
    OptionConfig.new(
      name: "src",
      short: "s",
      label: "src of file(s) to eval.",
      argument_string: "FILE",
      option_type: OptionStyle::RepeatingOption,
    ),
  ],
)

Here the action is the command object. For the option src we are using a RepeatingOption. Aappend the configuration to the parser.

opts.append(EVAL_OPTS)

For a complete example take a look at examples/

Cleaner

You wrap your code in a Cleaner.run

Cleaner.run do |x|
  opts.parse
  tempfile = Cleaner.tempfile
  do_stuff(temfile)

  tempdir = Cleaner.tempdir
  do_stuff(tempdir)

  raise "something wrong happend"
end

Cleaner.run will remove created tempfiles and temp directories on exiting the block. It will also catch any raised exceptions and exit printing the error message, or, if in debug mode, it will print a colorized backtrace.

For temp files and directories you will probably want to use a block, to make sure files are removed as soon as you're done with them.

Cleaner.run do |x|
  Cleaner.tempfile do |file|
    # do something with file
  end
  # file will be removed
end

You can register cleanup procs that will be run on block exit, or when a Cleaner explicit exit is called.

Cleaner.register -> { puts "will do cleanups" }

In cases you want to exit with an error, or just exit cleanly, you can run

if File.file? file
  Cleaner.exit_success
else
  Cleaner.exit_failure("Failed to create file")
end

Both will run a cleanup job before they exit, running any registered cleaners in addition to removing tempfiles and temp directories.

Helper

# printing messages
Helper.debug_msg("Will print this in debug mode"
Helper.debug_inspect(@opts)
Helper.put_error("Something went wrong")
Helper.put_verbose("Creating files")

# running commands
Helper.run("touch newfile")

# asserts
Helper.assert(x < 5, "Not in range")
Helper.assert_file(file)
Helper.assert_directory(dir)

# reading files
@config = Helper.read_yaml(file)
@config = Helper.parse_yaml(str, context_str)

Helper.read_gzip(file) do |line|
  puts line
end

s = Helper.read_gzip(path)

# Create a directory with a verbose option
Helper.mkdir(dir, verbose: true)

# digests for files/strings
puts Helper.file_md5sum(path)
puts Helper.file_sha1sum(path)
puts Helper.string_sha256sum("test")
puts Helper.string_sha512sum("test")

# Times a job and prints time it took
Helper.timer {
 long_job
}

Bits

x = 5.to_u16
y = 12.to_u64

Bits.set_bits(x, [1, 3, 7, 8])

# test if a bit is set
do_something if Bits.bit_set?(3, y)

z = Bits.get_bits(y, from: 2, count 3)

z = Bits.popcount_bits(y, from: 1, count 3)

puts Bits.bits_to_string(y)

Bits.loop_over_set_bits(x) do |i|
  puts "bit #{i} is set"
end

# store int as uint and vice versa, for example if you want
# to save uints in an sqlite database.
m = bits.store_as_int(x)
u = bits.store_as_uint(m)

see spec/

Config

A ConfigSingleton that simplifies loading config from a yaml file. You use the included macro config_type, for example:

  class Config < ConfigSingleton
    config_type(MyConfig) do
      name : String = "foo"
      age : Int32 = 150
    end
...

Which creates accessors on both the instance and on the Config module. And you can pass a yaml config file to initialize the instance like so:

Config.load_config("config.yaml")

puts Config.to_yaml

puts Config.name
puts Config.age
...

You can also add a validator for the config data,

def self.validate
  if self.age > 200
    self.exit_error("bad age #{self.age}")
  end
end

Which you can call by calling

Config.validate

You can also create complex types that are classes and nest config, just make sure to create them with sub_config_type see examples,

sub_config_type(URLConfig) do
  ...
end

Take a look at the code in examples/.