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 { impl From<&[String]> for Args {
fn from(value: &[String]) -> Self { fn from(value: &[String]) -> Self {
Self(value.to_vec()) Self(value.to_vec())
@@ -46,8 +40,8 @@ impl CommandBuilder {
Self::default().args("docker compose") Self::default().args("docker compose")
} }
pub fn arg(mut self, arg: impl Into<String>) -> Self { pub fn arg(mut self, arg: impl AsRef<str>) -> Self {
self.args.push(arg.into()); self.args.push(arg.as_ref().to_string());
self self
} }

View File

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

View File

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

View File

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