Core app foundation/opt parsing

This commit is contained in:
Joe Bellus 2021-06-19 03:20:29 -04:00
commit c9ae7be6d0
13 changed files with 1350 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target/
debug/

667
Cargo.lock generated Normal file
View File

@ -0,0 +1,667 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "arkham"
version = "0.1.0"
dependencies = [
"console",
"criterion",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bstr"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
[[package]]
name = "cast"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57cdfa5d50aad6cb4d44dcab6101a7f79925bd59d82ca42f38a9856a28865374"
dependencies = [
"rustc_version",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"bitflags",
"textwrap",
"unicode-width",
]
[[package]]
name = "console"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cc80946b3480f421c2f17ed1cb841753a371c7c5104f51d507e13f532c856aa"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"regex",
"terminal_size",
"unicode-width",
"winapi",
]
[[package]]
name = "criterion"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab327ed7354547cc2ef43cbe20ef68b988e70b4b593cbd66a2a61733123a3d23"
dependencies = [
"atty",
"cast",
"clap",
"criterion-plot",
"csv",
"itertools 0.10.1",
"lazy_static",
"num-traits",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_cbor",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d"
dependencies = [
"cast",
"itertools 0.9.0",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd"
dependencies = [
"cfg-if",
"crossbeam-utils",
"lazy_static",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
dependencies = [
"cfg-if",
"lazy_static",
]
[[package]]
name = "csv"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
dependencies = [
"bstr",
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
dependencies = [
"memchr",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "half"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
[[package]]
name = "hermit-abi"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
dependencies = [
"libc",
]
[[package]]
name = "itertools"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "js-sys"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03b07a082330a35e43f63177cc01689da34fbffa0105e1246cf0311472cac73a"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "memchr"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "memoffset"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9"
dependencies = [
"autocfg",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "oorandom"
version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "pest"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
dependencies = [
"ucd-trie",
]
[[package]]
name = "plotters"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07fffcddc1cb3a1de753caa4e4df03b79922ba43cf882acc1bdd7e8df9f4590"
[[package]]
name = "plotters-svg"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b38a02e23bd9604b842a812063aec4ef702b57989c37b655254bb61c471ad211"
dependencies = [
"plotters-backend",
]
[[package]]
name = "proc-macro2"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
dependencies = [
"autocfg",
"crossbeam-deque",
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"lazy_static",
"num_cpus",
]
[[package]]
name = "regex"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "regex-syntax"
version = "0.6.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
[[package]]
name = "rustc_version"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "semver"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [
"pest",
]
[[package]]
name = "serde"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
[[package]]
name = "serde_cbor"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622"
dependencies = [
"half",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "terminal_size"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
[[package]]
name = "web-sys"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

18
Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "arkham"
version = "0.1.0"
authors = ["Joe Bellus <joe@5sigma.io>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# indicatif = "0.15.0"
console = "0.14.0"
[dev-dependencies]
criterion = "0.3"
[[bench]]
name = "arg_parsing"
harness = false

42
benches/arg_parsing.rs Normal file
View File

@ -0,0 +1,42 @@
use arkham::{App, Command, Opt, OptKind};
use criterion::{criterion_group, criterion_main, Criterion};
fn parse_args(c: &mut Criterion) {
c.bench_function("parse_args", |b| {
let args = vec![
"--user".into(),
"joe".into(),
"--config".into(),
"c.json".into(),
"thing".into(),
];
let app = App::new()
.opt(Opt {
name: "user".into(),
short: "u".into(),
long: "user".into(),
kind: OptKind::String,
})
.command(
Command::new("thing")
.opt(Opt {
name: "config".into(),
short: "c".into(),
long: "config".into(),
kind: OptKind::String,
})
.handler(|_, ctx, _| {
assert_eq!(ctx.get_string("user"), Some("joe".into()));
assert_eq!(ctx.get_string("config"), Some("c.json".into()));
}),
);
b.iter(|| {
let _ = app.run_with(args.clone());
})
});
}
criterion_group!(benches, parse_args,);
criterion_main!(benches);

29
examples/fib.rs Normal file
View File

@ -0,0 +1,29 @@
use arkham::{App, Context, Opt};
fn main() {
let _ = App::new()
.name("Fibonacci App")
.version("1.0")
.opt(Opt::scalar("count").short("n").long("num"))
.handler(fibonacci_handler)
.run()
.unwrap();
}
fn fibonacci_handler(_app: &App, ctx: &Context, _args: &[String]) {
let v = fibonacci(
ctx.get_string("count")
.unwrap_or("1".to_string())
.parse()
.unwrap_or(1),
);
println!("Value is: {}", v);
}
fn fibonacci(n: u32) -> u32 {
match n {
0 => 1,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}

8
examples/tasks.rs Normal file
View File

@ -0,0 +1,8 @@
// use arkham::TaskGroup;
fn main() {
// let tasks = TaskGroup::new();
// let task = tasks.start_task("task 1");
// task.tick();
// tasks.join();
}

311
src/app.rs Normal file
View File

@ -0,0 +1,311 @@
use super::command::{help, Command, Handler};
use super::context::Context;
use super::opt::{ActiveOpt, Opt, OptError, OptKind};
use std::env;
type Result<T> = std::result::Result<T, OptError>;
pub struct App {
name: Option<&'static str>,
version: Option<&'static str>,
pub root: Command,
}
impl Default for App {
fn default() -> Self {
Self {
name: None,
version: None,
root: Command::new("root"),
}
}
}
impl App {
pub fn application_header(&self) -> String {
format!(
"{} - {}",
self.name.as_ref().unwrap_or(&env!("CARGO_PKG_NAME")),
self.version.as_ref().unwrap_or(&env!("CARGO_PKG_VERSION"))
)
}
}
impl App {
/// Contructs a new App instance which can have opts defined and subcommands attached.
pub fn new() -> Self {
App::default().command(Command::new("help").handler(help))
}
/// Sets the name of the application. If not set the cargo package name will be used.
///
/// Exmaple:
///
/// ```rust
/// use arkham::App;
/// let app = App::new().name("new app");
/// ```
pub fn name(mut self, name: &'static str) -> Self {
self.name = Some(name);
self
}
/// Sets the version for the application. If not set the cargo package version is used.
///
/// Example:
///
/// ```rust
/// use arkham::App;
/// App::new().version("1.0.0");
/// ```
pub fn version(mut self, version: &'static str) -> Self {
self.version = Some(version);
self
}
/// Adds a root level command to the application. This command can then be executed with:
///
/// myapp command_name
///
/// Help flags will also be generated for the command which will display command
/// information for:
///
/// myapp --help command_name or myapp help command_name
///
/// Example:
/// ```rust
/// use arkham::{App, Command, Context};
/// App::new().command(Command::new("subcommand").handler(my_handler));
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {}
/// ```
pub fn command(mut self, cmd: Command) -> Self {
self.root.commands.push(cmd);
self
}
/// Adds a root level opt/flag that is available to all commands. Opts are given a name which
/// is used to reference them, as well as a short and long identifier.
///
/// Example:
/// ```rust
/// use arkham::{App, Opt};
/// App::new().opt(Opt::flag("verbose").short("v").long("verbose"));
/// ```
pub fn opt(mut self, opt: Opt) -> Self {
self.root.opts.push(opt);
self
}
/// Sets a handler function for the bare root command. If this is not set an error will be
/// generated and a help message will be displayed indicating the available subcommands.
/// The handler function takes an instance of the app, the context which contains the opts and
/// flags, and any additionally passeed arguments.
///
/// Example:
/// ```rust
/// use arkham::{App, Command, Context};
/// App::new().handler(my_handler);
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {}
/// ```
pub fn handler(mut self, f: Handler) -> Self {
self.root.handler = Some(f);
self
}
/// Execute the app and any specified handlers based on the passed arguemnts. This function is
/// mostly used for testing or any situation where you need to pass arbitrary arguments instead
/// of using the ones passed to the application.
/// Example:
/// ```rust
/// use arkham::{App, Command, Context, Opt};
/// App::new()
/// .opt(Opt::flag("name").short("n").long("name"))
/// .handler(my_handler)
/// .run_with(vec!["-n".to_string(), "alice".to_string()]);
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {
/// println!("Hello, {}", ctx.get_string("name").unwrap());
/// }
/// ```
pub fn run_with(&self, args: Vec<String>) -> Result<()> {
run_command(self, &self.root, &args, &mut vec![])
}
/// Execute the app and any specified handlers based on the arguments passsed to the
/// application.
///
/// Example:
/// running with myapp --name alice
/// ```rust
/// use arkham::{App, Command, Context, Opt};
/// App::new()
/// .opt(Opt::flag("name").short("n").long("name"))
/// .handler(my_handler)
/// .run();
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {
/// println!("Hello, {}", ctx.get_string("name").unwrap_or_else(|| "unnamed".into()));
/// }
/// ```
pub fn run(&mut self) -> Result<()> {
self.run_with(env::args().collect())
}
}
/// This is the core logic for parsing arguments and executing handlers. It is ran but the App::run
/// and App::run_with functions.
fn run_command(app: &App, cmd: &Command, args: &[String], opts: &mut Vec<ActiveOpt>) -> Result<()> {
// Get an iterator for the incomming arguments
let mut args = args.iter();
// We will keep track of any arguments that arent consumed by the current command.
// These will be either used to collect arguments from subcommands or passed as additional args
// to the command.
let mut ignored: Vec<String> = vec![];
// Loop through all passed in args
while let Some(arg) = args.next() {
// Check for long args
if arg.starts_with("--") {
if let Some(opt) = cmd.opts.iter().find(|o| &o.long == &arg[2..]) {
match opt.kind {
OptKind::Flag => {
opts.push(ActiveOpt::new(opt.clone(), vec!["".into()]));
}
OptKind::String => {
if let Some(value) = args.next() {
opts.push(ActiveOpt::new(opt.clone(), vec![value.clone()]));
} else {
return Err(OptError::InvalidOpt(opt.name.clone()));
}
}
}
} else {
ignored.push(arg.clone());
}
continue;
}
// Check for short args
if arg.starts_with("-") {
if let Some(opt) = cmd.opts.iter().find(|o| &o.short == &arg[1..]) {
opts.push(ActiveOpt::new(opt.clone(), vec!["".into()]));
} else {
ignored.push(arg.clone());
}
continue;
}
ignored.push(arg.clone());
}
// Find an recurse into sub commands if the remaining argumetns match any subcommand name
if let Some(cmd) = cmd
.commands
.iter()
.find(|cmd| ignored.iter().any(|a| *a == cmd.name))
{
ignored.retain(|a| *a != cmd.name);
return run_command(app, cmd, &ignored, opts);
}
// If any ignored parameters start with "-" we will throw an unknwon flag error.
if let Some(arg) = ignored.iter().find(|a| a.starts_with("-")) {
return Err(OptError::InvalidOpt(arg.clone()));
}
// Execute the command handler
if let Some(handler) = cmd.handler {
handler(app, &Context::new(opts.clone()), &ignored);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::App;
#[test]
fn test_long_string() {
let args: Vec<String> = vec!["--user".into(), "joe".into()];
let app = App::new()
.opt(Opt::scalar("user").short("u").long("user"))
.handler(|_, ctx, _| {
assert_eq!(ctx.get_string("user"), Some("joe".into()));
});
let res = app.run_with(args);
assert!(res.is_ok());
}
#[test]
fn test_subcommand() {
let args = vec![
"--user".into(),
"joe".into(),
"--config".into(),
"c.json".into(),
"thing".into(),
];
let app = App::new()
.opt(Opt::scalar("user").short("u").long("user"))
.command(
Command::new("thing")
.opt(Opt::scalar("config").short("c").long("config"))
.handler(|_, ctx, _| {
assert_eq!(ctx.get_string("user"), Some("joe".into()));
assert_eq!(ctx.get_string("config"), Some("c.json".into()));
}),
);
let res = app.run_with(args);
assert!(res.is_ok());
}
fn function_handler(_app: &App, _ctx: &Context, _args: &[String]) {
assert!(true);
}
#[test]
fn test_function_handler() {
App::new().handler(function_handler);
}
#[test]
fn test_extra_args() {
let args = vec!["somefile".to_string()];
App::new()
.handler(|_, _, args| {
assert_eq!(args.len(), 1);
assert_eq!(args.first(), Some(&"somefile".to_string()));
})
.run_with(args)
.expect("app error");
}
#[test]
fn test_short_flag() {
App::new()
.opt(Opt::flag("verbose").short("v").long("verbose"))
.handler(|_, ctx, _| {
assert_eq!(ctx.flag("verbose"), true);
})
.run_with(vec!["-v".into()])
.unwrap();
}
#[test]
fn test_invalid_long_flag() {
let r = App::new()
.opt(Opt::flag("verbose").short("v").long("verbose"))
.run_with(vec!["--user".into()]);
assert!(r.is_err(), "Should error for invalid long flag");
}
#[test]
fn test_invalid_short_flag() {
let r = App::new()
.opt(Opt::flag("verbose").short("v").long("verbose"))
.run_with(vec!["-u".into()]);
assert!(r.is_err(), "Should error for invalid short flag");
}
}

39
src/command.rs Normal file
View File

@ -0,0 +1,39 @@
use crate::{
context::Context,
opt::{self, Opt},
vox, App,
};
pub type Handler = fn(&App, &Context, &[String]);
pub struct Command {
pub name: String,
pub commands: Vec<Command>,
pub handler: Option<Handler>,
pub opts: Vec<opt::Opt>,
}
impl Command {
pub fn new<T: Into<String>>(name: T) -> Self {
Command {
name: name.into(),
commands: vec![],
handler: None,
opts: vec![],
}
}
pub fn handler(mut self, f: Handler) -> Self {
self.handler = Some(f);
self
}
pub fn opt(mut self, opt: Opt) -> Self {
self.opts.push(opt);
self
}
}
pub fn help(app: &App, _ctx: &Context, _args: &[String]) {
vox::print(app.application_header());
}

31
src/context.rs Normal file
View File

@ -0,0 +1,31 @@
use crate::opt::{ActiveOpt, OptKind};
#[derive(Debug)]
pub struct Context {
opts: Vec<ActiveOpt>,
}
impl Context {
pub(crate) fn new(opts: Vec<ActiveOpt>) -> Self {
Self { opts }
}
pub fn flag(&self, name: &str) -> bool {
self.opts
.iter()
.any(|o| o.definition.name == name && matches!(o.definition.kind, OptKind::Flag))
}
pub fn get_string(&self, name: &str) -> Option<String> {
self.opts
.iter()
.find_map(|o| {
if o.definition.name == name {
Some(o.raw_value.first().cloned())
} else {
None
}
})
.flatten()
}
}

12
src/lib.rs Normal file
View File

@ -0,0 +1,12 @@
mod app;
mod command;
mod context;
mod opt;
// mod style;
// mod tasks;
pub mod vox;
pub use app::*;
pub use command::*;
pub use context::*;
pub use opt::*;
// pub use style::*;

91
src/opt.rs Normal file
View File

@ -0,0 +1,91 @@
#[derive(Debug)]
pub enum OptError {
InvalidOpt(String),
}
#[derive(Clone, Debug)]
pub struct Opt {
pub name: String,
pub short: String,
pub long: String,
pub(crate) kind: OptKind,
}
impl Opt {
/// Create a boolean opt that is present or not and does not accept additional arguments
///
/// Example:
/// ```rust
/// use arkham::{Opt, App};
/// App::new().opt(Opt::flag("verbose").short("v").long("verbose"));
///```
pub fn flag(name: &str) -> Self {
Self {
name: name.into(),
short: "".into(),
long: "".into(),
kind: OptKind::Flag,
}
}
/// Create a opt that accepts additioanl arguments
///
/// Example:
/// ```rust
/// use arkham::{Opt, App};
/// App::new().opt(Opt::scalar("user").short("u").long("user"));
///```
pub fn scalar(name: &str) -> Self {
Self {
name: name.into(),
short: "".into(),
long: "".into(),
kind: OptKind::String,
}
}
/// Sets the short flag that can be used with -x
///
/// Example:
/// ```rust
/// use arkham::{Opt, App};
/// App::new().opt(Opt::scalar("user").short("u").long("user"));
///```
pub fn short(mut self, short: &str) -> Self {
self.short = short.into();
self
}
/// Sets the long flag that can be used with --xxxxx
///
/// Example:
/// ```rust
/// use arkham::{Opt, App};
/// App::new().opt(Opt::scalar("user").short("u").long("user"));
///```
pub fn long(mut self, long: &str) -> Self {
self.long = long.into();
self
}
}
#[derive(Clone, Debug)]
pub(crate) enum OptKind {
Flag,
String,
}
#[derive(Clone, Debug)]
pub(crate) struct ActiveOpt {
pub definition: Opt,
pub raw_value: Vec<String>,
}
impl ActiveOpt {
pub fn new(definition: Opt, raw_value: Vec<String>) -> Self {
ActiveOpt {
definition,
raw_value,
}
}
}

65
src/tasks.rs Normal file
View File

@ -0,0 +1,65 @@
use console::style;
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
use std::{
sync::{Arc, Mutex},
time::Instant,
};
#[derive(Clone)]
pub struct TaskGroup {
mp: Arc<MultiProgress>,
}
impl TaskGroup {
pub fn new() -> Self {
Self {
mp: Arc::new(MultiProgress::new()),
}
}
pub fn start_task(&self, desc: &str) -> Task {
let pb = self.mp.add(ProgressBar::new(1));
let spinner_style = ProgressStyle::default_spinner()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ")
.template("{prefix:.bold.dim} {spinner} {wide_msg}");
pb.set_style(spinner_style);
Task::new(TaskState {
pb,
desc: desc.into(),
started_at: Instant::now(),
})
}
pub fn join(&self) {
self.mp.join().unwrap();
}
}
#[derive(Clone)]
pub struct Task(Arc<Mutex<TaskState>>);
impl Task {
pub fn new(state: TaskState) -> Self {
Self(Arc::new(Mutex::new(state)))
}
pub fn tick(&self) {
let state = self.0.lock().unwrap();
state.pb.set_message(&format!(
"{} [{}]",
state.desc,
style(HumanDuration(state.started_at.elapsed())).yellow()
));
state.pb.tick();
}
pub fn complete(&self) {
self.0.lock().unwrap().pb.finish_with_message("OK");
}
}
pub struct TaskState {
pb: ProgressBar,
desc: String,
started_at: Instant,
}

35
src/vox.rs Normal file
View File

@ -0,0 +1,35 @@
use console::style;
use std::collections::HashMap;
use std::fmt::Display;
pub fn message<T: Display>(str: T) {
println!("{}", style(str).white().bold());
}
pub fn header<T: Display>(str: T) {
println!(
"{} {} {}",
style("-=[").red().dim(),
style(str).white().bold(),
style("]=-").red().dim()
);
}
pub fn note<T: Display>(str: T) {
println!("{}", style(str).white().dim());
}
pub fn description_list(list: HashMap<String, String>) {
let max_length = list.keys().map(|v| v.len()).max().unwrap_or(10) + 3;
for (name, desc) in list {
let spaced_name = format!("{:width$}", name, width = max_length);
println!("{}{}", style(spaced_name).bold(), style(desc).dim())
}
}
pub fn print<T: Display>(s: T) {
println!("{}", s);
}
#[cfg(test)]
mod tests {}