{"id":13503085,"url":"https://github.com/MSDimos/commander-rust","last_synced_at":"2025-03-29T13:30:46.736Z","repository":{"id":57609067,"uuid":"182378127","full_name":"MSDimos/commander-rust","owner":"MSDimos","description":"a more intuitive development method of CLI in Rust","archived":false,"fork":false,"pushed_at":"2020-06-18T09:48:01.000Z","size":2029,"stargazers_count":43,"open_issues_count":4,"forks_count":5,"subscribers_count":4,"default_branch":"feature/pre-alpha","last_synced_at":"2025-03-04T05:05:25.108Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MSDimos.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-04-20T08:00:17.000Z","updated_at":"2025-02-13T19:46:19.000Z","dependencies_parsed_at":"2022-08-27T22:41:58.208Z","dependency_job_id":null,"html_url":"https://github.com/MSDimos/commander-rust","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MSDimos%2Fcommander-rust","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MSDimos%2Fcommander-rust/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MSDimos%2Fcommander-rust/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MSDimos%2Fcommander-rust/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MSDimos","download_url":"https://codeload.github.com/MSDimos/commander-rust/tar.gz/refs/heads/feature/pre-alpha","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246190216,"owners_count":20737997,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T22:02:36.372Z","updated_at":"2025-03-29T13:30:46.492Z","avatar_url":"https://github.com/MSDimos.png","language":"Rust","readme":"# Glance\nSee `example/glance.rs` for more details.\n```rust\n#[default_options]\n#[option(--https, \"use https instead of http\")]\n#[sub_command(connect \u003caddress\u003e, \"connect to the address\")]\nfn connect(address: Option\u003cAddress\u003e, opts: Opts, global_opts: GlobalOpts) {\n    /* .. */\n}\n\n#[default_options]\n#[sub_command(disconnect \u003caddress\u003e, \"disconnect the connection\")]\nfn disconnect(address: Option\u003cAddress\u003e) {\n    /* .. */\n}\n\n#[default_options]\n#[option(--proxy \u003cproxy_address\u003e, \"use proxy to connect\")]\n#[command(net, \"network tool\")]\nfn net() { /* .. */}\n\nfn main() {\n    execute!(net, [connect, disconnect]);\n}\n```\n\n# Note\nThe current `master` branch will no longer be supported. \nAnd the branch `pre-alpha` will be the `master` branch once it's stable.\n\n# Usage\nThe current `commander_rust` in `crate.io` is no longer supported.\nBefore it becomes stable, `github` will be used as distribution source. Add it to your dependencies.\n```toml\n[dependencies.commander_rust]\ngit = \"https://github.com/MSDimos/commander-rust/\"\nbranch = \"pre-alpha\"\n\n# or\ncommander_rust = { git = \"https://github.com/MSDimos/commander-rust/\", branch = \"pre-alpha\" }\n```\n\n# Viewpoint\n\nAs I think, developers should devote more time to the realization of functions instead of leaning how to use `command line interface (CLI)`.\n So a crate of `CLI` should be easy to use.\nSpecifically, it should have the following advantages:\n\n- Firstly, it should have less `API`s.\n- Secondly, it should be intuitive enough to use. What u see is what u get.\n- Thirdly, it should make full use of the advantages of programming language.\n\nInspired by [Rocket](https://rocket.rs/) and [commander.js](https://github.com/tj/commander.js/), the crate is born.\n\n# Design concept\n\nA `CLI` program consists of `Command`，`SubCommand`，`Options` and `Argument`\n\n\u003e `Options` is exactly `Option`, but `Rust` has used `Option` already, so I use it as replacement.\n\nThe relationships between them are:\n\n1. One `CLI` has **ONLY ONE** `Command`.\n2. One `Command` has **ZERO or MORE** `SubCommand`s.\n3. `Command` and `SubCommand` have **ZERO or MORE** `Options`.\n4. `SubCommand` and `Options` can accept **ZERO or MORE** `Argument`.\n\nFor instance, see examples below:\n\n```shell\ncommand \u003crequire_argument\u003e \u003coptional_argument\u003e --option\ncommand sub_command \u003crequire_argument\u003e \u003coptional_argument\u003e --option\n```\n\n# attribute macros\n## `#[option]`\nDefining options using `#[option]`. Syntax shows below:\n```rust\n#[option([-s], --long-name \u003carg1\u003e [arg2], \"description about this option\")]\n```\n\nNote, options of `command` are global options, it means `sub-command` can access them.\n\n```rust\n#[option(--global-option)]\n#[command(test)]\nfn test() {\n    // input\n    // paht/of/example test --global-option\n    assert!(!opts.contains_key(\"local-option\"));\n    assert!(opts.contains_key(\"global-option\"));\n}\n\n#[option(--local-option)]\n#[sub_command(test_sub)]\nfn test_sub(opts: Opts) {\n    // input:\n    // path/of/example test test_sub --global-option --local-option\n    assert!(opts.contains_key(\"global-option\"));\n    assert!(opts.contains_key(\"local-option\"))\n}\n```\n\nOptions without arguments are also called `flag` or `switch` (Ha, not `Nintendo Switch`).\n\n### restriction of `#[option]`\nAll options should be defined above `command` or `sub_command`.  \nAll options defined below `command` or `sub_command` will be ignored. See example below:\n```rust\n// valid\n#[option(--display, \"display something\")]\n#[option(-v, --version, \"display version\")]\n#[sub_command(cmd_name, \"this is a sub_command\")]\nfn sub_cmd_fn() {} \n\n\n// these below are all invalid\n\n#[sub_command(cmd_name, \"this is a sub_command\")]\n#[option(--display, \"display something\")]\n#[option(-v, --version, \"display version\")]\nfn sub_cmd_fn1() {} \n\n#[option(--display, \"display something\")]\n#[sub_command(cmd_name, \"this is a sub_command\")]\n#[option(-v, --version, \"display version\")]\nfn sub_cmd_fn2() {} \n```\n\n## `#[default_options]`\n\n```rust\n#[default_options]\n#[sub_command(test)]\nfn test() {}\n\n// is equal to\n#[option(-v, --version, \"print version information\")]\n#[option(-h, --help, \"print help information\")]\n#[sub_command(test)]\nfn test() {}\n```\n\n### restriction of `#[default_options]`\n\nOnce you use `#[default_options]`,\nother options of this `sub_command` or `command` can't use short names `-v` `-h` or long names `--version` `--help`.\nSee example below:\n\n```rust\n#[default_options]\n// Error, `-v` is reserved keyword which is used by `#[default_options]`\n#[option(-v, --verbose, \"display verbose information\")]\n// Error, `--help` is reserved keyword which is used by `#[default_options]`\n#[option(--help, \"need help\")]\n#[sub_command(test)]\nfn test() {}\n```\n\n## `#[command]` and `#[sub_command]`\n\nThey have the similar syntax which are shown below:\n```rust\n// sub_command\n#[sub_command(sub_cmd_name \u003carg1\u003e [args], \"this is a sub-command\")]\n\n// command without app version\n// in this case, environment variable `CARGO_PKG_VERSION` (i.e., std::env!(\"CARGO_PKG_VERSION\")) will be used as app version\n#[command(cmd_name \u003carg1\u003e [args], \"this is a sub-command\")]\n// command with app version\n#[command(\"0.0.1-pre-alpha\", cmd_name \u003carg1\u003e [args], \"this is a sub-command\")]\n```\n\n### restriction of `#[command]` or `#[sub_command]`\n`#[command]` is only, but `#[sub_command]` s are not.\n\n# procedural macros\n\n## `execute!()`\n\nRun the cli app. If you don't call it, the cli app will not run.\n\nSyntax is shown as example below:\n\n```rust\n// Note, these `*_fn_name*` are names of function instead of names of `sub_command` or `command`\nexecute!(command_fn_name, [sub_command_fn_name1, sub_command_fn_name2, ...])\n// if no sub_command needed, provide a `[]`\nexecute!(command_fn_name, [])\n```\n\n\u003e 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.\n\n### restriction of `execute!()`\n\nBecause 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:\n\n```rust\nmod child_mod {\n    #[sub_command(test_sub, \"test1\")]\n\tpub fn test1() {}\n}\n\n#[command(test_cmd, \"test2\")]\nfn test2() {}\n\nuse child_mod::test1;\n\nfn main() {\n     // Error, cannot find function `_commander_rust_prefix_test1_commander_rust_suffix_` in this scope\n    execute!(test2, [test1]);\n}\n```\n\n# Extract arguments\n\n## types of arguments\n\nThere are four types of arguments. Listed below:\n\n- required single argument:`\u003carg\u003e`\n- required multiply arguments: `\u003c..args\u003e` or `\u003c...args\u003e`\n- optional single argument:`[arg]`\n- optional multiply arguments: `[..args]` or `[...args]`\n\n\u003e In fact, `required multiply arguments` is equal to `optional multiply arguments`. \n\nNote: there are several restrictions:\n\n1. All `optional` arguments should be **after** all `required` arguments.\n\n```rust\n// valid\n#[option(test \u003ca\u003e \u003cb\u003e [c] [d])]\n// invalid\n#[option(test \u003ca\u003e [b] \u003cc\u003e [d])]\n```\n\n2. There can be only one `multiply` argument, and it can only be used as the **last** parameter.\n\n```rust\n// valid\n#[otpion(test \u003ca\u003e \u003c..b\u003e)]\n// invalid\n#[option(test \u003c..a\u003e \u003cb\u003e)]\n```\n\n## extract named arguments\n\nSee example below.\n\n```rust\n#[option(--user-name \u003cname\u003e, \"login with username\")]\n#[option(--passwd \u003cpasswd\u003e, \"login with passwd\")]\n// all named arguments (e.g. here, \u003curl\u003e) should be used in function signature\n#[command(login \u003curl\u003e, \"login\")]\nfn login_fn(url: String) {}\n```\n\n\u003e \u003cname\u003e is named argument of option `--user-name`, `\u003cpasswd\u003e` is named argument of option `--passwd`. And `url` is a named argument of command `login`.\n\n\u003e 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.\n\u003e\n\u003e What's different between `FromArg` and `FromArgs`?\n\u003e\n\u003e - `FromArg` is used for type of named arguments which are `single` arguments (e.g., `\u003carg\u003e` or `[arg]`).\n\u003e - `FromArgs` is used for type of named arguments which are `multiply` arguments (e.g., `\u003c..arg\u003e` or `[..arg]`).\n\u003e\n\u003e There are several types that implement the trait `FromArg`:\n\u003e\n\u003e - `String` and `\u0026str`\n\u003e - `Option\u003cT: FromArg\u003e`\n\u003e - `Result\u003cT: FromArg, T::Error\u003e`\n\u003e - `i8` `i16` `i32` `i64` `i128` `u8` `u16` `u32` `u64` `u128`\n\u003e - `\u0026Arg`\n\u003e - `Path` and `PathBuf`\n\u003e\n\u003e There are several types that implement the trait `FromArgs`:\n\u003e\n\u003e - `String`\n\u003e - `Vec\u003cT: FromArg\u003e (not T: FromArgs)`\n\u003e - `Option\u003cT: FromArgs\u003e`\n\u003e - `Result\u003cT: FromArgs, T::Error\u003e`\n\u003e - `\u0026Args`\n\nHow to implement the two traits above? Let me show u an example.\n\nNow, I define an `command` with an argument.\n\n```rust\n#[command(download \u003cpkg\u003e, \"download an package\")]\nfn connect(pkg: Pkg) {\n    match down_load_pkg(\u0026pkg.name, \u0026pkg.version) {\n        Ok(_) =\u003e println!(\"success\"),\n        Err(e) =\u003e eprintln!(\"{}\", e),\n    }\n}\n```\n\nI don't want to use `String`, but I use a type named `Pkg`.\nI want to decode user's input which is formatted like `react=16.13.1`.\nNow, let's define the struct `Pkg`.\n\n```rust\nstruct Version(u8, u8, u8);\n\nstruct Pkg {\n    name: String,\n    version: Version,\n}\n```\n\nNow, the highlight is coming. Let's implement the trait `FromArg`, then we can use it.\n\n```rust\nimpl\u003c'a\u003e FromArg\u003c'a\u003e for Pkg {\n    type Error = ();\n\n    // see document for more details about `Arg` and `Args`\n    fn from_arg(arg: \u0026'a Arg) -\u003e Result\u003cSelf, Self::Error\u003e {\n        let splits: Vec\u003c\u0026str\u003e = arg.split('=').collect();\n\n        if splits.len() != 2 {\n            Err(())\n        } else {\n            let name = splits[0];\n            let vers: Vec\u003c\u0026str\u003e = splits[1].split('.').collect();\n\n            if vers.len() != 3 {\n                Err(())\n            } else {\n                let mut vs = [0, 0, 0];\n\n                for (idx, ver) in vers.into_iter().enumerate() {\n                    if let Ok(v) = ver.parse::\u003cu8\u003e() {\n                        vs[idx] = v;\n                    } else {\n                        return Err(());\n                    }\n                }\n\n                Ok(Pkg {\n                    name: name.to_string(),\n                    version: Version(vs[0], vs[1], vs[2]),\n                })\n            }\n        }\n    }\n}\n```\n\nHa, it's done. Now you can use the cli app like:\n\n```shell\n$ /path/of/download react=16.13.1\n```\n\nBut there are some bugs here.\nLook the `line 8` `line 14` `line 22` in the code above.\nIt returned the `Err`. It means sometimes it will crash.\nTry to input like this:\n\n```shell\n// Error, parse failed, can't parse input `react=16` as type `Pkg`\n$ /path/of/download react\n```\n\nHow to catch errors and handle them yourself ? \nIt's easy, do u remember that there are several types which implement the trait `FromArg`? \n`Option\u003cT: FromArg\u003e` and `Result\u003cT: FromArg, T::Error\u003e` are two of them. \nSo, change the signature of function:\n```rust\n#[command(download \u003cpkg\u003e, \"download an package\")]\n// Or pkg: Result\u003cPkg, ()\u003e, both are okay\nfn connect(pkg: Option\u003cPkg\u003e) {\n\tif let Some(pkg) = pkg {\n        match down_load_pkg(\u0026pkg.name, \u0026pkg.version) {\n            Ok(_) =\u003e println!(\"success\"),\n            Err(e) =\u003e eprintln!(\"{}\", e),\n        }\n    } else {\n        // if you want to do something, do it.\n        eprintln!(\"can't parse package.\");\n    }\n}\n```\n\nNow, If you input:\n\n```shell\n// customize error, can't parse package.\n$ /path/of/download react\n```\n\nIf you want to download multiply packages like:\n\n```shell\n$ /path/of/download react=16.13.1 react-redux=7.2.0\n```\n\nChange signature of function `download` like:\n\n```rust\n#[command(download \u003cpkg\u003e, \"download an package\")]\n// Or pkgs: Vec\u003cResult\u003cPkg, ()\u003e\u003e, both are okay\nfn connect(pkgs: Vec\u003cOption\u003cPkg\u003e\u003e) {\n\tif pkgs.is_empty() {\n        eprintln!(\"no packages offered.\");\n    } else {\n        for pkg in pkgs.into_iter() {\n            if let Some(pkg) = pkg {\n                match down_load_pkg(\u0026pkg.name, \u0026pkg.version) {\n                    Ok(_) =\u003e printlnr!(\"success\"),\n                    Err(e) =\u003e eprintln!(\"{}\", e),\n                }\n            } else {\n                // if you want to do something, do it.\n                eprintln!(\"can't parse package.\");\n            }\n        }\n    }\n}\n```\n\n# Extract options\n\n## `Opts` and `GlobalOpts`\n\nI offer u two types to get options. One is `Opts`,\nthe other one is `GlobalOpts`. \nBy names, you should be able to know the difference between them.\n\n```rust\n#[option(-f, --force, \"force to install even if this package has already installed\")]\n#[option(-g, --global, \"install as a global package\")]\n#[sub_command(install \u003cpkg\u003e, \"install a package\")]\nfn install_fn(pkg: Result\u003cPkg, ()\u003e, opts: Opts, global_opts: GlobalOpts) {\n   \tif opts.contains_key(\"force\") {\n        // do something here\n    }\n    \n    if global_opts.contains_key(\"verbose\") {\n        // do something here\n    }\n}\n```\n\n## extract arguments of options\n\nLike arguments of `command`, arguments of `option`s have implemented the trait `FromArg` or `FromArgs`. See example below:\n\n```rust\n#[option(--fruit \u003cfruit\u003e)]\n#[commmand(eat)]\nfn eat(opts: Opts) {\n    // try to get option\n    if let Some(Mixed::Single(fruit)) = opts.get(\"fruit\") {\n        // \"apple\" is default value\n        let fruit = String::from_arg(fruit).unwrap_or(\"apple\".to_string());\n        \n        println!(\"I eat a(n) {}\", fruit);\n    }\n}\n```\n\n\u003e Code above used type `Mixed`. Why it? See example below:\n\u003e\n\u003e ```rust\n\u003e #[option(--test \u003ca\u003e \u003cb\u003e \u003c..c\u003e)]\n\u003e ```\n\u003e\n\u003e As you can see, `\u003ca\u003e` and `\u003cb\u003e` are both `sigle` arguments. But `\u003c..c\u003e` is multiply argument. \n\u003e\n\u003e If you want to get named arguments of `--test`  by using api `get`, it will return value of type `Result\u003cMixed, ()\u003e`. \n\u003e\n\u003e 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. \n\u003e\n\u003e You can see document or source code for more details.\n\n## advanced usage of options\n\nRepeat: All types of named arguments should implement the trait `FromArg`(for `single` argument) or `FromArgs`(for `multiply` arguments).\n\nIf you offer non-named arguments, the types of them should implement the trait `FromApp`.\n\n\u003e See document for more details about struct `App`.\n\n`Opts` and `GlobalOpts` have already implemented the trait `FromApp`. There are several types that implement the trait `FromApp`.\n\n- `Application` and `\u0026Application`(alias `App` and `\u0026App`)\n- `\u0026Command`\n- `Result\u003cT: FromApp, T::Error\u003e`\n- `Option\u003cT: FromApp\u003e`\n\n\u003e See document for more details about struct `Command`. It contains all information about you cli app.\n\u003e\n\u003e If you want to show help or version information, there are three `api`s of `Command` you can use:\n\u003e\n\u003e 1. `cmd.println()` -- print help information of command\n\u003e 2. `cmd.println_sub(\"sub_name\")` -- print help information of the specified sub-command\n\u003e 3. `cmd.println_version()` -- print version information\n\u003e\n\u003e ```rust\n\u003e #[command(test)]\n\u003e fn npms_fn(cmd: \u0026Command) {\n\u003e     cmd.println();\n\u003e }\n\u003e ```\n\nHow to implement the trait `FromApp`. See example below:\n\n```rust\nenum DangerousThing {\n    Cephalosporin,\n    Wine,\n    None,\n}\n\n// by implementing the trait `FromApp`, you can do many multiple custom options types\n// e.g. here, mutually exclusive options\nstruct MutexThing(DangerousThing);\n\nimpl\u003c'a\u003e FromApp\u003c'a\u003e for MutexThing {\n    type Error = String;\n\n    fn from_app(app: \u0026'a Application) -\u003e Result\u003cSelf, Self::Error\u003e {\n        // You can use `\u003cT as FromApp\u003e::from_app(app)` to convert app to `T`\n        if let Ok(opts) = GlobalOpts::from_app(app) {\n            if opts.contains_key(\"cephalosporin\") \u0026\u0026 opts.contains_key(\"drink-wine\") {\n                Err(String::from(\"DANGER!!! DO NOT DO IT! DO NOT take cephalosporin while drinking wine!\"))\n            } else if opts.contains_key(\"cephalosporin\") {\n                Ok(MutexThing(DangerousThing::Cephalosporin))\n            } else {\n                Ok(MutexThing(DangerousThing::Wine))\n            }\n        } else {\n            Ok(MutexThing(DangerousThing::None))\n        }\n    }\n}\n\n// WARN: DO NOT take cephalosporin while drinking wine! It's fatal behavior!!!!!!!!\n#[option(--cephalosporin, \"take cephalosporin\")]\n#[option(--drink-wine, \"drink wine\")]\n#[default_options]\n#[command(\"0.0.1-fruits-eater\", eat \u003cfood\u003e, \"eat food\")]\n// Here, do u see it?\n// mutex_thing is not named argument, so it should implement the trait `FromApp`.\nfn eat_fn(food: String, mutex_thing: Result\u003cMutexThing, String\u003e) {\n    match food.as_str() {\n        \"apple\" | \"banana\" | \"pear\" | \"watermelon\" | \"orange\" =\u003e println!(\"I eat a(n) {}, it's delicious!\", food),\n        _ =\u003e println!(\"I dislike {}\", food),\n    }\n\n    match mutex_thing {\n        Ok(MutexThing(DangerousThing::Cephalosporin)) =\u003e println!(\"DO NOT drink wine recently!\"),\n        Ok(MutexThing(DangerousThing::Wine)) =\u003e println!(\"DO NOT take cephalosporin recently!\"),\n        Ok(MutexThing(DangerousThing::None)) =\u003e println!(\"I want to eat more!\"),\n        Err(note) =\u003e println!(\"{}\", note),\n    }\n}\n\n```\n\n\n\n# Conclusion\n\n1. There are three traits you may will use:\n\n|    Name    | description                                                  |\n| :--------: | ------------------------------------------------------------ |\n| `FromArg`  | single named arguments should implement it, e.g., `\u003carg\u003e` or `[arg]` |\n| `FromArgs` | multiply named arguments should implement it, e.g., `\u003c..args\u003e` or `[..args]` |\n| `FromApp`  | non-named arguments of function signature(if it exists) should implement it. |\n\n2. You can use `Opts` and `GlobalOpts` to get options.\n\n2. You can use `\u0026Command` to print help and version information by yourself.\n3. Run cli app by calling macro `execute!()`.\n\n# Examples\n\nYou can check examples in folder `examples` of this crate for full usage of `commander_rust`.\n\n# Contribution\n\nBecause of something that happened to me, I stopped maintaining the previous version of this project for a long time. \n\nAfter all that, I have time to maintain the project. \nI 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.\nNow, starting from scratch, any useful contribution is welcome.\n\nIf you find bug and fix it, please create an `Merge Request`.\n\nIf you have a good idea and implement it, please create an `Merge Request`.\n\nIf you have any questions, please open an `issue`.\n\n# TODO list\n\n- [ ] i18n support?\n\n- [ ] sub-sub*n-sub-commands support?\n\n- [ ] cross modules support?\n\n","funding_links":[],"categories":["Rust"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMSDimos%2Fcommander-rust","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FMSDimos%2Fcommander-rust","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMSDimos%2Fcommander-rust/lists"}