Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/jojojet/type-cli
https://github.com/jojojet/type-cli
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/jojojet/type-cli
- Owner: JoJoJet
- Created: 2020-12-10T15:22:57.000Z (about 4 years ago)
- Default Branch: main
- Last Pushed: 2020-12-13T18:45:36.000Z (about 4 years ago)
- Last Synced: 2024-11-02T06:53:38.757Z (about 2 months ago)
- Language: Rust
- Size: 49.8 KB
- Stars: 13
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# type-cli
`type-cli` is a convenient, strongly-typed command-line interface parser.To start, let's create an interface for `grep`.
## Basics
```rust
use type_cli::CLI;#[derive(CLI)]
struct Grep(String, String);fn main() {
let Grep(pattern, file) = Grep::process();
let pattern = regex::Regex::new(&pattern).unwrap();eprintln!("Searching for `{}` in {}", pattern, file);
}
```Now, if we run the binary with arguments, they'll be properly parsed.
And if we miss an argument, it'll give a helpful error.```
$ grep foo* myFile
Searching for `foo*` in myFile$ grep foo*
Expected an argument at position `2`
```However, this isn't exactly a faithful grep interface: in grep, the file is optional. Plus, that `unwrap()` is a little gross.
#
```rust
use type_cli::CLI;#[derive(CLI)]
struct Grep(regex::Regex, #[optional] Option);fn main() {
match Grep::process() {
Grep(pattern, Some(file)) => eprintln!("Serching for `{}` in {}", pattern, file),
Grep(pattern, None) => eprintln!("Searching for `{}` in stdin", pattern),
}
}
```What's that? We're accepting a `Regex` directly as an argument? In `type-cli`, any type that implements `FromStr` can be an argument.
Any parsing errors will be gracefully passed back to the user without you having to worry about it.```
$ grep foo(
Error parsing positional argument `1`:
regex parse error:
foo(
^
error: unclosed group
```Here, you can also see that optional arguments must be annotated with `#[optional]`.
```
$ grep foo* myFile
Serching for `foo*` in myFile$ grep foo*
Searching for `foo*` in stdin
```This interface _still_ isn't faithful though; `grep` allows multiple files to be searched.
#
```rust
use type_cli::CLI;#[derive(CLI)]
struct Grep(regex::Regex, #[variadic] Vec);fn main(){
let Grep(pattern, file_list) = Grep::process();
if file_list.is_empty() {
eprintln!("Searching for `{}` in stdin", pattern);
} else {
eprint!("Searching for `{}` in ", pattern);
file_list.iter().for_each(|f| eprint!("{}, ", f));
}
}
```If you annote the final field with `#[variadic]`, it will parse an arbitrary number of arguments.
This works for any collection that implements `FromIterator`.```
$ grep foo*
Searching for `foo*` in stdin$grep foo* myFile yourFile ourFile
Searching for `foo*` in myFile, yourFile, ourFile,
```This still isn't ideal, though. None of the fields have names, and there's no flags or options!
Clearly, tuple structs are limiting us.## Named arguments and flags
```rust
use type_cli::CLI;#[derive(CLI)]
struct Grep {
pattern: regex::Regex,#[named]
file: String,#[flag(short = "i")]
ignore_case: bool,
}fn main() {
let Grep { pattern, file, ignore_case } = Grep::process();
eprint!("Searching for `{}` in {}", pattern, file);
if ignore_case {
eprint!(", ignoring case");
}
eprintln!();
}
```Named arguments are annoted with `#[named]`, and that allows them to be passed to the command in any order.
By default, named arguments are still required, but they can also be marked with `#[optional]`.```
$ grep foo*
Expected an argument named `--file`$ grep foo* --file myFile
Searching for `foo*` in myFile
```Flags are annoted with `#[flag]`, and are completely optional boolean or integer flags.
You can optionally specify a shorter form with `#[flag(short = "a")]` (this form also works for named arguments).```
$ grep foo* --file myFile --ignore-case
Searching for `foo*` in myFile, ignoring case$ grep foo* --file myFile -i
Searching for `foo*` in myFile, ignoring case
```This seems well and good, but what if I want multiple commands in my application?
## Subcommands
```rust
use type_cli::CLI;#[derive(CLI)]
enum Cargo {
New(String),
Build {
#[named] #[optional]
target: Option,
#[flag]
release: bool,
},
Clippy {
#[flag]
pedantic: bool,
}
}fn main() {
match Cargo::process() {
Cargo::New(name) => eprintln!("Creating new crate `{}`", name),
Cargo::Build { target, release } => {
let target = target.as_deref().unwrap_or("windows");
if release {
eprintln!("Building for {} in release", target);
} else {
eprintln!("Building for {}", target);
}
}
Cargo::Clippy { pedantic: true } => eprintln!("Annoyingly checking your code."),
Cargo::Clippy { pedantic: false } => eprintln!("Checking your code."),
}
}
```If you derive `CLI` on an enum, each variant will represent a subcommand.
Each subcommand is parsed with the same syntax as before.Rust's pascal case will be automatically converted to the standard for shells:
`SubCommand` -> `sub-command````
$ cargo new myCrate
Creating new crate `myCrate`$ cargo build
Building for windows$ cargo build --target linux
Building for linux$ cargo build --target linux --release
Building for linux in release$ cargo clippy
Checking your code.$ cargo clippy --pedantic
Annoyingly checking your code.
```What about documentation?
## --help
```rust
use type_cli::CLI;#[derive(CLI)]
#[help = "Build manager tool for rust"]
enum Cargo {
New(String),#[help = "Build the current crate."]
Build {
#[named] #[optional]
#[help = "the target platform"]
target: Option,#[flag]
#[help = "build for release mode"]
release: bool,
},#[help = "Lint your code"]
Clippy {
#[flag]
#[help = "include annoying and subjective lints"]
pedantic: bool,
}
}
````type-cli` will automatically generate a help screen for your commands.
If you annote a subcommand or argument with `#[help = ""]`, it will include your short description.
When shown, it will be sent to stderr and the process will exit with a nonzero status.```
$ cargo
Help - cargo
Build manager tool for rustSUBCOMMANDS:
new
build Build the current crate.
clippy Lint your code
```For enums, this will be shown if the command is called without specifying a subcommand.
```
$ cargo build --help
Help - build
Build the current crate.ARGUMENTS:
--target the target platform [optional]FLAGS:
--release build for release mode$ cargo clippy -h
Help - clippy
Lint your codeFLAGS:
--pedantic include annoying and subjective lints
```For structs or subcommands, this will be called if the flag `--help` or `-h` is passed.
Help messages are not currently supported for tuple structs.