Skip to main content

docker_tools/
cli.rs

1use clap::error::ErrorKind;
2use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
3use nils_common::cli_contract::exit;
4use std::ffi::OsString;
5
6use crate::{completion, runtime};
7
8const ROOT_AFTER_HELP: &str = "\
9EXAMPLES:
10  docker-tools container sh my-container
11  docker-tools container rm --no-force old-container
12  docker-tools compose down --all --yes
13  docker-tools run zsh ubuntu:latest
14  docker-tools completion zsh
15
16ENVIRONMENT:
17  ZSH_DOCKER_COMPOSE_CMD  Override compose command, for example 'docker compose' or docker-compose.
18
19EXIT CODES:
20  0    success
21  1    runtime error or cancelled interactive action
22  2    zsh-kit-compatible usage guard for migrated helper commands
23  64   command-line usage error
24  127  required Docker executable is unavailable";
25
26#[derive(Debug, Parser)]
27#[command(
28    name = "docker-tools",
29    version,
30    long_version = nils_build_info::long_version(env!("CARGO_PKG_VERSION")),
31    about = "Docker helper CLI",
32    long_about = "Run Docker helpers migrated out of zsh-kit. Shell alias mutation remains owned by zsh-kit.",
33    after_help = ROOT_AFTER_HELP
34)]
35pub struct Cli {
36    #[command(subcommand)]
37    pub command: Option<Command>,
38}
39
40#[derive(Debug, Subcommand)]
41pub enum Command {
42    /// Container helper commands
43    Container(ContainerArgs),
44    /// Docker Compose helper commands
45    Compose(ComposeArgs),
46    /// Interactive docker run helpers
47    Run(RunArgs),
48    /// Export shell completion script
49    Completion(CompletionArgs),
50}
51
52#[derive(Debug, Args)]
53pub struct ContainerArgs {
54    #[command(subcommand)]
55    pub command: Option<ContainerCommand>,
56}
57
58#[derive(Debug, Subcommand)]
59pub enum ContainerCommand {
60    /// Exec into a container with zsh -> bash -> sh fallback
61    Sh(ContainerShellArgs),
62    /// Alias for container sh
63    Zsh(ContainerShellArgs),
64    /// Remove one or more containers
65    Rm(ContainerRmArgs),
66}
67
68#[derive(Debug, Args)]
69pub struct ContainerShellArgs {
70    /// User inside the container
71    #[arg(
72        short = 'u',
73        long = "user",
74        value_name = "user",
75        conflicts_with = "root"
76    )]
77    pub user: Option<String>,
78    /// Exec as root
79    #[arg(short = 'r', long = "root")]
80    pub root: bool,
81    /// Container name or id
82    #[arg(value_name = "container")]
83    pub container: String,
84}
85
86#[derive(Debug, Args)]
87pub struct ContainerRmArgs {
88    /// Do not force remove containers
89    #[arg(long = "no-force")]
90    pub no_force: bool,
91    /// Remove anonymous volumes
92    #[arg(short = 'v', long = "volumes")]
93    pub volumes: bool,
94    /// Container names or ids
95    #[arg(value_name = "container", required = true, num_args = 1..)]
96    pub containers: Vec<String>,
97}
98
99#[derive(Debug, Args)]
100pub struct ComposeArgs {
101    #[command(subcommand)]
102    pub command: Option<ComposeCommand>,
103}
104
105#[derive(Debug, Subcommand)]
106pub enum ComposeCommand {
107    /// Run docker compose down
108    Down(ComposeDownArgs),
109}
110
111#[derive(Debug, Args)]
112pub struct ComposeDownArgs {
113    /// Add --remove-orphans --volumes --rmi all
114    #[arg(short = 'a', long = "all")]
115    pub all: bool,
116    /// Skip confirmation prompts for --all
117    #[arg(short = 'y', long = "yes")]
118    pub yes: bool,
119    /// Extra arguments passed after docker compose down
120    #[arg(value_name = "compose-arg", num_args = 0.., trailing_var_arg = true, allow_hyphen_values = true)]
121    pub args: Vec<String>,
122}
123
124#[derive(Debug, Args)]
125pub struct RunArgs {
126    #[command(subcommand)]
127    pub command: Option<RunCommand>,
128}
129
130#[derive(Debug, Subcommand)]
131pub enum RunCommand {
132    /// Run an image with zsh -> bash -> sh fallback
133    Zsh(RunZshArgs),
134}
135
136#[derive(Debug, Args)]
137pub struct RunZshArgs {
138    /// Do not mount the current working directory to /work
139    #[arg(long = "no-mount")]
140    pub no_mount: bool,
141    /// Container workdir
142    #[arg(short = 'w', long = "workdir", value_name = "path")]
143    pub workdir: Option<String>,
144    /// Container name
145    #[arg(short = 'n', long = "name", value_name = "name")]
146    pub name: Option<String>,
147    /// User inside the container
148    #[arg(
149        short = 'u',
150        long = "user",
151        value_name = "user",
152        conflicts_with = "root"
153    )]
154    pub user: Option<String>,
155    /// Run as root
156    #[arg(short = 'r', long = "root")]
157    pub root: bool,
158    /// Image to run
159    #[arg(value_name = "image")]
160    pub image: String,
161}
162
163#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
164pub enum CompletionShell {
165    Bash,
166    Zsh,
167}
168
169#[derive(Debug, Args)]
170pub struct CompletionArgs {
171    /// Shell to generate completion script for
172    #[arg(value_enum, value_name = "shell")]
173    pub shell: CompletionShell,
174}
175
176pub fn run() -> i32 {
177    run_from(std::env::args_os())
178}
179
180pub fn run_from<I, T>(args: I) -> i32
181where
182    I: IntoIterator<Item = T>,
183    T: Into<OsString> + Clone,
184{
185    let cli = match Cli::try_parse_from(args) {
186        Ok(cli) => cli,
187        Err(err) => return print_parse_error(err),
188    };
189
190    match cli.command {
191        None => print_help_stdout(),
192        Some(Command::Container(args)) => match args.command {
193            None => print_subcommand_help("container"),
194            Some(ContainerCommand::Sh(args) | ContainerCommand::Zsh(args)) => {
195                runtime::container_shell(args)
196            }
197            Some(ContainerCommand::Rm(args)) => runtime::container_rm(args),
198        },
199        Some(Command::Compose(args)) => match args.command {
200            None => print_subcommand_help("compose"),
201            Some(ComposeCommand::Down(args)) => runtime::compose_down(args),
202        },
203        Some(Command::Run(args)) => match args.command {
204            None => print_subcommand_help("run"),
205            Some(RunCommand::Zsh(args)) => runtime::run_zsh(args),
206        },
207        Some(Command::Completion(args)) => completion::run(args.shell),
208    }
209}
210
211fn print_parse_error(err: clap::Error) -> i32 {
212    let kind = err.kind();
213    if let Err(print_err) = err.print() {
214        eprintln!("{print_err}");
215        return exit::RUNTIME;
216    }
217
218    if matches!(kind, ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
219        exit::SUCCESS
220    } else {
221        exit::USAGE
222    }
223}
224
225fn print_help_stdout() -> i32 {
226    let mut command = Cli::command();
227    if let Err(err) = command.print_help() {
228        eprintln!("{err}");
229        return exit::RUNTIME;
230    }
231    println!();
232    exit::SUCCESS
233}
234
235fn print_subcommand_help(name: &str) -> i32 {
236    let mut command = Cli::command();
237    let Some(subcommand) = command.find_subcommand_mut(name) else {
238        return exit::SOFTWARE;
239    };
240    if let Err(err) = subcommand.print_help() {
241        eprintln!("{err}");
242        return exit::RUNTIME;
243    }
244    println!();
245    exit::SUCCESS
246}