diff --git a/Cargo.toml b/Cargo.toml index d193171..b5d8473 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,10 @@ [package] name = "snakify" -version = "1.0.0" +version = "1.1.0" edition = "2021" -authors = ["m-hgn"] +authors = ["mxhagen"] description = "Quickly snake-caseify any input text" license = "GPL-3.0" readme = "README.md" - -[dependencies] diff --git a/src/main.rs b/src/main.rs index f13e096..47128d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,78 @@ -#![feature(iter_intersperse)] -fn main() { - let args = std::env::args(); - let separators: Vec = vec!['-', '_', ' ']; +use Flag::*; - let input: String = args.skip(1).intersperse(" ".to_string()).collect(); - - let output: String = input - .chars() - .map(|c| { - if separators.contains(&c) { - '_' - } else { - c.to_lowercase().next().unwrap_or_else(|| { - panic!("Error: couldn't convert character to lowercase: `{}`", c) - }) - } - }) - .collect(); - - println!("{}", output); +#[derive(PartialEq, Eq, Debug)] +enum Flag { + Force, + Help, + Version, +} + +fn main() { + let (appname, version) = (env!("CARGO_BIN_NAME"), env!("CARGO_PKG_VERSION")); + let (flag, arg_start) = std::env::args() + .enumerate() + .skip(1) + .next() + .map(|(i, arg)| match arg.as_str() { + "-f" | "--force" => (Some(Force), i), + "-h" | "--help" => (Some(Help), i), + "-V" | "--version" => (Some(Version), i), + _ => (None, 0), + }) + .unwrap_or_default(); + + if flag == Some(Help) { + println!("Convert space-separated strings to `snake_case` and print to stdout."); + println!("If input contains invalid symbols, exits with code 1."); + println!(""); + println!("Usage: {appname} [Option] [...]"); + println!(""); + println!("Arguments:"); + println!(" [...] One or more strings treated as a single input"); + println!(""); + println!("Options:"); + println!(" -f, --force Force successful conversion, ignoring invalid chars"); + println!(" -h, --help Print this usage information"); + println!(" -V, --version Print version information"); + std::process::exit(0); + } else if flag == Some(Version) { + println!("{appname} {version}"); + std::process::exit(0); + } + + let mut arg_warned = false; + let mut output = String::new(); + for arg in std::env::args().skip(1 + arg_start) { + if !arg_warned + && matches!( + arg.as_str(), + "-f" | "--force" | "-h" | "--help" | "-V" | "--version" + ) + { + eprintln!("Warning: You might have passed an option where input was expected."); + eprintln!(" If this was unintentional: pass options *before* input."); + eprintln!(""); + arg_warned = true; + } + + let last_was_separator = output.bytes().last().map(|x| x == b'_').unwrap_or(true); + for c in arg.chars() { + match c { + ' ' | '-' | '_' if last_was_separator => {} + ' ' | '-' | '_' => output.push('_'), + + 'a'..='z' | 'A'..='Z' | '0'..='9' => output.push(c.to_ascii_lowercase()), + _ if flag == Some(Force) => output.push(c), + + _ => { + eprintln!("Error: Invalid snake_case character: `{c}`"); + std::process::exit(1); + } + } + } + output.push('_') + } + output.pop(); + + println!("{output}"); }