use clap::{CommandFactory, Parser, Subcommand}; use eyre::{format_err, Result}; use log::{debug, error}; use tokio::fs; #[derive(Parser)] #[command()] struct Cli { #[command(subcommand)] command: Command, } #[derive(Subcommand)] enum Command { ApplyConfig { /// config file to use #[arg(default_value = "config.yaml")] config: String, /// glob filters to select files to apply #[arg(short = 'F', long)] filters: Vec, /// path prefix (aka chroot) #[arg(short = 'P', long, default_value = "/")] prefix: String, }, Logger { #[arg(long, short = 'p', default_value = "/var/log")] log_path: String, #[arg(long, short = 'n')] log_name: Option, #[arg(long)] with_prefix: bool, command: String, args: Vec, }, Log { #[arg(long, short = 'p', default_value = "/var/log")] log_path: String, log_name: String, since: Option, until: Option, }, } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { clap_complete::CompleteEnv::with_factory(Cli::command).complete(); let cli = Cli::parse(); env_logger::builder() .parse_filters("info") .parse_default_env() .init(); use Command as C; match cli.command { C::ApplyConfig { config, filters, prefix, } => { let filters = parse_globs(&filters)?; apply_config(&config, &filters, &prefix).await } C::Logger { ref log_path, ref log_name, with_prefix, command, args, } => { let command = command.as_str(); let log_name = log_name.as_deref().unwrap_or_else(|| basename(command)); dkl::logger::Logger { log_path, log_name, with_prefix, } .run(command, &args) .await } C::Log { log_path, log_name, since, until, } => { let since = parse_ts_arg(since)?; let until = parse_ts_arg(until)?; let mut files = dkl::logger::log_files(&log_path, &log_name).await?; files.sort(); let mut out = tokio::io::stdout(); for f in files { if !since.is_none_or(|since| f.timestamp >= since) { continue; } if !until.is_none_or(|until| f.timestamp <= until) { continue; } debug!("{f:?}"); f.copy_to(&mut out).await?; } Ok(()) } } } fn parse_ts_arg(ts: Option) -> Result> { match ts { None => Ok(None), Some(ts) => { let ts = dkl::logger::parse_ts(&ts) .map_err(|e| format_err!("invalid timestamp: {ts}: {e}"))?; Ok(Some(ts)) } } } fn basename(path: &str) -> &str { path.rsplit_once('/').map_or(path, |split| split.1) } async fn apply_config(config_file: &str, filters: &[glob::Pattern], chroot: &str) -> Result<()> { let config = fs::read_to_string(config_file).await?; let config: dkl::Config = serde_yaml::from_str(&config)?; let files = if filters.is_empty() { config.files } else { (config.files.into_iter()) .filter(|f| filters.iter().any(|filter| filter.matches(&f.path))) .collect() }; dkl::apply::files(&files, chroot).await } fn parse_globs(filters: &[String]) -> Result> { let mut errors = false; let filters = (filters.iter()) .filter_map(|s| { glob::Pattern::new(s) .inspect_err(|e| { error!("invalid filter: {s:?}: {e}"); errors = true; }) .ok() }) .collect(); if errors { return Err(format_err!("invalid filters")); } Ok(filters) }