An open API service indexing awesome lists of open source software.

https://github.com/papierkorb/toka

A type-safe, object-oriented option parser
https://github.com/papierkorb/toka

cli crystal mapping options options-parsing type-safe

Last synced: about 1 month ago
JSON representation

A type-safe, object-oriented option parser

Awesome Lists containing this project

README

        

# Toːka [![Build Status](https://travis-ci.org/Papierkorb/toka.svg?branch=master)](https://travis-ci.org/Papierkorb/toka)

A type-safe, object-oriented option parser using the mapping-pattern.

## Simple usage

**Note**: If you're unfamiliar with UNIX-style argument passing, see the
[explanation below](#explanation-of-argument-passing).

Doesn't get much simpler than this:

```crystal
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!

### A note on running sample code

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](#explanation-of-argument-passing).

## Advanced usage

`Toka` can do much more than that. To adjust further settings, you can pass
a `NamedTuple` as value too!

First, an example:

```crystal
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:

* `type` The type. Examples: `String`, `Int32?`, `Array(String)`
* `nilable` If the type is optional ("nil-able"). You can also make the `type` nilable for the same effect.
* `default` The default value.
* `long` Allows to manually configure long-options. Auto-generated from the name otherwise.
* `short` Allows to manually configure short-options. Auto-generated otherwise. Set to `false` to disable.
* `converter` Converter to use for the value. See below.
* `value_converter` Alias for `converter`.
* `key_converter` Converter for the key to use for a `Hash` type.
* `description` The human-readable description. Can be multi-line.
* `value_name` Human-readable value name, shown next to the option name like: `--foo=HERE`
* `category` Human-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:

* `banner` The banner string. Displayed as first line(s) in the help page.
* `footer` The footer string. Displayed as last line(s) in the help page.
* `help` If to auto-generate the `--help`/`-h` option. Defaults to `true`.
* `colors` If the help page shall be colorized. Defaults to `true`.

## Positional options

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" ]`.

## Converters

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.

```crystal
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
},
})
end
```
## Input verification

It 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.

```crystal
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" }
}
})
end
```

**Note**: 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.

## Error handling

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.

## Sequential and associative options

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!

```crystal
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
},
})
end
```

**Note**: The parser is restricted to `Array` and `Hash`. You can't use other
generic types.

## Boolean behaviour

`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**.

### Bool in containers

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 }`.

## Generation behaviour

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: String` will 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.
* `Bool` type options get both a getter with question-mark and one without: `#verbose` is the same as `#verbose?`
* A `--help` page is generated. Upon activation through the user, the process is exited!

## Installation

Add this to your application's `shard.yml`:

```yaml
dependencies:
toka:
github: Papierkorb/toka
```

## Explanation of argument passing

This 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.

### Long- and short-options

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`.

### Value passing

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".

### Cancelling option parsing

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:

1. Escape it by prepending a back-slash: `\--foo`
2. Ignore everything following by stand-alone double-dash: `--foo -- --bar`
will activate the "foo" switch, but pass "--bar" as positional option.

## Contributing

1. Fork it ( https://github.com/Papierkorb/toka/fork )
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Add some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create a new Pull Request

## Contributors

- [Papierkorb](https://github.com/Papierkorb) Stefan Merettig - creator, maintainer