diff --git a/README.md b/README.md index 51d31af..826fc5a 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,14 @@ You will need rust installed. Clone repo and run: cargo install --path . ``` -To print the underlying docker commands as they run, build with debug -assertions: +## seeing what it runs + +`-v` prints each docker command as it runs, and `--dry-run` prints the ones it +would run without running them. Both work before or after the subcommand. + ```bash -cargo install --path . --debug +ahab -v django test +ahab --dry-run postgres import ./dump ``` ## shell completion diff --git a/src/cli/ahab.rs b/src/cli/ahab.rs index 9596252..9d62d14 100644 --- a/src/cli/ahab.rs +++ b/src/cli/ahab.rs @@ -8,8 +8,15 @@ use clap_complete::Shell; pub struct Ahab { #[command(subcommand)] pub command: Commands, -} + /// Print each docker command as it runs + #[arg(short, long, global = true)] + pub verbose: bool, + + /// Print the docker commands that would run, without running them + #[arg(long, global = true)] + pub dry_run: bool, +} #[derive(Debug, Subcommand)] pub enum Commands { /// Django related subcommands diff --git a/src/command_builder.rs b/src/command_builder.rs index dddfa4e..3eaf0c9 100644 --- a/src/command_builder.rs +++ b/src/command_builder.rs @@ -2,10 +2,9 @@ use anyhow::{Context, Result, bail}; use std::{ fmt::Display, process::{Child, Command, ExitStatus, Stdio}, + sync::OnceLock, }; -use crate::debug_eprintln; - pub struct Args(Vec); impl From<&str> for Args { @@ -54,8 +53,9 @@ impl CommandBuilder { } pub fn build(self) -> Result { - debug_eprintln!("running `{self}`"); - + if options().verbose { + eprintln!("running `{self}`"); + } let (first, rest) = self.args.split_first().context("empty args")?; let mut command = Command::new(first); command.args(rest); @@ -72,6 +72,11 @@ impl CommandBuilder { } pub fn exec(self) -> Result<()> { + if options().dry_run { + eprintln!("would run `{self}`"); + return Ok(()); + } + let shown = self.to_string(); let status = self.build()?.spawn()?.wait()?; @@ -83,6 +88,11 @@ impl CommandBuilder { } pub fn exec_redirect_stdout(self, stdio: Stdio) -> Result<()> { + if options().dry_run { + eprintln!("would run `{self}`"); + return Ok(()); + } + let shown = self.to_string(); let status = self.build()?.stdout(stdio).spawn()?.wait()?; @@ -100,3 +110,23 @@ fn check(command: &str, status: ExitStatus) -> Result<()> { None => bail!("`{command}` was killed by a signal"), } } + +#[derive(Default, Clone, Copy)] +pub struct Options { + pub verbose: bool, + pub dry_run: bool, +} + +static OPTIONS: OnceLock = OnceLock::new(); + +pub fn set_options(options: Options) { + let _ = OPTIONS.set(options); +} + +fn options() -> Options { + OPTIONS.get().copied().unwrap_or_default() +} + +pub fn is_dry_run() -> bool { + options().dry_run +} diff --git a/src/main.rs b/src/main.rs index f70e2e4..db7d96f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use ahab::{cli, scripts}; +use ahab::{cli, command_builder, scripts}; use anyhow::Result; use clap::Parser; @@ -6,6 +6,11 @@ use clap::Parser; fn main() -> Result<()> { let args = cli::Ahab::parse(); + command_builder::set_options(command_builder::Options { + verbose: args.verbose, + dry_run: args.dry_run, + }); + match args.command { cli::Commands::Django { command } => match command { cli::Django::Bash => scripts::django::bash(), diff --git a/src/scripts/postgres.rs b/src/scripts/postgres.rs index 8d94631..4a0dda2 100644 --- a/src/scripts/postgres.rs +++ b/src/scripts/postgres.rs @@ -10,7 +10,8 @@ use std::{ use super::docker_compose; use crate::cli::Format; -use crate::{command_builder::CommandBuilder, compose::Compose, debug_eprintln}; +use crate::command_builder; +use crate::{command_builder::CommandBuilder, compose::Compose}; const CUSTOM_MAGIC: &[u8] = b"PGDMP"; const TAR_MAGIC: &[u8] = b"toc.dat"; @@ -193,7 +194,10 @@ fn piped(db: &Database, script: &str, input: &Path) -> Result Result<()> { - debug_eprintln!("waiting until pg_isready"); + if command_builder::is_dry_run() { + return Ok(()); + } + let deadline = Instant::now() + READY_TIMEOUT; loop { @@ -319,25 +323,29 @@ pub fn import(file: &Path) -> Result<()> { } wait_until_ready(&db)?; - match (kind, restore.as_deref()) { - // psql's output is read rather than streamed here, to keep the expected - // role errors out of the way - (Kind::Cluster, Some(script)) => restore_cluster(&db, script, file)?, - (_, restore) => { - let status = match restore { - Some(script) => piped(&db, script, file)?.spawn()?.wait()?, - None => in_container(&db) - .args("pg_restore -U") - .arg(&db.user) - .arg(format!("--dbname={}", db.name)) - .arg(&remote) - .build()? - .spawn()? - .wait()?, - }; + if command_builder::is_dry_run() { + eprintln!("would restore with {tool}"); + } else { + match (kind, restore.as_deref()) { + // psql's output is read rather than streamed here, to keep the expected + // role errors out of the way + (Kind::Cluster, Some(script)) => restore_cluster(&db, script, file)?, + (_, restore) => { + let status = match restore { + Some(script) => piped(&db, script, file)?.spawn()?.wait()?, + None => in_container(&db) + .args("pg_restore -U") + .arg(&db.user) + .arg(format!("--dbname={}", db.name)) + .arg(&remote) + .build()? + .spawn()? + .wait()?, + }; - if !status.success() { - bail!("{tool} failed, the database is left empty"); + if !status.success() { + bail!("{tool} failed, the database is left empty"); + } } } }