use crate::{
config::{maybe_config, DependencyInference},
ThagError, ThagResult, RS_SUFFIX,
};
use bitflags::bitflags;
use clap::{ArgGroup , Parser};
use firestorm::{profile_fn, profile_method, profile_section};
use std::{fmt, str};
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone, Default, Parser, Debug)]
#[command(name = "thag_rs", version, about, long_about/*, color = ColorChoice::Always, styles=get_styles(), next_help_heading = Some("Information Options")*/)]
#[command(group(
ArgGroup::new("commands")
.required(true)
.args(&["script", "expression", "repl", "filter", "stdin", "edit", "config"]),
))]
#[command(group(
ArgGroup::new("verbosity")
.required(false)
.args(&["quiet", "normal", "verbose"]),
))]
#[command(group(
ArgGroup::new("norun_options")
.required(false)
.args(&["generate", "build", "check", "executable", "expand", "cargo"]),
))]
pub struct Cli {
pub script: Option<String>,
#[arg(last = true, requires = "script")]
pub args: Vec<String>,
#[arg(short, long, requires = "script", help_heading = Some("Processing Options"))]
pub force: bool,
#[arg(short, long = "expr", help_heading = Some("Dynamic Options (no script)"), conflicts_with_all(["generate", "build"]))]
pub expression: Option<String>,
#[arg(short = 'r', long, help_heading = Some("Dynamic Options (no script)"), conflicts_with_all(["generate", "build"]))]
pub repl: bool,
#[arg(short, long, help_heading = Some("Dynamic Options (no script)"), conflicts_with_all(["generate", "build"]))]
pub stdin: bool,
#[arg(short = 'd', long, help_heading = Some("Dynamic Options (no script)"), conflicts_with_all(["generate", "build"]))]
pub edit: bool,
#[arg(short = 'l', long = "loop", help_heading = Some("Dynamic Options (no script)"), conflicts_with_all(["generate", "build"]))]
pub filter: Option<String>,
#[arg(short = 'M', long, help_heading = Some("Dynamic Options (no script)"), requires = "filter", value_name = "CARGO-TOML")]
pub toml: Option<String>,
#[arg(short = 'B', long, help_heading = Some("Dynamic Options (no script)"), requires = "filter", value_name = "PRE-LOOP")]
pub begin: Option<String>,
#[arg(short = 'E', long, help_heading = Some("Dynamic Options (no script)"), requires = "filter", value_name = "POST-LOOP")]
pub end: Option<String>,
#[arg(short, long, help_heading = Some("Processing Options"))]
pub multimain: bool,
#[arg(short, long, help_heading = Some("Output Options"))]
pub timings: bool,
#[arg(short, long, help_heading = Some("Output Options"), action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(short = 'N', long = "normal verbosity", help_heading = Some("Output Options"))]
pub normal: bool,
#[arg(short, long, help_heading = Some("Output Options"), action = clap::ArgAction::Count)]
pub quiet: u8,
#[arg(short, long = "gen", help_heading = Some("No-run Options"), default_value_ifs([
/*("force", "true", "true"),*/
("expression", "_", "true"),
("executable", "true", "true"),
("check", "true", "true"),
]))]
pub generate: bool,
#[arg(short, long, help_heading = Some("No-run Options"), default_value_ifs([
/*("force", "true", "true"),*/
("expression", "_", "true"),
("executable", "true", "true"),
]))]
pub build: bool,
#[arg(short = 'x', long, help_heading = Some("No-run Options"))]
pub executable: bool,
#[arg(short, long, help_heading = Some("No-run Options"))]
pub check: bool,
#[arg(short = 'X', long, help_heading = Some("No-run Options"))]
pub expand: bool,
#[arg(
short,
long, help_heading = Some("Output Options"),
// 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,
#[arg(short = 'i', long, help_heading = Some("Processing Options"))]
pub infer: Option<DependencyInference>,
#[arg(short = 'A', long, requires = "script", help_heading = Some("No-run Options"))]
pub cargo: bool,
}
#[must_use]
pub fn get_args() -> Cli {
profile_fn!(get_args);
Cli::parse()
}
pub fn validate_args(args: &Cli, proc_flags: &ProcFlags) -> ThagResult<()> {
profile_fn!(validate_args);
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 DEBUG = 16384;
const VERBOSE = 32768;
const NORMAL = 65536;
const QUIET = 131_072;
const QUIETER = 262_144;
const UNQUOTE = 524_288;
const CONFIG = 1_048_576;
const EXPAND = 2_097_152;
const CARGO = 4_194_304;
const INFER = 8_388_608;
}
}
impl fmt::Debug for ProcFlags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
profile_method!(proc_flags_fmt_debug);
bitflags::parser::to_writer(self, f)
}
}
impl fmt::Display for ProcFlags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
profile_method!(proc_flags_fmt_display);
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> {
profile_method!(proc_flags_from_str);
bitflags::parser::from_str(flags)
}
}
pub fn get_proc_flags(args: &Cli) -> ThagResult<ProcFlags> {
profile_fn!(get_proc_flags);
let is_expr = args.expression.is_some();
let is_loop = args.filter.is_some();
let is_infer = args.infer.is_some();
profile_section!(init_config_loop_assert);
let proc_flags = {
let mut proc_flags = ProcFlags::empty();
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 == 1);
proc_flags.set(ProcFlags::DEBUG, args.verbose >= 2);
proc_flags.set(ProcFlags::TIMINGS, args.timings);
proc_flags.set(
ProcFlags::NORUN,
args.generate | args.build | args.check | args.executable | args.expand | args.cargo,
);
proc_flags.set(ProcFlags::NORMAL, args.normal);
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::EXPAND, args.expand);
proc_flags.set(ProcFlags::CARGO, args.cargo);
proc_flags.set(ProcFlags::INFER, is_infer);
profile_section!(config_loop_assert);
let unquote = args.unquote.map_or_else(
|| maybe_config().map_or_else(|| false, |config| config.misc.unquote),
|unquote| {
unquote
},
);
proc_flags.set(ProcFlags::UNQUOTE, unquote);
proc_flags.set(ProcFlags::CONFIG, args.config);
profile_section!(loop_assert);
if !is_loop && (args.toml.is_some() || args.begin.is_some() || args.end.is_some()) {
if args.toml.is_some() {
eprintln!("Option --toml (-M) requires --loop (-l)");
}
if args.begin.is_some() {
eprintln!("Option --begin (-B) requires --loop (-l)");
}
if args.end.is_some() {
eprintln!("Option --end (-E) requires --loop (-l)");
}
return Err("Missing --loop option".into());
}
#[cfg(debug_assertions)]
{
profile_section!(assert);
let formatted = proc_flags.to_string();
let parsed = formatted.parse::<ProcFlags>()?;
assert_eq!(proc_flags, parsed);
}
Ok::<ProcFlags, ThagError>(proc_flags)
}?;
Ok(proc_flags)
}