From 1bb205bb79e927bc845503ced8149363d7752fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Mon, 7 Sep 2026 13:49:24 +0000 Subject: [PATCH] feat: build commands from arguments instead of strings --- src/command_builder.rs | 10 +--- src/scripts/django.rs | 9 ++- src/scripts/docker_compose.rs | 14 +++-- src/scripts/postgres.rs | 105 +++++++++++++++++++--------------- 4 files changed, 75 insertions(+), 63 deletions(-) diff --git a/src/command_builder.rs b/src/command_builder.rs index 8e8832c..dddfa4e 100644 --- a/src/command_builder.rs +++ b/src/command_builder.rs @@ -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) -> Self { - self.args.push(arg.into()); + pub fn arg(mut self, arg: impl AsRef) -> Self { + self.args.push(arg.as_ref().to_string()); self } diff --git a/src/scripts/django.rs b/src/scripts/django.rs index 47f339f..c302043 100644 --- a/src/scripts/django.rs +++ b/src/scripts/django.rs @@ -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 diff --git a/src/scripts/docker_compose.rs b/src/scripts/docker_compose.rs index d2212a6..763f53b 100644 --- a/src/scripts/docker_compose.rs +++ b/src/scripts/docker_compose.rs @@ -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<()> { diff --git a/src/scripts/postgres.rs b/src/scripts/postgres.rs index b0fcbcd..6c66e73 100644 --- a/src/scripts/postgres.rs +++ b/src/scripts/postgres.rs @@ -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 Result 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 }