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(ContainerArgs),
44 Compose(ComposeArgs),
46 Run(RunArgs),
48 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 Sh(ContainerShellArgs),
62 Zsh(ContainerShellArgs),
64 Rm(ContainerRmArgs),
66}
67
68#[derive(Debug, Args)]
69pub struct ContainerShellArgs {
70 #[arg(
72 short = 'u',
73 long = "user",
74 value_name = "user",
75 conflicts_with = "root"
76 )]
77 pub user: Option<String>,
78 #[arg(short = 'r', long = "root")]
80 pub root: bool,
81 #[arg(value_name = "container")]
83 pub container: String,
84}
85
86#[derive(Debug, Args)]
87pub struct ContainerRmArgs {
88 #[arg(long = "no-force")]
90 pub no_force: bool,
91 #[arg(short = 'v', long = "volumes")]
93 pub volumes: bool,
94 #[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 Down(ComposeDownArgs),
109}
110
111#[derive(Debug, Args)]
112pub struct ComposeDownArgs {
113 #[arg(short = 'a', long = "all")]
115 pub all: bool,
116 #[arg(short = 'y', long = "yes")]
118 pub yes: bool,
119 #[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 Zsh(RunZshArgs),
134}
135
136#[derive(Debug, Args)]
137pub struct RunZshArgs {
138 #[arg(long = "no-mount")]
140 pub no_mount: bool,
141 #[arg(short = 'w', long = "workdir", value_name = "path")]
143 pub workdir: Option<String>,
144 #[arg(short = 'n', long = "name", value_name = "name")]
146 pub name: Option<String>,
147 #[arg(
149 short = 'u',
150 long = "user",
151 value_name = "user",
152 conflicts_with = "root"
153 )]
154 pub user: Option<String>,
155 #[arg(short = 'r', long = "root")]
157 pub root: bool,
158 #[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 #[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}