feat: build commands from arguments instead of strings

This commit is contained in:
2026-09-07 13:49:24 +00:00
parent 25f7b7df9d
commit 1bb205bb79
4 changed files with 75 additions and 63 deletions

View File

@@ -14,12 +14,6 @@ impl From<&str> for Args {
}
}
impl From<&String> for Args {
fn from(value: &String) -> Self {
Self(Vec::from_iter(value.split_whitespace().map(String::from)))
}
}
impl From<&[String]> for Args {
fn from(value: &[String]) -> Self {
Self(value.to_vec())
@@ -46,8 +40,8 @@ impl CommandBuilder {
Self::default().args("docker compose")
}
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
pub fn arg(mut self, arg: impl AsRef<str>) -> Self {
self.args.push(arg.as_ref().to_string());
self
}

View File

@@ -4,7 +4,6 @@ use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use crate::command_builder::CommandBuilder;
use crate::compose::Compose;
use crate::scripts::docker_compose;
use crate::{create_file, safe_create_file};
@@ -65,10 +64,10 @@ pub fn run(rest: &[String]) -> Result<()> {
}
pub fn manage(rest: &[String]) -> Result<()> {
let container = Compose::resolve()?.django()?;
let joined = rest.join(" ");
let command = format!("run --rm {container} python manage.py {joined}");
CommandBuilder::docker_compose().args(&command).exec()
let mut args = vec!["python".to_string(), "manage.py".to_string()];
args.extend_from_slice(rest);
run(&args)
}
// shortcuts

View File

@@ -13,7 +13,7 @@ pub fn down() -> Result<()> {
pub fn run(service: &str, rest: &[String]) -> Result<()> {
CommandBuilder::docker_compose()
.args("run --rm")
.args(service)
.arg(service)
.args(rest)
.exec()
}
@@ -21,7 +21,7 @@ pub fn run(service: &str, rest: &[String]) -> Result<()> {
pub fn exec(service: &str, rest: &[String]) -> Result<()> {
CommandBuilder::docker_compose()
.args("exec")
.args(service)
.arg(service)
.args(rest)
.exec()
}
@@ -30,9 +30,13 @@ pub fn ps() -> Result<()> {
CommandBuilder::docker_compose().args("ps").exec()
}
pub fn start(containers: Option<&str>) -> Result<()> {
let args = format!("start {}", containers.unwrap_or(""));
CommandBuilder::docker_compose().args(&args).exec()
pub fn start(service: Option<&str>) -> Result<()> {
let mut command = CommandBuilder::docker_compose().args("start");
if let Some(service) = service {
command = command.arg(service);
}
command.exec()
}
pub fn stop() -> Result<()> {

View File

@@ -40,7 +40,7 @@ impl Database {
let container = CommandBuilder::docker_compose()
.args("ps -q")
.args(&service)
.arg(&service)
.exec_get_stdout()?
.trim()
.to_string();
@@ -173,7 +173,7 @@ fn piped(db: &Database, script: &str, input: &Path) -> Result<std::process::Comm
let file = File::open(input).with_context(|| format!("opening {}", input.display()))?;
let mut command = CommandBuilder::docker()
.args("exec -i")
.args(&db.container)
.arg(&db.container)
.args("sh -c")
.arg(script)
.build()?;
@@ -185,10 +185,12 @@ fn piped(db: &Database, script: &str, input: &Path) -> Result<std::process::Comm
fn wait_until_ready(db: &Database) -> Result<()> {
debug_eprintln!("waiting until pg_isready");
while !CommandBuilder::docker()
.args(&format!(
"exec {} pg_isready -U {} -d {}",
db.container, db.user, db.name
))
.args("exec")
.arg(&db.container)
.args("pg_isready -U")
.arg(&db.user)
.args("-d")
.arg(&db.name)
.build()?
.stdout(Stdio::null())
.spawn()?
@@ -201,9 +203,13 @@ fn wait_until_ready(db: &Database) -> Result<()> {
Ok(())
}
fn run_when_ready(db: &Database, command: &str) -> Result<()> {
fn when_ready(db: &Database, command: CommandBuilder) -> Result<()> {
wait_until_ready(db)?;
CommandBuilder::docker().args(command).exec()
command.exec()
}
fn in_container(db: &Database) -> CommandBuilder {
CommandBuilder::docker().args("exec").arg(&db.container)
}
pub fn import(file: &Path) -> Result<()> {
@@ -220,9 +226,12 @@ pub fn import(file: &Path) -> Result<()> {
// a directory cannot be streamed, so it is the one shape that gets copied in
if matches!(dump, Dump::Directory) {
run_when_ready(
when_ready(
&db,
&format!("cp -L {} {}:{remote}", file.display(), db.container),
CommandBuilder::docker()
.args("cp -L")
.arg(file.to_string_lossy())
.arg(format!("{}:{remote}", db.container)),
)?;
}
@@ -264,20 +273,25 @@ pub fn import(file: &Path) -> Result<()> {
let tool = kind.tool();
eprintln!("restoring database with {tool}");
run_when_ready(
when_ready(
&db,
&format!("exec {} dropdb -U {} {}", db.container, db.user, db.name),
in_container(&db)
.args("dropdb -U")
.arg(&db.user)
.arg(&db.name),
)?;
// a cluster dump creates the database itself, and would trip over one that
// is already there
if kind != Kind::Cluster {
run_when_ready(
when_ready(
&db,
&format!(
"exec {} createdb -U {} -E utf8 -T template0 {}",
db.container, db.user, db.name
),
in_container(&db)
.args("createdb -U")
.arg(&db.user)
.args("-E utf8 -T template0")
.arg(&db.name),
)?;
}
@@ -289,11 +303,11 @@ pub fn import(file: &Path) -> Result<()> {
(_, restore) => {
let status = match restore {
Some(script) => piped(&db, script, file)?.spawn()?.wait()?,
None => CommandBuilder::docker()
.args(&format!(
"exec {} pg_restore -U {} --dbname={} {remote}",
db.container, db.user, db.name
))
None => in_container(&db)
.args("pg_restore -U")
.arg(&db.user)
.arg(format!("--dbname={}", db.name))
.arg(&remote)
.build()?
.spawn()?
.wait()?,
@@ -306,9 +320,7 @@ pub fn import(file: &Path) -> Result<()> {
}
if matches!(dump, Dump::Directory) {
let _ = CommandBuilder::docker()
.args(&format!("exec {} rm -rf {remote}", db.container))
.exec();
let _ = in_container(&db).args("rm -rf").arg(&remote).exec();
}
eprintln!("restarting containers");
@@ -332,19 +344,25 @@ pub fn dump(file: &PathBuf, format: Format, gzip: bool) -> Result<()> {
eprintln!("dumping to local file {}", file.to_string_lossy());
let stdout = Stdio::from(File::create(file)?);
let dumping = dump_command(&db, format);
if gzip {
// the whole pipeline has to arrive as one shell argument
CommandBuilder::docker()
.args("exec")
.args(&db.container)
.arg(&db.container)
.args("sh -c")
.arg(format!("{dumping} | gzip"))
.arg(format!("{} | gzip", dump_command(&db, format)))
.exec_redirect_stdout(stdout)?;
} else {
CommandBuilder::docker()
.args(&format!("exec {} {dumping}", db.container))
.exec_redirect_stdout(stdout)?;
let dumping = match format.flag() {
Some(flag) => in_container(&db)
.args("pg_dump -U")
.arg(&db.user)
.arg(format!("--format={flag}"))
.arg(&db.name),
None => in_container(&db).args("pg_dumpall -U").arg(&db.user),
};
dumping.exec_redirect_stdout(stdout)?;
}
Ok(())
@@ -371,24 +389,21 @@ fn dump_directory(db: &Database, target: &Path) -> Result<()> {
eprintln!("dumping to local directory {}", target.display());
let remote = remote_dump();
CommandBuilder::docker()
.args(&format!(
"exec {} pg_dump -U {} --format=d -f {remote} {}",
db.container, db.user, db.name
))
in_container(db)
.args("pg_dump -U")
.arg(&db.user)
.args("--format=d -f")
.arg(&remote)
.arg(&db.name)
.exec()?;
let copied = CommandBuilder::docker()
.args(&format!(
"cp {}:{remote} {}",
db.container,
target.display()
))
.args("cp")
.arg(format!("{}:{remote}", db.container))
.arg(target.to_string_lossy())
.exec();
let _ = CommandBuilder::docker()
.args(&format!("exec {} rm -rf {remote}", db.container))
.exec();
let _ = in_container(db).args("rm -rf").arg(&remote).exec();
copied
}