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,6 +323,9 @@ pub fn import(file: &Path) -> Result<()> {
} }
wait_until_ready(&db)?; wait_until_ready(&db)?;
if command_builder::is_dry_run() {
eprintln!("would restore with {tool}");
} else {
match (kind, restore.as_deref()) { match (kind, restore.as_deref()) {
// psql's output is read rather than streamed here, to keep the expected // psql's output is read rather than streamed here, to keep the expected
// role errors out of the way // role errors out of the way
@@ -341,6 +348,7 @@ pub fn import(file: &Path) -> Result<()> {
} }
} }
} }
}
if matches!(dump, Dump::Directory) { if matches!(dump, Dump::Directory) {
let _ = in_container(&db).args("rm -rf").arg(&remote).exec(); let _ = in_container(&db).args("rm -rf").arg(&remote).exec();