Add basic command line boilerplate and arg parsing

This commit is contained in:
Thom Dickson 2021-12-16 03:13:59 -05:00
commit 2d711d2dfa
Signed by: boots
GPG Key ID: 40BE2AF8EBF8D2BB
6 changed files with 345 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

131
Cargo.lock generated Normal file
View File

@ -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",
]

11
Cargo.toml Normal file
View File

@ -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"

55
src/config.rs Normal file
View File

@ -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<dyn Error>> {
// 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<String, toml::ser::Error> {
toml::to_string_pretty(self)
}
}

134
src/lib.rs Normal file
View File

@ -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<Cmd>,
url: Option<String>,
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<dyn Error>> {
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] <command> [<args>]
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"
)
}

13
src/main.rs Normal file
View File

@ -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);
}
}