wrapc 0.1.2

A zero-fuss parser for rustc arguments, designed for RUSTC_WRAPPER tools
Documentation
use crate::*;
use std::path::PathBuf;

/// Internal representation of a parsed argument.
#[derive(Debug, Clone)]
#[allow(unused)]
enum Arg {
    // Flags
    Help                , Version           , Verbose               , Test          ,
    OptLevel            , DebugInfo         ,
    
    // Single values
    CrateName(String)   , Edition(String)   , OutDir(PathBuf)       , Out(PathBuf)  ,
    Target(String)      , Sysroot(PathBuf)  , ErrorFormat(String)   , Color(String) ,
    Json(String)        , CapLints(String)  , Explain(String)       , Print(String) ,
    Unpretty(String)    ,
    
    // Collections
    Cfg(String)         , CheckCfg(String)  , CrateType(String)     , Emit(String)  ,
    Extern(String)      , RemapPathPrefix(String)                   ,
    
    LibPath(LibrarySearchPath)              , Link(LinkLib)         ,
    
    Warn(String)        , Allow(String)     , Deny(String)          , Forbid(String),
    
    Codegen(String)     , Z(String)         ,
    
    ArgFile(PathBuf)    , Input(PathBuf)    , Unknown(String)       ,
}

fn split_eq(arg: &str) -> Option<(&str, &str)> {
    let mut parts = arg.splitn(2, '=');
    let key = parts.next()?;
    let val = parts.next()?;
    Some((key, val))
}

fn parse_libpath(val: &str) -> LSP {
    if let Some((kind, path)) = val.split_once('=') {
        let k = match kind {
            "dependency"    => LSPK::Dep        ,
            "crate"         => LSPK::Crate      ,
            "native"        => LSPK::Native     ,
            "framework"     => LSPK::Framework  ,
            _               => LSPK::All        ,
        };
        LSP { kind: k, path: path.into() }
    } else {
        LSP { kind: LSPK::All, path: val.into() }
    }
}

fn parse_link(val: &str) -> LinkLib {
    let (prefix, rest) = match val.split_once('=') {
        Some((p, r)) => (Some(p), r),
        None => (None, val),
    };
    
    let mut kind = None;
    let mut modifiers = Vec::new();
    
    if let Some(p) = prefix {
        let (k_str, mods_str) = match p.split_once(':') {
            Some((k, m)) => (k, Some(m)),
            None => (p, None),
        };
        
        kind = match k_str {
            "dylib"     => Some(LLK::Dylib)     ,
            "static"    => Some(LLK::Static)    ,
            "framework" => Some(LLK::Framework) ,
            _           => None                 ,
        };
        
        if let Some(m) = mods_str {
            for mod_str in m.split(',') {
                modifiers.push(mod_str.to_string());
            }
        }
    }
    
    let (name, rename) = match rest.split_once(':') {
        Some((n, r)) => (n.to_string(), Some(r.to_string())),
        None => (rest.to_string(), None),
    };
    
    LL { kind, modifiers, name, rename }
}

fn parse_emit(val: &str) -> Vec<Emit> {
    let mut emits = Vec::new();
    for part in val.split(',') {
        if let Some((kind, path)) = part.split_once('=') {
            emits.push(Emit { kind: kind.to_string(), path: Some(path.into()) });
        } else {
            emits.push(Emit { kind: part.to_string(), path: None });
        }
    }
    emits
}

fn parse_extern(val: &str) -> Extern {
    if let Some((name, path)) = val.split_once('=') {
        Extern { name: name.to_string(), path: Some(path.into()) }
    } else {
        Extern { name: val.to_string(), path: None }
    }
}

fn parse_remap(val: &str) -> Option<(String, String)> {
    let (from, to) = val.split_once('=')?;
    Some((from.to_string(), to.to_string()))
}

