refactor: give every command we run a type of its own
This commit is contained in:
150
src/cmd/shell.rs
Normal file
150
src/cmd/shell.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use super::{Argv, Cmd};
|
||||
|
||||
// sh -c, the only way to reach a shell feature inside a container
|
||||
pub struct Sh(String);
|
||||
|
||||
impl Sh {
|
||||
pub fn new(script: impl Into<String>) -> Self {
|
||||
Self(script.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Cmd for Sh {
|
||||
fn argv(&self) -> Argv {
|
||||
// -c has no long form
|
||||
Argv::new("sh").arg("-c").arg(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
// commands joined by pipes, which only a shell can run
|
||||
pub struct Pipeline {
|
||||
stages: Vec<String>,
|
||||
pipefail: bool,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
pub fn starting(first: String) -> Self {
|
||||
Self {
|
||||
stages: vec![first],
|
||||
pipefail: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pipe(mut self, next: &dyn Cmd) -> Self {
|
||||
self.stages.push(next.shell());
|
||||
self
|
||||
}
|
||||
|
||||
// for a pipeline whose last stage closes the pipe on purpose, where the
|
||||
// SIGPIPE that kills an earlier stage is the expected end and not a failure
|
||||
pub fn allow_early_close(mut self) -> Self {
|
||||
self.pipefail = false;
|
||||
self
|
||||
}
|
||||
|
||||
fn script(&self) -> String {
|
||||
let piped = self.stages.join(" | ");
|
||||
|
||||
// a pipeline reports only its last stage's status, so a first stage that
|
||||
// dies mid-stream reads as success without this
|
||||
if self.pipefail {
|
||||
format!("set -o pipefail; {piped}")
|
||||
} else {
|
||||
piped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cmd for Pipeline {
|
||||
fn argv(&self) -> Argv {
|
||||
Sh::new(self.script()).argv()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Gzip;
|
||||
|
||||
impl Cmd for Gzip {
|
||||
fn argv(&self) -> Argv {
|
||||
Argv::new("gzip")
|
||||
}
|
||||
}
|
||||
|
||||
// busybox, which alpine based images ship, has no long options for these three
|
||||
pub struct Gunzip;
|
||||
|
||||
impl Cmd for Gunzip {
|
||||
fn argv(&self) -> Argv {
|
||||
Argv::new("gunzip").arg("-c")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Head {
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
impl Head {
|
||||
pub fn bytes(bytes: usize) -> Self {
|
||||
Self { bytes }
|
||||
}
|
||||
}
|
||||
|
||||
impl Cmd for Head {
|
||||
fn argv(&self) -> Argv {
|
||||
Argv::new("head").arg("-c").arg(self.bytes.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rm {
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl Rm {
|
||||
pub fn recursive(path: &str) -> Self {
|
||||
Self {
|
||||
path: path.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cmd for Rm {
|
||||
fn argv(&self) -> Argv {
|
||||
Argv::new("rm").arg("-rf").arg(&self.path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Gunzip, Gzip, Head};
|
||||
use crate::cmd::Cmd;
|
||||
|
||||
#[test]
|
||||
fn a_pipeline_reports_a_stage_that_dies_mid_stream() {
|
||||
let script = Gunzip.pipe(&Gzip).shell();
|
||||
|
||||
assert_eq!(script, "sh -c 'set -o pipefail; gunzip -c | gzip'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pipeline_that_closes_the_pipe_on_purpose_keeps_the_default() {
|
||||
let script = Gunzip.pipe(&Head::bytes(512)).allow_early_close().shell();
|
||||
|
||||
assert_eq!(script, "sh -c 'gunzip -c | head -c 512'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pipeline_reaches_a_container_as_one_argument() {
|
||||
let argv = Gunzip.pipe(&Gzip).in_container("abc123").argv();
|
||||
|
||||
assert_eq!(
|
||||
argv.words(),
|
||||
[
|
||||
"docker",
|
||||
"exec",
|
||||
"abc123",
|
||||
"sh",
|
||||
"-c",
|
||||
"set -o pipefail; gunzip -c | gzip"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user