A type-safe, object-oriented option parser using the mapping-pattern.
Note: If you're unfamiliar with UNIX-style argument passing, see the explanation below.
Doesn't get much simpler than this:
require "toka"
class MyOptions # Create a container class
  Toka.mapping({ # Don't forget the opening braces!
    name: String, # Mandatory option
    last_name: String?, # Optional option
    verbose: Bool, # Mandatory option
  })
end
# Now, create an instance:
opts = MyOptions.new # It will use `ARGV` by default!
# And access the fields.  Bool-type fields have an question-mark getter too!
puts "Hello, #{opts.name} #{opts.last_name}" if opts.verbose?With this, the user can pass something like --name Alice --no-verbose, or
just --help (or -h) to print a help page.
Hint You can find usage-examples in samples/ !
Note: The default help page renderer will exit() the process after
printing its output!
If you're running the sample code directly through crystal like this:
$ crystal samples/simple.cr --name=Me --verbose
you'll get a nasty error message from crystal that it doesn't know "--name".
For this to work, you have to tell crystal to stop processing arguments by adding a "--" between the arguments for the sample program and the source file:
$ crystal samples/simple.cr -- --name=Me --verbose
If this is news to you, consider taking a refresher on UNIX-style argument passing.
Toka can do much more than that.  To adjust further settings, you can pass
a NamedTuple as value too!
First, an example:
class MyOptions
  Toka.mapping({ # Still don't forget the opening braces!
    name: { # This is the settings (named-)tuple
      type: String, # Still a String
      default: "World", # But greet the World if none was given
      description: "Whom to greet", # For the help page
      value_name: "NAME", # Same ^
    },
    last_name: {
      type: String?,
      # nilable: true, # Alternatively, write `type: String` and this
    },
    verbose: {
      type: Bool?, # Trick to detect explicit activation and deactivation
    },
  }, { # Optionally, the info tuple
    banner: "Usage: my_cool_tool [--name]",
    footer: "I'm at the bottom!",
  })
end
# Now, create an instance:
opts = MyOptions.new # It will use `ARGV` by default!
# And access the fields.  Bool-type fields have an question-mark getter too!
puts "Hello, #{opts.name} #{opts.last_name}" if opts.verbose?The settings tuple supports the following options:
- typeThe type. Examples:- String,- Int32?,- Array(String)
- nilableIf the type is optional ("nil-able"). You can also make the- typenilable for the same effect.
- defaultThe default value.
- longAllows to manually configure long-options. Auto-generated from the name otherwise.
- shortAllows to manually configure short-options. Auto-generated otherwise. Set to- falseto disable.
- converterConverter to use for the value. See below.
- value_converterAlias for- converter.
- key_converterConverter for the key to use for a- Hashtype.
- descriptionThe human-readable description. Can be multi-line.
- value_nameHuman-readable value name, shown next to the option name like:- --foo=HERE
- categoryHuman-readable category name, for grouping in the help page. Optional.
Note: The long and short fields can take a single string, or an
array of strings.  Do not prepend dashes yourself, Toka will do that!
The info argument allows the following options:
- bannerThe banner string. Displayed as first line(s) in the help page.
- footerThe footer string. Displayed as last line(s) in the help page.
- helpIf to auto-generate the- --help/- -hoption. Defaults to- true.
- colorsIf the help page shall be colorized. Defaults to- true.
All positional options are collected into a the #positional_options array.
This includes bare words, argument-looking words with a leading back-slash, and everything after a stand-alone double-dash "argument":
The line --one \--two three -- --four -five six would activate the "one"
switch, and collect the positional options like this:
[ "--two", "three", "--four", "-five", "six" ].
A converter is a module or class, responding to .read(raw_value : String) : T?.
On success, this method returns an instance of T, otherwise, it can
either return nil to prompt a default error message, or raise a more
descriptive one itself.
module IpV4Converter # Sample only!  Please do more error checking in your converters!
  def self.read(input : String) : Int32? # You can also just write `Int32`
    input.split(".", 4).map(&.to_u32).reduce(0u32){|x, a| (a << 8) | x}
  end
end
class MyOptions
  Toka.mapping({
    addr: { # Reacts to `--addr`
      type: UInt32, # The result will be a UInt32
      converter: IpV4Converter # And we want to use this converter
    },
  })
endIt is possible to verify input data before it's being used.  To do this,
pass a Proc through the verifier (or value_verifier) field setting.
For the key of a Hash type, key_verifier is what you're looking for.
This Proc gets passed the already converted value, and is then expected to
return either a false or a String to signal an error, or anything else
to signal success.
If the verifier responds with a false, the user will receive a generic
Toka::VerificationError.  If the response is a String, it will be
appeneded to its message for further context.
class MyOptions
  Toka.mapping({
    name: {
      type: String, #  vvvvvv Type is required for Crystal!
      verifier: ->(x : String){ x == "Bob" } # Only accepts "Bob" as input
    },
    age: {
      type: Int32, # Simple age restriction with additional message:
      verifier: ->(x : Int32){ x >= 18 || "Must be an adult" }
    }
  })
