From c9ae7be6d0e7b37fff115069d146d9790cf3589c Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Sat, 19 Jun 2021 03:20:29 -0400 Subject: [PATCH] Core app foundation/opt parsing --- .gitignore | 2 + Cargo.lock | 667 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 18 ++ benches/arg_parsing.rs | 42 +++ examples/fib.rs | 29 ++ examples/tasks.rs | 8 + src/app.rs | 311 +++++++++++++++++++ src/command.rs | 39 +++ src/context.rs | 31 ++ src/lib.rs | 12 + src/opt.rs | 91 ++++++ src/tasks.rs | 65 ++++ src/vox.rs | 35 +++ 13 files changed, 1350 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 benches/arg_parsing.rs create mode 100644 examples/fib.rs create mode 100644 examples/tasks.rs create mode 100644 src/app.rs create mode 100644 src/command.rs create mode 100644 src/context.rs create mode 100644 src/lib.rs create mode 100644 src/opt.rs create mode 100644 src/tasks.rs create mode 100644 src/vox.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f380ac0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +debug/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..95d9a46 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b16c3b9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "arkham" +version = "0.1.0" +authors = ["Joe Bellus "] +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 diff --git a/benches/arg_parsing.rs b/benches/arg_parsing.rs new file mode 100644 index 0000000..3f3f92b --- /dev/null +++ b/benches/arg_parsing.rs @@ -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); diff --git a/examples/fib.rs b/examples/fib.rs new file mode 100644 index 0000000..bc0b512 --- /dev/null +++ b/examples/fib.rs @@ -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), + } +} diff --git a/examples/tasks.rs b/examples/tasks.rs new file mode 100644 index 0000000..6f71351 --- /dev/null +++ b/examples/tasks.rs @@ -0,0 +1,8 @@ +// use arkham::TaskGroup; + +fn main() { + // let tasks = TaskGroup::new(); + // let task = tasks.start_task("task 1"); + // task.tick(); + // tasks.join(); +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..6fd94bb --- /dev/null +++ b/src/app.rs @@ -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 = std::result::Result; + +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) -> 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) -> 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 = 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 = 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"); + } +} diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..30209d0 --- /dev/null +++ b/src/command.rs @@ -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, + pub handler: Option, + pub opts: Vec, +} + +impl Command { + pub fn new>(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()); +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..0ec5c4e --- /dev/null +++ b/src/context.rs @@ -0,0 +1,31 @@ +use crate::opt::{ActiveOpt, OptKind}; + +#[derive(Debug)] +pub struct Context { + opts: Vec, +} + +impl Context { + pub(crate) fn new(opts: Vec) -> 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 { + self.opts + .iter() + .find_map(|o| { + if o.definition.name == name { + Some(o.raw_value.first().cloned()) + } else { + None + } + }) + .flatten() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5f387c9 --- /dev/null +++ b/src/lib.rs @@ -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::*; diff --git a/src/opt.rs b/src/opt.rs new file mode 100644 index 0000000..dd6d457 --- /dev/null +++ b/src/opt.rs @@ -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, +} + +impl ActiveOpt { + pub fn new(definition: Opt, raw_value: Vec) -> Self { + ActiveOpt { + definition, + raw_value, + } + } +} diff --git a/src/tasks.rs b/src/tasks.rs new file mode 100644 index 0000000..ccbb518 --- /dev/null +++ b/src/tasks.rs @@ -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, +} + +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>); + +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, +} diff --git a/src/vox.rs b/src/vox.rs new file mode 100644 index 0000000..4aa1e1b --- /dev/null +++ b/src/vox.rs @@ -0,0 +1,35 @@ +use console::style; +use std::collections::HashMap; +use std::fmt::Display; + +pub fn message(str: T) { + println!("{}", style(str).white().bold()); +} + +pub fn header(str: T) { + println!( + "{} {} {}", + style("-=[").red().dim(), + style(str).white().bold(), + style("]=-").red().dim() + ); +} + +pub fn note(str: T) { + println!("{}", style(str).white().dim()); +} + +pub fn description_list(list: HashMap) { + 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(s: T) { + println!("{}", s); +} + +#[cfg(test)] +mod tests {}