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
- Host: GitHub
- URL: https://github.com/papierkorb/toka
- Owner: Papierkorb
- License: mit
- Created: 2017-08-11T16:27:48.000Z (almost 8 years ago)
- Default Branch: master
- Last Pushed: 2023-07-08T20:44:26.000Z (almost 2 years ago)
- Last Synced: 2025-04-30T12:15:15.719Z (about 2 months ago)
- Topics: cli, crystal, mapping, options, options-parsing, type-safe
- Language: Crystal
- Size: 40 KB
- Stars: 9
- Watchers: 2
- Forks: 3
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Toːka [](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
endclass 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 verificationIt 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