cargo-equip 0.13.0

A Cargo subcommand to bundle your code into one `.rs` file for competitive programming.
Documentation
use anyhow::{anyhow, bail, Context as _};
use itertools::Itertools as _;
use std::{
    env,
    ffi::{OsStr, OsString},
    fmt,
    path::{Path, PathBuf},
    process::{Output, Stdio},
    str,
};

use crate::shell::Shell;

pub(crate) fn process(program: impl AsRef<OsStr>) -> ProcessBuilder<NotPresent> {
    ProcessBuilder {
        program: program.as_ref().to_owned(),
        args: vec![],
        cwd: (),
    }
}

pub(crate) fn cargo_exe() -> anyhow::Result<PathBuf> {
    env::var_os("CARGO")
        .with_context(|| {
            "missing `$CARGO`. run this program with `cargo equip`, not `cargo-equip equip`"
        })
        .map(Into::into)
}

#[derive(Debug)]
pub(crate) struct ProcessBuilder<C: Presence<PathBuf>> {
    program: OsString,
    args: Vec<OsString>,
    cwd: C::Value,
}

impl<C: Presence<PathBuf>> ProcessBuilder<C> {
    pub(crate) fn arg(mut self, arg: impl AsRef<OsStr>) -> Self {
        self.args.push(arg.as_ref().to_owned());
        self
    }

    pub(crate) fn args(mut self, args: &[impl AsRef<OsStr>]) -> Self {
        self.args.extend(args.iter().map(|s| s.as_ref().to_owned()));
        self
    }

    pub(crate) fn cwd(self, cwd: impl AsRef<Path>) -> ProcessBuilder<Present> {
        ProcessBuilder {
            program: self.program,
            args: self.args,
            cwd: cwd.as_ref().to_owned(),
        }
    }
}

impl ProcessBuilder<Present> {
    fn output(&self, check: bool, stdout: Stdio, stderr: Stdio) -> anyhow::Result<Output> {
        let output = std::process::Command::new(&self.program)
            .args(&self.args)
            .current_dir(&self.cwd)
            .stdout(stdout)
            .stderr(stderr)
            .output()?;
        if check && !output.status.success() {
            bail!("{} didn't exit successfully: {}", self, output.status);
        }
        Ok(output)
    }

    pub(crate) fn exec(&self) -> anyhow::Result<()> {
        self.output(true, Stdio::inherit(), Stdio::inherit())?;
        Ok(())
    }

    pub(crate) fn read(&self, check: bool) -> anyhow::Result<String> {
        let Output { stdout, .. } = self.output(check, Stdio::piped(), Stdio::inherit())?;
        let stdout =
            str::from_utf8(&stdout).map_err(|_| anyhow!("stream did not contain valid UTF-8"))?;
        Ok(stdout.trim_end().to_owned())
    }

    pub(crate) fn read_with_status(
        &self,
        check: bool,
        shell: &mut Shell,
    ) -> anyhow::Result<String> {
        shell.status("Running", self)?;
        self.read(check)
    }

    pub(crate) fn read_bytes_with_status(&self, shell: &mut Shell) -> anyhow::Result<Vec<u8>> {
        shell.status("Running", self)?;
        let Output { stdout, .. } = self.output(true, Stdio::piped(), Stdio::inherit())?;
        Ok(stdout)
    }
}

impl fmt::Display for ProcessBuilder<Present> {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            fmt,
            "`{}{}`",
            shell_escape::escape(self.program.to_string_lossy()),
            self.args.iter().format_with("", |arg, f| f(&format_args!(
                " {}",
                shell_escape::escape(arg.to_string_lossy()),
            ))),
        )
    }
}

pub(crate) trait Presence<T> {
    type Value;
}

#[derive(Debug)]
pub(crate) enum NotPresent {}

impl<T> Presence<T> for NotPresent {
    type Value = ();
}

#[derive(Debug)]
pub(crate) enum Present {}

impl<T> Presence<T> for Present {
    type Value = T;
}