fn parse_arg(arg: &str, next_arg: Option<&str>) -> (Arg, usize) {
    if arg.starts_with('@') {
        return (Arg::ArgFile(arg[1..].into()), 1);
    }
    
    let macro_match = |key: &str, val: &str, consumed: usize| -> (Arg, usize) {
        match key {
            "--crate-name"          => (Arg::CrateName      (val.to_string()    ), consumed ),
            "--edition"             => (Arg::Edition        (val.to_string()    ), consumed ),
            "--out-dir"             => (Arg::OutDir         (val.into()         ), consumed ),
            "-o" | "--out"          => (Arg::Out            (val.into()         ), consumed ),
            "--target"              => (Arg::Target         (val.to_string()    ), consumed ),
            "--sysroot"             => (Arg::Sysroot        (val.into()         ), consumed ),
            "--error-format"        => (Arg::ErrorFormat    (val.to_string()    ), consumed ),
            "--color"               => (Arg::Color          (val.to_string()    ), consumed ),
            "--json"                => (Arg::Json           (val.to_string()    ), consumed ),
            "--cap-lints"           => (Arg::CapLints       (val.to_string()    ), consumed ),
            "--explain"             => (Arg::Explain        (val.to_string()    ), consumed ),
            "--print"               => (Arg::Print          (val.to_string()    ), consumed ),
            "--unpretty"            => (Arg::Unpretty       (val.to_string()    ), consumed ),
            "--cfg"                 => (Arg::Cfg            (val.to_string()    ), consumed ),
            "--check-cfg"           => (Arg::CheckCfg       (val.to_string()    ), consumed ),
            "--crate-type"          => (Arg::CrateType      (val.to_string()    ), consumed ),
            "--emit"                => (Arg::Emit           (val.to_string()    ), consumed ),
            "--extern"              => (Arg::Extern         (val.to_string()    ), consumed ),
            "--remap-path-prefix"   => (Arg::RemapPathPrefix(val.to_string()    ), consumed ),
            "-W" | "--warn"         => (Arg::Warn           (val.to_string()    ), consumed ),
            "-A" | "--allow"        => (Arg::Allow          (val.to_string()    ), consumed ),
            "-D" | "--deny"         => (Arg::Deny           (val.to_string()    ), consumed ),
            "-F" | "--forbid"       => (Arg::Forbid         (val.to_string()    ), consumed ),
            "-C" | "--codegen"      => (Arg::Codegen        (val.to_string()    ), consumed ),
            "-Z"                    => (Arg::Z              (val.to_string()    ), consumed ),
            "-L"                    => (Arg::LibPath        (parse_libpath(val) ), consumed ),
            "-l"                    => (Arg::Link           (parse_link(val)    ), consumed ),
            _                       => (Arg::Unknown        (arg.to_string()    ), 1        ),
        }
    };

    if let Some((key, val)) = split_eq(arg) {
        let res = macro_match(key, val, 1);
        if let Arg::Unknown(_) = res.0 {
            // Fall through to exact match or prefix logic
        } else {
            return res;
        }
    }
    
    match arg {
        "-h" | "--help"     => (Arg::Help       , 1),
        "-V" | "--version"  => (Arg::Version    , 1),
        "-v" | "--verbose"  => (Arg::Verbose    , 1),
        "--test"            => (Arg::Test       , 1),
        "-O"                => (Arg::OptLevel   , 1),
        "-g"                => (Arg::DebugInfo  , 1),
        
        "--crate-name" | "--edition" | "--out-dir" | "-o" | "--out" | "--target" | 
        "--sysroot" | "--error-format" | "--color" | "--json" | "--cap-lints" | 
        "--explain" | "--print" | "--unpretty" | "--cfg" | "--check-cfg" | 
        "--crate-type" | "--emit" | "--extern" | "--remap-path-prefix" | 
        "-W" | "--warn" | "-A" | "--allow" | "-D" | "--deny" | "-F" | "--forbid" | 
        "-C" | "--codegen" | "-Z" | "-L" | "-l" => {
            if let Some(val) = next_arg {
                macro_match(arg, val, 2)
            } else {
                (Arg::Unknown(arg.to_string()), 1)
            }
        }
        
        _ => {
            if arg.starts_with('-') {
                if let Some(stripped) = arg.strip_prefix("-C") { return (Arg::Codegen(stripped.to_string()), 1); }
                if let Some(stripped) = arg.strip_prefix("-Z") { return (Arg::Z(stripped.to_string()), 1); }
                if let Some(stripped) = arg.strip_prefix("-W") { return (Arg::Warn(stripped.to_string()), 1); }
                if let Some(stripped) = arg.strip_prefix("-A") { return (Arg::Allow(stripped.to_string()), 1); }
                if let Some(stripped) = arg.strip_prefix("-D") { return (Arg::Deny(stripped.to_string()), 1); }
                if let Some(stripped) = arg.strip_prefix("-F") { return (Arg::Forbid(stripped.to_string()), 1); }
                if let Some(stripped) = arg.strip_prefix("-l") { return (Arg::Link(parse_link(stripped)), 1); }
                if let Some(stripped) = arg.strip_prefix("-L") { return (Arg::LibPath(parse_libpath(stripped)), 1); }
                
                (Arg::Unknown(arg.to_string()), 1)
            } else {
                (Arg::Input(arg.into()), 1)
            }
        }
    }
}

