txtpp 0.2.4

A simple-to-use general purpose preprocessor for text files.
Documentation
use clap::{Args, Parser, Subcommand};
use std::env;
use std::process::ExitCode;
use txtpp::{txtpp, Config, Mode, Verbosity, TXTPP_FILE};

/// txtpp CLI
///
/// See https://github.com/Pistonite/txtpp for more info
#[derive(Debug, Parser)]
#[command(author, version, about)]
struct Cli {
    #[command(subcommand)]
    subcommand: Option<Command>,
    #[command(flatten)]
    flags: Flags,
    #[command(flatten)]
    shell: BuildFlags,

    /// Only touch output file if the content need to change
    ///
    /// This will keep the fresh output in memory until the end, and compare it
    /// with the existing output file. If the content is the same, the output
    /// will not be written. This is useful when running txtpp will a watch system like
    /// `cargo watch`.
    ///
    /// Note that this will increase memory usage and may fail if the file cannot fit in memory.
    #[arg(short = 'N', long)]
    needed: bool,
}

impl Cli {
    fn apply_to(&self, config: &mut Config) {
        match &self.subcommand {
            Some(subcommand) => subcommand.apply_to(config),
            None => {
                config.mode = if self.needed {
                    Mode::InMemoryBuild
                } else {
                    Mode::Build
                };
                self.flags.apply_to(config);
                self.shell.apply_to(config);
            }
        }
    }
}

#[derive(Debug, Clone, Subcommand)]
enum Command {
    /// Clean output files
    ///
    /// See https://docs.rs/txtpp/latest/txtpp/enum.Mode.html#variant.Clean for more details
    Clean {
        #[command(flatten)]
        flags: Flags,
    },
    /// Verify that files generated by txtpp are up to date
    ///
    /// See https://docs.rs/txtpp/latest/txtpp/enum.Mode.html#variant.Verify for more details
    Verify {
        #[command(flatten)]
        flags: Flags,
        #[command(flatten)]
        shell: BuildFlags,
    },
}

impl Command {
    fn apply_to(&self, config: &mut Config) {
        match self {
            Command::Clean { flags } => {
                config.mode = Mode::Clean;
                flags.apply_to(config);
            }
            Command::Verify { flags, shell } => {
                config.mode = Mode::Verify;
                flags.apply_to(config);
                shell.apply_to(config);
            }
        }
    }
}

#[derive(Debug, Clone, Args)]
struct Flags {
    /// Show no output.
    ///
    /// Errors will still be printed.
    #[arg(short, long)]
    quiet: bool,

    /// Show more output
    #[arg(short, long, conflicts_with = "quiet")]
    verbose: bool,

    /// If subdirectories should be recursively scanned for input files.
    ///
    /// By default (recursive=false), only directories specified in the input will be scanned.
    /// Newly discovered subdirectories will be ignored.
    #[arg(short, long)]
    recursive: bool,

    /// Specify the number of worker threads
    #[arg(short = 'j', long, default_value = "4")]
    threads: usize,

    /// Input files and/or directories
    ///
    /// Either the `.txtpp` input file or the corresponding output file should be specified.
    /// If a directory is specified, all `.txtpp` files in the directory will be processed.
    /// Subdirectories will not be processed unless `-r/--recursive` is specified.
    ///
    /// The current directory is used if no input is specified.
    #[arg(default_value = ".")]
    inputs: Vec<String>,
}

impl Flags {
    fn apply_to(&self, config: &mut Config) {
        if self.quiet {
            config.verbosity = Verbosity::Quiet;
        } else if self.verbose {
            config.verbosity = Verbosity::Verbose;
        }
        config.recursive = self.recursive;
        config.num_threads = self.threads;
        config.inputs = self.inputs.clone();
    }
}

#[derive(Debug, Clone, Args)]
struct BuildFlags {
    /// The shell command to use
    ///
    /// This should be a command that takes one argument, which is the command to run.
    /// For example `bash -c`. The string will be split on whitespace. The first segment
    /// will be the executable, and the rest will be arguments.
    ///
    /// If a shell is not specified, the platform-specific default shell will be used,
    /// which is `sh -c` on non-Windows. PowerShell is used on Windows with CMD as a fallback.
    /// See https://github.com/Pistonite/txtpp#run-directive for the default PowerShell flags used.
    #[arg(short, long, default_value = "")]
    shell: String,

    /// Don't add a trailing newline to the output.
    ///
    /// This will only affect the output files, not temporary files. Temporary files are
    /// controlled by their arguments. See https://github.com/Pistonite/txtpp#line-endings
    /// for more details.
    #[arg(short, long)]
    no_trailing_newline: bool,
}

impl BuildFlags {
    fn apply_to(&self, config: &mut Config) {
        config.shell_cmd = self.shell.clone();
        config.trailing_newline = !self.no_trailing_newline;
    }
}

fn main() -> ExitCode {
    if let Ok(f) = env::var(TXTPP_FILE) {
        if !f.is_empty() {
            eprintln!("Cannot run txtpp as a subcommand!");
            return ExitCode::FAILURE;
        }
    }

    env_logger::init();
    let args = Cli::parse();

    log::debug!("{:?}", args);
    let mut config = Config::default();
    args.apply_to(&mut config);

    match txtpp(config) {
        Ok(()) => ExitCode::SUCCESS,
        Err(_) => ExitCode::FAILURE,
    }
}