endNote: The verifier can be anything that responds to #call(), behaving
like a Proc.  You could also have a module which responds to
self.call(x), and pass in that module.
When an error is encountered while parsing the input, a sub-class of
Toka::Error is raised.  All error classes provide additional data to
the error handler by carrying additional fields, next to the standard
message.
Note: A converter or verifier raising a custom error are not handled by Toka. They're passed through. Albeit losing the additional information Toka errors provide, this is supported.
By using Array(T) or Hash(K, V) as type, you allow the user to pass in
multiple values for a single option.  They're added in the order they're
read: The left-most value will be the first one to be added, and so on.
For Array(T), the user has to repeat the option for each element to be
added. If you have an option called many of type Array(String), the user
passes --many one --many two --many three to generate
[ "one", "two", "three" ].
For Hash(K, V), the user repeats the option for each key-value pair,
separating the key from the value using an equal-sign ("=") like this:
--many foo=bar --many one=two will generate
{ "foo" => "bar", "one" => "two" }.
You're not restricted to using String.  You can use whichever type you
want.  The built-in ones will just work, for others, use a custom converter.
Converters will be invoked with one value each, and thus work out of the box.
Note: These containers are not nil-able. Instead, you'll get an empty container. You can still pass a default one though!
class MyOptions
  Toka.mapping({
    name: Array(String), # Simple usage
    ints: Array(Int32), # Works too
    streets: {
      type: Array(String),
      default: [ "Foo st", "Bar st" ], # Will only be used if none are given.
    },
    birthday: {
      type: Hash(String, Time), # Associative data
      key_converter: TitelizeName, # Converter for the key
      value_converter: TimeConverter, # Converter for the value
      # converter: TimeConverter, # Alias, equivalent to the one above
    },
  })
endNote: The parser is restricted to Array and Hash.  You can't use other
generic types.
Bool is somewhat special.  Switches of this type don't require a value.
If the user wants to explicitly set one, --switch=false has to be used.
A following true or false will not be detected.
Further, a Bool switch automatically gets two versions:  The active
version, and the inactive one.  The long-name for the inactive one is the
active name with a "no-" prepended: --verbose gets turned into
--no-verbose.  For the short-name, the existing short-names are taken and,
if no collisions are detected, inversed by uppercasing the character.  This
also works, if the short-name was auto-generated in the first place.  In
our example, the --verbose switch would be assigned -v as short switch,
and it would be uppercased to negate: From -v to -V.
As already noted, the user has to follow a value immediately if one is
passed.  Only the long-name supports this, the short-name does not.
So, this will work: --verbose=true, but this will not: -vtrue.
The following inputs will be turned into true: true, yes, t and y.
For false: false, no, f, n.  Other inputs will raise an error.
It's possible to mix containers with Bool, like Array(Bool) or
Hash(String, Bool).  No automatic (de-)activation switch is generated
for these cases, meaning the user has to explicitly set the value.
Sample for an array: -ayes -ayes -ano gives [ true, true, false ].
Hash sample: -afoo=yes -abar=no gives { "foo" => true, "bar" => false }.
The macro tries to do as much for you as possible, so here's what's possible:
- The long-name is generated from the dasherized name: foo_bar: Stringwill be turned into--foo-bar.
- The short-names are generated from the long-name, prefering the first character of each word of each long-name, and using any following characters afterwards. Done until an unique one is found, or none at all.
- Booltype options get both a getter with question-mark and one without:- #verboseis the same as- #verbose?
- A --helppage is generated. Upon activation through the user, the process is exited!
Add this to your application's shard.yml:
dependencies:
  toka:
    github: Papierkorb/tokaThis just covers the common UNIX-style option-passing.  You can skip this if
you're familiar with it - Toka implements it!
Note: Windows-style passing, using a leading slash (e.g. /f) instead
of dashes, are not supported.
First, options are split word-wise (According to the program calling another program, usually your shell). A "word" may actually consist out of multiple readable words separated by a space (" "), so don't get confused.
Second, there are two kinds of options: Switches, and positional options. The first kind are those which are "named", and are accessed directly by one of their names. Positional options are not: All options which do not look like an option ("bare words") are positional options.
Example: wc -l foo This invokes the wc utility, passing the -l switch,
and the foo positional option.
Many options have a long-name, and a short-name. (They may have further aliases). Long-names are longer than their short-name counterparts. They are distinguished by the leading count of dashes: Two ("--") for a long-name, and one ("-") for a short-name.
Long-names for an option are usually whole words, but can span multiple
words.  It is common to separate words using a single dash:
--street-number.  It is not possible to define multiple long-names at
once.  Sometimes long-names are case-sensitive, other times they're not.
Toka implements long-names case-sensitively.
Short-names are commonly one character only.  You can combine multiple
short-name switches into a single word: -abc will flip the switches
for a, b and c.  This is equivalent to doing the following: -a -b -c.
In many cases it's desirable to pass specific values to a switch for further configuration.
For long-names, the value can either follow in the same word by separating
the value from the long-name using an equal-sign ("="): --foo=bar would
pass the value "bar" to the "foo" switch.  If no equal-sign is found while
requiring a value, the following word is used: --foo bar is equivalent.
For short-names, the value immediately follows the short option: -fBar
would pass "Bar" to the "f" switch.  This is a common source of confusion
and mistakes: Say we have the switch "a" not taking a value, and "b" taking
one.  Now, you want to invoke both, and pass a value to "b". So you write
-ba foo - And suddenly, you passed "a" to the "b" switch as value, and
"foo" is treated as positional option.  Correct is this: -ab foo - This
activates the "a" switch, and passes "foo" to the "b" switch.  You can also
combine non-value-taking with value-taking short-names into the same word:
-abfoo will activate "a", abd pass "foo" to "b".
Sometimes it may be useful to tell the option parser that some options are to be treated as positional options. There are two solutions to this:
- Escape it by prepending a back-slash: \--foo
- Ignore everything following by stand-alone double-dash: --foo -- --barwill activate the "foo" switch, but pass "--bar" as positional option.
- Fork it ( https://github.com/Papierkorb/toka/fork )
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create a new Pull Request
- Papierkorb Stefan Merettig - creator, maintainer