/// Parses an iterator of strings into an [`Info`] struct.
/// 
/// This function handles the standard `RUSTC_WRAPPER` invocation format:
/// `<wrapper> - <rustc> <args...>`.
pub fn parse<I, S>(args: I) -> Result<Info, ParseError>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    let mut args: Vec<String> = args.into_iter().map(|s| s.as_ref().to_string()).collect();
    let mut info = Info::default();
    
    info.rustc = Some(args[2].clone());
    args.drain(0..3);

    let mut i = 0;
    while i < args.len() {
        let arg = &args[i];
        let next_arg = args.get(i + 1).map(|s| s.as_str());
        
        if arg == "--" {
            info.unknown.push(arg.clone());
            for rest in &args[i+1..] {
                info.unknown.push(rest.clone());
            }
            break;
        }
        
        let (parsed, consumed) = parse_arg(arg, next_arg);
        
        match parsed {
            Arg::Help               => info.help            = true      ,
            Arg::Version            => info.version         = true      ,
            Arg::Verbose            => info.verbose         = true      ,
            Arg::Test               => info.test            = true      ,
            Arg::OptLevel           => info.opt_level       = true      ,
            Arg::DebugInfo          => info.debug_info      = true      ,
            
            Arg::CrateName(v)       => info.crate_name      = Some(v)   ,
            Arg::Edition(v)         => info.edition         = Some(v)   ,
            Arg::OutDir(v)          => info.out_dir         = Some(v)   ,
            Arg::Out(v)             => info.out             = Some(v)   ,
            Arg::Target(v)          => info.target          = Some(v)   ,
            Arg::Sysroot(v)         => info.sysroot         = Some(v)   ,
            Arg::ErrorFormat(v)     => info.error_format    = Some(v)   ,
            Arg::Color(v)           => info.color           = Some(v)   ,
            Arg::Json(v)            => info.json            = Some(v)   ,
            Arg::CapLints(v)        => info.cap_lints       = Some(v)   ,
            Arg::Explain(v)         => info.explain         = Some(v)   ,
            Arg::Print(v)           => info.print           = Some(v)   ,
            Arg::Unpretty(v)        => info.unpretty        = Some(v)   ,
            
            Arg::Cfg(v)             => info.configs         . push(v)   ,
            Arg::CheckCfg(v)        => info.confchecks      . push(v)   ,
            Arg::CrateType(v)       => info.crate_types     . push(v)   ,
            Arg::LibPath(v)         => info.libpaths        . push(v)   ,
            Arg::Link(v)            => info.links           . push(v)   ,
            Arg::Warn(v)            => info.warns           . push(v)   ,
            Arg::Allow(v)           => info.allows          . push(v)   ,
            Arg::Deny(v)            => info.denies          . push(v)   ,
            Arg::Forbid(v)          => info.forbids         . push(v)   ,
            Arg::Codegen(v)         => info.codegen_opts    . push(v)   ,
            Arg::Z(v)               => info.z_opts          . push(v)   ,
            Arg::Input(v)           => info.inputs          . push(v)   ,
            Arg::Unknown(v)         => info.unknown         . push(v)   ,
            Arg::Extern(v)          => info.externs         . push  (parse_extern   (&v)),
            Arg::ArgFile(_)         => info.unknown         . push  (arg.clone()        ),
            Arg::Emit(v)            => info.emits           . extend(parse_emit     (&v)),

            Arg::RemapPathPrefix(v) => {
                if let Some(remap) = parse_remap(&v) {
                    info.remap_path_prefixes.push(remap);
                }
            }
        }
        
        i += consumed;
    }
    
    Ok(info)
}

/// Fetches arguments from `std::env::args()` and parses them.
pub fn fetch() -> Result<Info, ParseError> {
    let args: Vec<String> = std::env::args().collect();
    parse(&args)
}