makers 0.8.0

a POSIX-compatible make implemented in Rust
use std::env;
use std::fmt;
use std::process::{Command, ExitStatus};

use eyre::{bail, Error};
#[cfg(feature = "full")]
use lazy_static::lazy_static;
#[cfg(feature = "full")]
use regex::Regex;

#[cfg(feature = "full")]
use super::r#macro::Set as MacroSet;
use super::target::Target;
use super::token::{Token, TokenString};
use super::Makefile;

// inspired by python's subprocess module
fn execute_command_line(
    command_line: &str,
    ignore_errors: bool,
    #[cfg(feature = "full")] macros: &MacroSet,
) -> Result<ExitStatus, Error> {
    let (program, args) = if cfg!(windows) {
        let cmd = env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into());
        let args = vec!["/c", command_line];
        (cmd, args)
    } else {
        let sh = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into());
        let args = if ignore_errors {
            vec!["-c", command_line]
        } else {
            vec!["-e", "-c", command_line]
        };
        (sh, args)
    };
    let mut command = Command::new(program);
    command.args(args);
    #[cfg(feature = "full")]
    command.envs(macros.resolve_exports::<&[u8]>(None)?);
    Ok(command.status()?)
}

#[derive(PartialEq, Eq, Clone, Debug)]
pub struct CommandLine {
    execution_line: TokenString,
}

impl CommandLine {
    pub const fn from(line: TokenString) -> Self {
        Self {
            execution_line: line,
        }
    }

    pub fn execute(&self, file: &Makefile, target: &Target) -> eyre::Result<()> {
        let is_recursive = self.execution_line.tokens().any(|x| match x {
            Token::MacroExpansion { name, .. } => name == "MAKE",
            _ => false,
        });
        log::trace!("executing {}", &self.execution_line);
        let execution_line = file.expand_macros(&self.execution_line, Some(target))?;
        #[cfg(feature = "full")]
        {
            let is_just_one_macro_expansion = self.execution_line.tokens().count() == 1
                && self.execution_line.tokens().all(|x| {
                    matches!(x, Token::MacroExpansion { .. } | Token::FunctionCall { .. })
                });
            // unfortunately, if we had a multiline macro somewhere with non-escaped newlines, now we have to run each of them as separate lines
            lazy_static! {
                static ref UNESCAPED_NEWLINE: Regex = #[allow(clippy::unwrap_used)]
                Regex::new(r"([^\\])\n").unwrap();
            }
            if is_just_one_macro_expansion && UNESCAPED_NEWLINE.is_match(&execution_line) {
                let lines = UNESCAPED_NEWLINE
                    .split(&execution_line)
                    .map(|x| Self::from(TokenString::text(x.trim_start())));
                for line in lines {
                    line.execute(file, target)?;
                }
                return Ok(());
            }
        }
        log::trace!("executing {}", &execution_line);
        let mut ignore_errors = false;
        let mut silent = false;
        let mut always_execute = false;

        // sometimes this is defined in macros rather than statically
        let execution_line: String = {
            let mut line_chars = execution_line
                .chars()
                .skip_while(char::is_ascii_whitespace)
                .peekable();
            while let Some(x) = line_chars.next_if(|x| matches!(x, '-' | '@' | '+')) {
                match x {
                    '-' => ignore_errors = true,
                    '@' => silent = true,
                    '+' => always_execute = true,
                    _ => unreachable!(),
                }
            }
            line_chars.collect()
        };

        let ignore_error = ignore_errors
            || file.args.ignore_errors
            || file.special_target_has_prereq(".IGNORE", &target.name);
        let silent = (silent && !file.args.dry_run)
            || file.args.silent
            || file.special_target_has_prereq(".SILENT", &target.name);

        if !silent {
            println!("{}", execution_line);
        }

        let should_execute = always_execute
            || is_recursive
            || !(file.args.dry_run || file.args.question || file.args.touch);
        if !should_execute {
            return Ok(());
        }

        let return_value = execute_command_line(
            &execution_line,
            ignore_error,
            #[cfg(feature = "full")]
            &file.macros,
        );
        let errored = return_value.map_or(true, |status| !status.success());
        if errored {
            // apparently there was an error. do we care?
            if !ignore_error {
                bail!("error from command execution!");
            }
        }

        Ok(())
    }
}

impl fmt::Display for CommandLine {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let execution_line = format!("{}", &self.execution_line);
        let execution_line = execution_line.replace("\n", "\n");
        write!(f, "{}", execution_line)?;
        Ok(())
    }
}