From 2d711d2dfaa8f7b774d8a47bdb04064abf57b6c1 Mon Sep 17 00:00:00 2001 From: Thom Dickson Date: Thu, 16 Dec 2021 03:13:59 -0500 Subject: [PATCH] Add basic command line boilerplate and arg parsing --- .gitignore | 1 + Cargo.lock | 131 ++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 11 +++++ src/config.rs | 55 +++++++++++++++++++++ src/lib.rs | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 13 +++++ 6 files changed, 345 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/config.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a74d183 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,131 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "proc-macro2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "serde" +version = "1.0.131" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.131" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yaml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "shepherd" +version = "0.1.0" +dependencies = [ + "serde", + "serde_yaml", + "toml", +] + +[[package]] +name = "syn" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..566c7db --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "shepherd" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.8" +toml = "0.5" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..170be29 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,55 @@ +use std::error::Error; +use serde::{Deserialize, Serialize}; +use std::env; +use std::fs; +use std::path::Path; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +/// The internal representation of the configuration file. +pub struct Config { + pub source_dir: String, +} + +impl Config { + /// Create new Config struct with default values + /// + /// The `source_dir` field defaults to `$HOME/sources` + pub fn new() -> Config { + Config { + source_dir: format!("{}/sources", env::var("HOME").unwrap()), + } + } + + /// Read TOML file and load values into a Config struct + /// + /// If the filename doesn't exist, read_config() will write the current struct to the given + /// config file. + pub fn read_config(&mut self, filename: &str) -> Result<(), Box> { + // Load raw yaml file. If the file doesn't exist, create it + if std::path::Path::new(filename).exists() { + let raw = fs::read_to_string(filename)?; + // Convert string to our Config struct + let toml: Config = toml::from_str(&raw)?; + *self = toml; + } else { + let config: String = toml::to_string(&self)?; + // Get the path to the config file + let path = match Path::new(filename).parent() { + Some(x) => x, + _ => Path::new(filename) + }; + // Make sure the path exists + fs::create_dir_all(path).unwrap(); + // Write default Config struct to file + fs::write(filename, config).expect("Couldn't write file"); + } + Ok(()) + } + + /// Create a string of the current loaded config. + /// + /// Useful for dumping the config + pub fn to_string(&self) -> Result { + toml::to_string_pretty(self) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..98f276f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,134 @@ +pub mod config; +use config::Config; +use std::error::Error; +use std::env; + +#[derive(Debug)] +/// Holds the current state of the application. +/// +/// This is used when processing command line arguments, and is thus loaded before the +/// configuration file is. +pub struct State { + cmd: Option, + url: Option, + pub config: String, +} + +#[derive(Debug)] +/// Used to signal which main command is going to be run +enum Cmd { + Fetch, + Help, + DumpConfig, +} + +impl State { + /// Parses command line flags/arguments and creates the application state. + /// + /// Argument parsing goes left to right. The first not flag argument it comes across is set as + /// the cmd. + /// + /// Parsing priority is as follows: + /// - `--long-flag` type arguments + /// - `-mcsf` multi character short flag type arguments + /// - `command` type arguments + pub fn new(mut args: std::env::Args) -> State { + let mut state = State { + cmd: None, + url: None, + config: format!("{}/.config/shepherd/config.toml", env::var("HOME").unwrap()), + }; + + let mut arg = args.next(); + // Iterate through the command line arguments + while let Some(x) = &arg { + // Long arguments + if x.starts_with("--") { + let option = x.strip_prefix("--").unwrap(); + match option { + "help" => state.cmd = Some(Cmd::Help), + "dump-config" => state.cmd = Some(Cmd::DumpConfig), + "config" => { + let file = args.next(); + match file { + Some(x) => state.config = x, + None => eprintln!("Expected config argument"), + } + } + _ => {} + } + } + // short arguments + else if x.starts_with("-") { + let options = x.strip_prefix("-").unwrap().chars(); + for opt in options { + match opt { + 'h' => state.cmd = Some(Cmd::Help), + _ => {} + } + } + } + // Fetch command + else if x == "fetch" { + let url = args.next(); + match url { + Some(x) => match state.cmd { + None => { + state.cmd = Some(Cmd::Fetch); + state.url = Some(x); + } + _ => {} + }, + None => { + eprintln!("No URL provided for \'number\' command\n"); + } + } + } + arg = args.next(); + } + + // Fall back to Help command if no other command was given + state.cmd = match state.cmd { + Some(x) => Some(x), + None => Some(Cmd::Help), + }; + state + } +} + +pub fn run(state: State, config: Config) -> Result<(), Box> { + match state.cmd { + Some(Cmd::Help) => { + println!("{}", help_msg()); + } + Some(Cmd::DumpConfig) => { + println!("{}", config.to_string().unwrap()); + } + Some(x) => { + println!("{:?} hasn't been implemented yet!", x) + } + _ => {} + } + Ok(()) +} + +fn help_msg() -> String { + format!( + "Git repository manager + +USAGE: + shepherd [--help] [] + +OPTIONS: + -h, --help Print out this help message + --dump-config dump the current configuration + +COMMANDS: +General + help Print out this help message + +Manage Repositories + clone Add another git repo to keep track of + fetch Update currently tracked repos" + ) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8936332 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,13 @@ +use std::env; +use shepherd::State; +use shepherd::config::Config; + +fn main() { + let args = env::args(); + let state = State::new(args); + let mut config = Config::new(); + config.read_config(&state.config).unwrap(); + if let Err(e) = shepherd::run(state, config) { + eprintln!("{}", e); + } +}