feat: add verbose and dry run

This commit is contained in:
2026-09-07 14:12:53 +00:00
parent ab93e7ef4f
commit b2a0c08256
5 changed files with 83 additions and 29 deletions

View File

@@ -10,10 +10,14 @@ You will need rust installed. Clone repo and run:
cargo install --path . cargo install --path .
``` ```
To print the underlying docker commands as they run, build with debug ## seeing what it runs
assertions:
`-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 ```bash
cargo install --path . --debug ahab -v django test
ahab --dry-run postgres import ./dump
``` ```
## shell completion ## shell completion

View File

@@ -8,8 +8,15 @@ use clap_complete::Shell;
pub struct Ahab { pub struct Ahab {
#[command(subcommand)] #[command(subcommand)]
pub command: Commands, 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)] #[derive(Debug, Subcommand)]
pub enum Commands { pub enum Commands {
/// Django related subcommands /// Django related subcommands

View File

@@ -2,10 +2,9 @@ use anyhow::{Context, Result, bail};
use std::{ use std::{
fmt::Display, fmt::Display,
process::{Child, Command, ExitStatus, Stdio}, process::{Child, Command, ExitStatus, Stdio},
sync::OnceLock,
}; };
use crate::debug_eprintln;
pub struct Args(Vec<String>); pub struct Args(Vec<String>);
impl From<&str> for Args { impl From<&str> for Args {
@@ -54,8 +53,9 @@ impl CommandBuilder {
} }
pub fn build(self) -> Result<Command> { pub fn build(self) -> Result<Command> {
debug_eprintln!("running `{self}`"); if options().verbose {
eprintln!("running `{self}`");
}
let (first, rest) = self.args.split_first().context("empty args")?; let (first, rest) = self.args.split_first().context("empty args")?;
let mut command = Command::new(first); let mut command = Command::new(first);
command.args(rest); command.args(rest);
@@ -72,6 +72,11 @@ impl CommandBuilder {
} }
pub fn exec(self) -> Result<()> { pub fn exec(self) -> Result<()> {
if options().dry_run {
eprintln!("would run `{self}`");
return Ok(());
}
let shown = self.to_string(); let shown = self.to_string();
let status = self.build()?.spawn()?.wait()?; let status = self.build()?.spawn()?.wait()?;
@@ -83,6 +88,11 @@ impl CommandBuilder {
} }
pub fn exec_redirect_stdout(self, stdio: Stdio) -> Result<()> { 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 shown = self.to_string();
let status = self.build()?.stdout(stdio).spawn()?.wait()?; 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"), 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<Options> = 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
}

View File

@@ -1,4 +1,4 @@
use ahab::{cli, scripts}; use ahab::{cli, command_builder, scripts};
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
@@ -6,6 +6,11 @@ use clap::Parser;
fn main() -> Result<()> { fn main() -> Result<()> {
let args = cli::Ahab::parse(); let args = cli::Ahab::parse();
command_builder::set_options(command_builder::Options {
verbose: args.verbose,
dry_run: args.dry_run,
});
match args.command { match args.command {
cli::Commands::Django { command } => match command { cli::Commands::Django { command } => match command {
cli::Django::Bash => scripts::django::bash(), cli::Django::Bash => scripts::django::bash(),

View File

@@ -10,7 +10,8 @@ use std::{
use super::docker_compose; use super::docker_compose;
use crate::cli::Format; 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 CUSTOM_MAGIC: &[u8] = b"PGDMP";
const TAR_MAGIC: &[u8] = b"toc.dat"; const TAR_MAGIC: &[u8] = b"toc.dat";
@@ -193,7 +194,10 @@ fn piped(db: &Database, script: &str, input: &Path) -> Result<std::process::Comm
} }
fn wait_until_ready(db: &Database) -> Result<()> { fn wait_until_ready(db: &Database) -> Result<()> {
debug_eprintln!("waiting until pg_isready"); if command_builder::is_dry_run() {
return Ok(());
}
let deadline = Instant::now() + READY_TIMEOUT; let deadline = Instant::now() + READY_TIMEOUT;
loop { loop {
@@ -319,25 +323,29 @@ pub fn import(file: &Path) -> Result<()> {
} }
wait_until_ready(&db)?; wait_until_ready(&db)?;
match (kind, restore.as_deref()) { if command_builder::is_dry_run() {
// psql's output is read rather than streamed here, to keep the expected eprintln!("would restore with {tool}");
// role errors out of the way } else {
(Kind::Cluster, Some(script)) => restore_cluster(&db, script, file)?, match (kind, restore.as_deref()) {
(_, restore) => { // psql's output is read rather than streamed here, to keep the expected
let status = match restore { // role errors out of the way
Some(script) => piped(&db, script, file)?.spawn()?.wait()?, (Kind::Cluster, Some(script)) => restore_cluster(&db, script, file)?,
None => in_container(&db) (_, restore) => {
.args("pg_restore -U") let status = match restore {
.arg(&db.user) Some(script) => piped(&db, script, file)?.spawn()?.wait()?,
.arg(format!("--dbname={}", db.name)) None => in_container(&db)
.arg(&remote) .args("pg_restore -U")
.build()? .arg(&db.user)
.spawn()? .arg(format!("--dbname={}", db.name))
.wait()?, .arg(&remote)
}; .build()?
.spawn()?
.wait()?,
};
if !status.success() { if !status.success() {
bail!("{tool} failed, the database is left empty"); bail!("{tool} failed, the database is left empty");
}
} }
} }
} }