use bitflags::{bitflags, parser::ParseError};
use clap::{ArgGroup, Parser};
use std::{error::Error, fmt, str};
pub const RS_SUFFIX: &str = ".rs";
#[derive(Debug)]
struct ParseFlagsError(ParseError);
impl std::fmt::Display for ParseFlagsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to parse flags: {}", self.0)
}
}
impl std::error::Error for ParseFlagsError {}
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone, Default, Parser, Debug)]
#[command(name = "thag_rs", version, about, long_about)]
#[command(group(
ArgGroup::new("commands")
.required(true)
.args(&["script", "expression", "repl", "filter", "stdin", "edit", "config"]),
))]
#[command(group(
ArgGroup::new("volume")
.required(false)
.args(&["quiet", "normal", "verbose"]),
))]
pub struct Cli {
pub script: Option<String>,
#[arg(last = true, requires = "script")]
pub args: Vec<String>,
#[arg(short, long = "gen", default_value_ifs([
("force", "true", "true"),
("expression", "_", "true"),
("executable", "true", "true"),
("check", "true", "true"),
]))]
pub generate: bool,
#[arg(short, long, default_value_ifs([
("force", "true", "true"),
("expression", "_", "true"),
("executable", "true", "true"),
]))]
pub build: bool,
#[arg(short, long)]
pub force: bool,
#[arg(short, long, conflicts_with_all(["edit", "expression", "filter", "repl", "stdin"]))]
pub norun: bool,
#[arg(short = 'x', long)]
pub executable: bool,
#[arg(short, long, conflicts_with_all(["build", "executable"]))]
pub check: bool,
#[arg(short, long = "expr", conflicts_with_all(["generate", "build"]))]
pub expression: Option<String>,
#[arg(short = 'r', long, conflicts_with_all(["generate", "build"]))]
pub repl: bool,
#[arg(short, long, conflicts_with_all(["generate", "build"]))]
pub stdin: bool,
#[arg(short = 'd', long, conflicts_with_all(["generate", "build"]))]
pub edit: bool,
#[arg(short = 'l', long = "loop", conflicts_with_all(["generate", "build"]))]
pub filter: Option<String>,
#[arg(short = 'T', long, requires = "filter", value_name = "CARGO-TOML")]
pub toml: Option<String>,
#[arg(short = 'B', long, requires = "filter", value_name = "PRE-LOOP")]
pub begin: Option<String>,
#[arg(short = 'E', long, requires = "filter", value_name = "POST-LOOP")]
pub end: Option<String>,
#[arg(short, long)]
pub multimain: bool,
#[arg(short, long)]
pub timings: bool,
#[arg(short, long)]
pub verbose: bool,
#[arg(short = 'N', long = "normal verbosity")]
pub normal: bool,
#[arg(short, long, action = clap::ArgAction::Count, conflicts_with("verbose"))]
pub quiet: u8,
#[arg(
short,
long,
// require_equals = true,
action = clap::ArgAction::Set,
num_args = 0..=1,
default_missing_value = "true", // Default to true if -u is present but no value is given
conflicts_with("multimain")
)]
pub unquote: Option<bool>,
#[arg(short = 'C', long, conflicts_with_all(["generate", "build", "executable"]))]
pub config: bool,
}
#[must_use]
pub fn get_args() -> Cli {
Cli::parse()
}
pub fn validate_args(args: &Cli, proc_flags: &ProcFlags) -> Result<(), Box<dyn Error>> {
if let Some(ref script) = args.script {
if !script.ends_with(RS_SUFFIX) {
return Err(format!("Script name {script} must end in {RS_SUFFIX}").into());
}
} else if !proc_flags.contains(ProcFlags::EXPR)
&& !proc_flags.contains(ProcFlags::REPL)
&& !proc_flags.contains(ProcFlags::STDIN)
&& !proc_flags.contains(ProcFlags::EDIT)
&& !proc_flags.contains(ProcFlags::LOOP)
&& !proc_flags.contains(ProcFlags::CONFIG)
{
return Err("Missing script name".into());
}
Ok(())
}
bitflags! {
#[derive(Clone, Default, PartialEq, Eq)]
pub struct ProcFlags: u32 {
const GENERATE = 1;
const BUILD = 2;
const FORCE = 4;
const RUN = 8;
const NORUN = 16;
const EXECUTABLE = 32;
const CHECK = 64;
const REPL = 128;
const EXPR = 256;
const STDIN = 512;
const EDIT = 1024;
const LOOP = 2048;
const MULTI = 4096;
const TIMINGS = 8192;
const VERBOSE = 16384;
const NORMAL = 32768;
const QUIET = 65536;
const QUIETER = 131_072;
const UNQUOTE = 262_144;
const CONFIG = 524_288;
}
}
impl fmt::Debug for ProcFlags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
bitflags::parser::to_writer(self, f)
}
}
impl fmt::Display for ProcFlags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
bitflags::parser::to_writer(self, f)
}
}
impl str::FromStr for ProcFlags {
type Err = bitflags::parser::ParseError;
fn from_str(flags: &str) -> Result<Self, Self::Err> {
bitflags::parser::from_str(flags)
}
}
pub fn get_proc_flags(args: &Cli) -> Result<ProcFlags, Box<dyn Error>> {
let is_expr = args.expression.is_some();
let is_loop = args.filter.is_some();
let proc_flags = {
let mut proc_flags = ProcFlags::empty();
eprintln!("args={args:#?}");
proc_flags.set(ProcFlags::GENERATE, args.generate);
proc_flags.set(ProcFlags::BUILD, args.build);
proc_flags.set(ProcFlags::CHECK, args.check);
proc_flags.set(ProcFlags::FORCE, args.force);
proc_flags.set(ProcFlags::QUIET, args.quiet == 1);
proc_flags.set(ProcFlags::QUIETER, args.quiet >= 2);
proc_flags.set(ProcFlags::MULTI, args.multimain);
proc_flags.set(ProcFlags::VERBOSE, args.verbose);
proc_flags.set(ProcFlags::TIMINGS, args.timings);
proc_flags.set(ProcFlags::NORUN, args.norun | args.check | args.executable);
proc_flags.set(ProcFlags::NORMAL, args.normal);
let gen_build = !args.norun && !args.executable && !args.check;
eprintln!("gen_build={gen_build}");
if gen_build {
proc_flags.set(ProcFlags::GENERATE | ProcFlags::BUILD, true);
}
proc_flags.set(ProcFlags::RUN, !proc_flags.contains(ProcFlags::NORUN));
proc_flags.set(ProcFlags::REPL, args.repl);
proc_flags.set(ProcFlags::EXPR, is_expr);
proc_flags.set(ProcFlags::STDIN, args.stdin);
proc_flags.set(ProcFlags::EDIT, args.edit);
proc_flags.set(ProcFlags::LOOP, is_loop);
proc_flags.set(ProcFlags::EXECUTABLE, args.executable);
proc_flags.set(ProcFlags::CONFIG, args.config);
if !is_loop && (args.toml.is_some() || args.begin.is_some() || args.end.is_some()) {
if args.toml.is_some() {
eprintln!("Option {} ({}) requires --loop (-l)", "--toml", "-T");
}
if args.begin.is_some() {
eprintln!("Option {} ({}) requires --loop (-l)", "--begin", "-B");
}
if args.end.is_some() {
eprintln!("Option {} ({}) requires --loop (-l)", "--end", "-E");
}
return Err("Missing --loop option".into());
}
let formatted = proc_flags.to_string();
let parsed = formatted
.parse::<ProcFlags>()
.map_err(|e| Box::new(ParseFlagsError(e)) as Box<dyn std::error::Error>)?;
assert_eq!(proc_flags, parsed);
Ok::<ProcFlags, Box<dyn Error>>(proc_flags)
}?;
Ok(proc_flags)
}
#[allow(dead_code)]
fn main() {
let opt = Cli::parse();
if opt.verbose {
println!("Verbosity enabled");
}
if opt.timings {
println!("Timings enabled");
}
if opt.generate {
println!("Generate option selected");
}
if opt.build {
println!("Build option selected");
}
if opt.force {
println!("Force option selected");
}
if opt.executable {
println!("Executable option selected");
}
println!("Unquote={:#?}", opt.unquote);
if opt.executable {
println!("Config option selected");
}
println!("Script to run: {:?}", opt.script);
if !opt.args.is_empty() {
println!("With arguments:");
for arg in &opt.args {
println!("{arg}");
}
}
let proc_flags = get_proc_flags(&opt);
println!("proc_flags={proc_flags:#?}");
}