Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/MSDimos/commander-rust
a more intuitive development method of CLI in Rust
https://github.com/MSDimos/commander-rust
Last synced: 13 days ago
JSON representation
a more intuitive development method of CLI in Rust
- Host: GitHub
- URL: https://github.com/MSDimos/commander-rust
- Owner: MSDimos
- License: mit
- Created: 2019-04-20T08:00:17.000Z (over 5 years ago)
- Default Branch: feature/pre-alpha
- Last Pushed: 2020-06-18T09:48:01.000Z (over 4 years ago)
- Last Synced: 2024-10-05T18:47:43.853Z (about 1 month ago)
- Language: Rust
- Homepage:
- Size: 1.94 MB
- Stars: 45
- Watchers: 4
- Forks: 5
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Glance
, "connect to the address")]
See `example/glance.rs` for more details.
```rust
#[default_options]
#[option(--https, "use https instead of http")]
#[sub_command(connect
fn connect(address: Option, opts: Opts, global_opts: GlobalOpts) {
/* .. */
}#[default_options]
, "disconnect the connection")]
#[sub_command(disconnect
fn disconnect(address: Option) {
/* .. */
}#[default_options]
#[option(--proxy , "use proxy to connect")]
#[command(net, "network tool")]
fn net() { /* .. */}fn main() {
execute!(net, [connect, disconnect]);
}
```# Note
The current `master` branch will no longer be supported.
And the branch `pre-alpha` will be the `master` branch once it's stable.# Usage
The current `commander_rust` in `crate.io` is no longer supported.
Before it becomes stable, `github` will be used as distribution source. Add it to your dependencies.
```toml
[dependencies.commander_rust]
git = "https://github.com/MSDimos/commander-rust/"
branch = "pre-alpha"# or
commander_rust = { git = "https://github.com/MSDimos/commander-rust/", branch = "pre-alpha" }
```# Viewpoint
As I think, developers should devote more time to the realization of functions instead of leaning how to use `command line interface (CLI)`.
So a crate of `CLI` should be easy to use.
Specifically, it should have the following advantages:- Firstly, it should have less `API`s.
- Secondly, it should be intuitive enough to use. What u see is what u get.
- Thirdly, it should make full use of the advantages of programming language.Inspired by [Rocket](https://rocket.rs/) and [commander.js](https://github.com/tj/commander.js/), the crate is born.
# Design concept
A `CLI` program consists of `Command`,`SubCommand`,`Options` and `Argument`
> `Options` is exactly `Option`, but `Rust` has used `Option` already, so I use it as replacement.
The relationships between them are:
1. One `CLI` has **ONLY ONE** `Command`.
2. One `Command` has **ZERO or MORE** `SubCommand`s.
3. `Command` and `SubCommand` have **ZERO or MORE** `Options`.
4. `SubCommand` and `Options` can accept **ZERO or MORE** `Argument`.For instance, see examples below:
```shell
command --option
command sub_command --option
```# attribute macros
## `#[option]`
Defining options using `#[option]`. Syntax shows below:
```rust
#[option([-s], --long-name [arg2], "description about this option")]
```Note, options of `command` are global options, it means `sub-command` can access them.
```rust
#[option(--global-option)]
#[command(test)]
fn test() {
// input
// paht/of/example test --global-option
assert!(!opts.contains_key("local-option"));
assert!(opts.contains_key("global-option"));
}#[option(--local-option)]
#[sub_command(test_sub)]
fn test_sub(opts: Opts) {
// input:
// path/of/example test test_sub --global-option --local-option
assert!(opts.contains_key("global-option"));
assert!(opts.contains_key("local-option"))
}
```Options without arguments are also called `flag` or `switch` (Ha, not `Nintendo Switch`).
### restriction of `#[option]`
All options should be defined above `command` or `sub_command`.
All options defined below `command` or `sub_command` will be ignored. See example below:
```rust
// valid
#[option(--display, "display something")]
#[option(-v, --version, "display version")]
#[sub_command(cmd_name, "this is a sub_command")]
fn sub_cmd_fn() {}// these below are all invalid
#[sub_command(cmd_name, "this is a sub_command")]
#[option(--display, "display something")]
#[option(-v, --version, "display version")]
fn sub_cmd_fn1() {}#[option(--display, "display something")]
#[sub_command(cmd_name, "this is a sub_command")]
#[option(-v, --version, "display version")]
fn sub_cmd_fn2() {}
```## `#[default_options]`
```rust
#[default_options]
#[sub_command(test)]
fn test() {}// is equal to
#[option(-v, --version, "print version information")]
#[option(-h, --help, "print help information")]
#[sub_command(test)]
fn test() {}
```### restriction of `#[default_options]`
Once you use `#[default_options]`,
other options of this `sub_command` or `command` can't use short names `-v` `-h` or long names `--version` `--help`.
See example below:```rust
#[default_options]
// Error, `-v` is reserved keyword which is used by `#[default_options]`
#[option(-v, --verbose, "display verbose information")]
// Error, `--help` is reserved keyword which is used by `#[default_options]`
#[option(--help, "need help")]
#[sub_command(test)]
fn test() {}
```## `#[command]` and `#[sub_command]`
They have the similar syntax which are shown below:
```rust
// sub_command
#[sub_command(sub_cmd_name [args], "this is a sub-command")]// command without app version
// in this case, environment variable `CARGO_PKG_VERSION` (i.e., std::env!("CARGO_PKG_VERSION")) will be used as app version
#[command(cmd_name [args], "this is a sub-command")]
// command with app version
#[command("0.0.1-pre-alpha", cmd_name [args], "this is a sub-command")]
```### restriction of `#[command]` or `#[sub_command]`
`#[command]` is only, but `#[sub_command]` s are not.# procedural macros
## `execute!()`
Run the cli app. If you don't call it, the cli app will not run.
Syntax is shown as example below:
```rust
// Note, these `*_fn_name*` are names of function instead of names of `sub_command` or `command`
execute!(command_fn_name, [sub_command_fn_name1, sub_command_fn_name2, ...])
// if no sub_command needed, provide a `[]`
execute!(command_fn_name, [])
```> Note: Because of restrictions of `Rust`, if you want to used procedural macro, you should add attribute `#![feature(proc_macro_hygiene)]`. See this [issue](https://github.com/rust-lang/rust/issues/54727)for more details.
### restriction of `execute!()`
Because of the internal mechanism, all functions which are used in `execute!()` should be at the same level of modules. It means the example below will raise error:
```rust
mod child_mod {
#[sub_command(test_sub, "test1")]
pub fn test1() {}
}#[command(test_cmd, "test2")]
fn test2() {}use child_mod::test1;
fn main() {
// Error, cannot find function `_commander_rust_prefix_test1_commander_rust_suffix_` in this scope
execute!(test2, [test1]);
}
```# Extract arguments
## types of arguments
There are four types of arguments. Listed below:
- required single argument:``
- required multiply arguments: `<..args>` or `<...args>`
- optional single argument:`[arg]`
- optional multiply arguments: `[..args]` or `[...args]`> In fact, `required multiply arguments` is equal to `optional multiply arguments`.
Note: there are several restrictions:
1. All `optional` arguments should be **after** all `required` arguments.
```rust
// valid
#[option(test [c] [d])]
// invalid
#[option(test [b] [d])]
```2. There can be only one `multiply` argument, and it can only be used as the **last** parameter.
```rust
// valid
#[otpion(test <..b>)]
// invalid
#[option(test <..a> )]
```## extract named arguments
See example below.
```rust
#[option(--user-name , "login with username")]
#[option(--passwd , "login with passwd")]
// all named arguments (e.g. here, ) should be used in function signature
#[command(login , "login")]
fn login_fn(url: String) {}
```> is named argument of option `--user-name`, `` is named argument of option `--passwd`. And `url` is a named argument of command `login`.
> Of course, you can customize the type of named arguments. Any type that implement the trait `FromArg` or `FromArgs` can be used as type of named arguments.
>
> What's different between `FromArg` and `FromArgs`?
>
> - `FromArg` is used for type of named arguments which are `single` arguments (e.g., `` or `[arg]`).
> - `FromArgs` is used for type of named arguments which are `multiply` arguments (e.g., `<..arg>` or `[..arg]`).
>
> There are several types that implement the trait `FromArg`:
>
> - `String` and `&str`
> - `Option`
> - `Result`
> - `i8` `i16` `i32` `i64` `i128` `u8` `u16` `u32` `u64` `u128`
> - `&Arg`
> - `Path` and `PathBuf`
>
> There are several types that implement the trait `FromArgs`:
>
> - `String`
> - `Vec (not T: FromArgs)`
> - `Option`
> - `Result`
> - `&Args`How to implement the two traits above? Let me show u an example.
Now, I define an `command` with an argument.
```rust
#[command(download , "download an package")]
fn connect(pkg: Pkg) {
match down_load_pkg(&pkg.name, &pkg.version) {
Ok(_) => println!("success"),
Err(e) => eprintln!("{}", e),
}
}
```I don't want to use `String`, but I use a type named `Pkg`.
I want to decode user's input which is formatted like `react=16.13.1`.
Now, let's define the struct `Pkg`.```rust
struct Version(u8, u8, u8);struct Pkg {
name: String,
version: Version,
}
```Now, the highlight is coming. Let's implement the trait `FromArg`, then we can use it.
```rust
impl<'a> FromArg<'a> for Pkg {
type Error = ();// see document for more details about `Arg` and `Args`
fn from_arg(arg: &'a Arg) -> Result {
let splits: Vec<&str> = arg.split('=').collect();if splits.len() != 2 {
Err(())
} else {
let name = splits[0];
let vers: Vec<&str> = splits[1].split('.').collect();if vers.len() != 3 {
Err(())
} else {
let mut vs = [0, 0, 0];for (idx, ver) in vers.into_iter().enumerate() {
if let Ok(v) = ver.parse::() {
vs[idx] = v;
} else {
return Err(());
}
}Ok(Pkg {
name: name.to_string(),
version: Version(vs[0], vs[1], vs[2]),
})
}
}
}
}
```Ha, it's done. Now you can use the cli app like:
```shell
$ /path/of/download react=16.13.1
```But there are some bugs here.
Look the `line 8` `line 14` `line 22` in the code above.
It returned the `Err`. It means sometimes it will crash.
Try to input like this:```shell
// Error, parse failed, can't parse input `react=16` as type `Pkg`
$ /path/of/download react
```How to catch errors and handle them yourself ?
It's easy, do u remember that there are several types which implement the trait `FromArg`?
`Option` and `Result` are two of them.
So, change the signature of function:
```rust
#[command(download , "download an package")]
// Or pkg: Result, both are okay
fn connect(pkg: Option) {
if let Some(pkg) = pkg {
match down_load_pkg(&pkg.name, &pkg.version) {
Ok(_) => println!("success"),
Err(e) => eprintln!("{}", e),
}
} else {
// if you want to do something, do it.
eprintln!("can't parse package.");
}
}
```Now, If you input:
```shell
// customize error, can't parse package.
$ /path/of/download react
```If you want to download multiply packages like:
```shell
$ /path/of/download react=16.13.1 react-redux=7.2.0
```Change signature of function `download` like:
```rust
#[command(download , "download an package")]
// Or pkgs: Vec>, both are okay
fn connect(pkgs: Vec>) {
if pkgs.is_empty() {
eprintln!("no packages offered.");
} else {
for pkg in pkgs.into_iter() {
if let Some(pkg) = pkg {
match down_load_pkg(&pkg.name, &pkg.version) {
Ok(_) => printlnr!("success"),
Err(e) => eprintln!("{}", e),
}
} else {
// if you want to do something, do it.
eprintln!("can't parse package.");
}
}
}
}
```# Extract options
## `Opts` and `GlobalOpts`
I offer u two types to get options. One is `Opts`,
the other one is `GlobalOpts`.
By names, you should be able to know the difference between them.```rust
#[option(-f, --force, "force to install even if this package has already installed")]
#[option(-g, --global, "install as a global package")]
#[sub_command(install , "install a package")]
fn install_fn(pkg: Result, opts: Opts, global_opts: GlobalOpts) {
if opts.contains_key("force") {
// do something here
}
if global_opts.contains_key("verbose") {
// do something here
}
}
```## extract arguments of options
Like arguments of `command`, arguments of `option`s have implemented the trait `FromArg` or `FromArgs`. See example below:
```rust
#[option(--fruit )]
#[commmand(eat)]
fn eat(opts: Opts) {
// try to get option
if let Some(Mixed::Single(fruit)) = opts.get("fruit") {
// "apple" is default value
let fruit = String::from_arg(fruit).unwrap_or("apple".to_string());
println!("I eat a(n) {}", fruit);
}
}
```> Code above used type `Mixed`. Why it? See example below:
>
> ```rust
> #[option(--test <..c>)]
> ```
>
> As you can see, `` and `` are both `sigle` arguments. But `<..c>` is multiply argument.
>
> If you want to get named arguments of `--test` by using api `get`, it will return value of type `Result`.
>
> In this case, for `single` arguments, `Mixed` is `Mixed::Signle` which only contains one input value. For `multiply` arguments, `Mixed` is `Mixed::Multiply` which contains more input value.
>
> You can see document or source code for more details.## advanced usage of options
Repeat: All types of named arguments should implement the trait `FromArg`(for `single` argument) or `FromArgs`(for `multiply` arguments).
If you offer non-named arguments, the types of them should implement the trait `FromApp`.
> See document for more details about struct `App`.
`Opts` and `GlobalOpts` have already implemented the trait `FromApp`. There are several types that implement the trait `FromApp`.
- `Application` and `&Application`(alias `App` and `&App`)
- `&Command`
- `Result`
- `Option`> See document for more details about struct `Command`. It contains all information about you cli app.
>
> If you want to show help or version information, there are three `api`s of `Command` you can use:
>
> 1. `cmd.println()` -- print help information of command
> 2. `cmd.println_sub("sub_name")` -- print help information of the specified sub-command
> 3. `cmd.println_version()` -- print version information
>
> ```rust
> #[command(test)]
> fn npms_fn(cmd: &Command) {
> cmd.println();
> }
> ```How to implement the trait `FromApp`. See example below:
```rust
enum DangerousThing {
Cephalosporin,
Wine,
None,
}// by implementing the trait `FromApp`, you can do many multiple custom options types
// e.g. here, mutually exclusive options
struct MutexThing(DangerousThing);impl<'a> FromApp<'a> for MutexThing {
type Error = String;fn from_app(app: &'a Application) -> Result {
// You can use `::from_app(app)` to convert app to `T`
if let Ok(opts) = GlobalOpts::from_app(app) {
if opts.contains_key("cephalosporin") && opts.contains_key("drink-wine") {
Err(String::from("DANGER!!! DO NOT DO IT! DO NOT take cephalosporin while drinking wine!"))
} else if opts.contains_key("cephalosporin") {
Ok(MutexThing(DangerousThing::Cephalosporin))
} else {
Ok(MutexThing(DangerousThing::Wine))
}
} else {
Ok(MutexThing(DangerousThing::None))
}
}
}// WARN: DO NOT take cephalosporin while drinking wine! It's fatal behavior!!!!!!!!
#[option(--cephalosporin, "take cephalosporin")]
#[option(--drink-wine, "drink wine")]
#[default_options]
#[command("0.0.1-fruits-eater", eat , "eat food")]
// Here, do u see it?
// mutex_thing is not named argument, so it should implement the trait `FromApp`.
fn eat_fn(food: String, mutex_thing: Result) {
match food.as_str() {
"apple" | "banana" | "pear" | "watermelon" | "orange" => println!("I eat a(n) {}, it's delicious!", food),
_ => println!("I dislike {}", food),
}match mutex_thing {
Ok(MutexThing(DangerousThing::Cephalosporin)) => println!("DO NOT drink wine recently!"),
Ok(MutexThing(DangerousThing::Wine)) => println!("DO NOT take cephalosporin recently!"),
Ok(MutexThing(DangerousThing::None)) => println!("I want to eat more!"),
Err(note) => println!("{}", note),
}
}```
# Conclusion
1. There are three traits you may will use:
| Name | description |
| :--------: | ------------------------------------------------------------ |
| `FromArg` | single named arguments should implement it, e.g., `` or `[arg]` |
| `FromArgs` | multiply named arguments should implement it, e.g., `<..args>` or `[..args]` |
| `FromApp` | non-named arguments of function signature(if it exists) should implement it. |2. You can use `Opts` and `GlobalOpts` to get options.
2. You can use `&Command` to print help and version information by yourself.
3. Run cli app by calling macro `execute!()`.# Examples
You can check examples in folder `examples` of this crate for full usage of `commander_rust`.
# Contribution
Because of something that happened to me, I stopped maintaining the previous version of this project for a long time.
After all that, I have time to maintain the project.
I am sorry for those people who opened issues, because of refactoring of the project, I can't and needn't to respond them any more.
Now, starting from scratch, any useful contribution is welcome.If you find bug and fix it, please create an `Merge Request`.
If you have a good idea and implement it, please create an `Merge Request`.
If you have any questions, please open an `issue`.
# TODO list
- [ ] i18n support?
- [ ] sub-sub*n-sub-commands support?
- [ ] cross modules support?