Skip to main content

start_command/
args_parser_queries.rs

1//! Query/control option parsing for the start-command wrapper.
2//!
3//! These options address an execution that already exists instead of starting
4//! a new one: `--status`, `--list [--running]`, `--upload-log`, `--stop`,
5//! `--terminate`, `--attach [--read-only]`, `--resume [-- <command>]`,
6//! `--resume-all` and `--cleanup [<uuid-or-session-name>]`. They are parsed here so `args_parser` stays
7//! focused on the options that shape a new execution (issue #162).
8
9use crate::args_parser::{WrapperOptions, VALID_OUTPUT_FORMATS};
10
11/// Setter applied to the parsed options when an identifier option matches.
12type IdentifierSetter = fn(&mut WrapperOptions, String);
13
14/// Setter applied to the parsed options when a flag option matches.
15type FlagSetter = fn(&mut WrapperOptions);
16
17/// Options that take a UUID or session name argument.
18const IDENTIFIER_OPTIONS: [(&str, IdentifierSetter); 6] = [
19    ("--status", |o, v| o.status = Some(v)),
20    ("--upload-log", |o, v| o.upload_log = Some(v)),
21    ("--stop", |o, v| o.stop = Some(v)),
22    ("--terminate", |o, v| o.terminate = Some(v)),
23    ("--attach", |o, v| o.attach = Some(v)),
24    ("--resume", |o, v| o.resume = Some(v)),
25];
26
27/// Options that are plain flags.
28const FLAG_OPTIONS: [(&str, FlagSetter); 4] = [
29    ("--list", |o| o.list = true),
30    ("--running", |o| o.running = true),
31    ("--read-only", |o| o.read_only = true),
32    ("--resume-all", |o| o.resume_all = true),
33];
34
35/// `--cleanup` and `--cleanup-dry-run` take an optional execution: without one
36/// they clean up stale records, with one they remove that execution's Docker
37/// containers and snapshot images (issue #224).
38fn parse_cleanup_option(
39    args: &[String],
40    index: usize,
41    options: &mut WrapperOptions,
42) -> Result<usize, String> {
43    let arg = args[index].as_str();
44    let (name, inline) = match arg.split_once('=') {
45        Some((name, value)) => (name, Some(value)),
46        None => (arg, None),
47    };
48    if name != "--cleanup" && name != "--cleanup-dry-run" {
49        return Ok(0);
50    }
51    options.cleanup = true;
52    options.cleanup_dry_run |= name == "--cleanup-dry-run";
53    if let Some(value) = inline {
54        if value.is_empty() {
55            return Err(format!(
56                "Option {} requires a UUID or session name argument",
57                name
58            ));
59        }
60        options.cleanup_target = Some(value.to_string());
61        return Ok(1);
62    }
63    match args.get(index + 1).filter(|next| !next.starts_with('-')) {
64        Some(next) => {
65            options.cleanup_target = Some(next.clone());
66            Ok(2)
67        }
68        None => Ok(1),
69    }
70}
71
72/// Query modes that may be combined with `--output-format`.
73const OUTPUT_FORMAT_MODES: [&str; 4] = ["--status", "--list", "--resume", "--resume-all"];
74
75/// Parse one query/control option.
76///
77/// Returns the number of arguments consumed, or `Ok(0)` when `args[index]` is
78/// not a query/control option.
79pub fn parse_query_option(
80    args: &[String],
81    index: usize,
82    options: &mut WrapperOptions,
83) -> Result<usize, String> {
84    let arg = &args[index];
85
86    let cleanup_consumed = parse_cleanup_option(args, index, options)?;
87    if cleanup_consumed > 0 {
88        return Ok(cleanup_consumed);
89    }
90
91    for (name, apply) in FLAG_OPTIONS {
92        if arg == name {
93            apply(options);
94            return Ok(1);
95        }
96    }
97
98    for (name, apply) in IDENTIFIER_OPTIONS {
99        if arg == name {
100            if index + 1 < args.len() && !args[index + 1].starts_with('-') {
101                apply(options, args[index + 1].clone());
102                return Ok(2);
103            }
104            return Err(format!(
105                "Option {} requires a UUID or session name argument",
106                arg
107            ));
108        }
109
110        let prefix = format!("{}=", name);
111        if let Some(value) = arg.strip_prefix(&prefix) {
112            if value.is_empty() {
113                return Err(format!(
114                    "Option {} requires a UUID or session name argument",
115                    name
116                ));
117            }
118            apply(options, value.to_string());
119            return Ok(1);
120        }
121    }
122
123    if arg == "--output-format" {
124        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
125            options.output_format = Some(args[index + 1].to_lowercase());
126            return Ok(2);
127        }
128        return Err(format!("Option {} requires a format argument", arg));
129    }
130
131    if arg.starts_with("--output-format=") {
132        options.output_format = Some(arg.split('=').nth(1).unwrap_or("").to_lowercase());
133        return Ok(1);
134    }
135
136    Ok(0)
137}
138
139/// List the query/control modes selected by these options.
140pub fn active_query_modes(options: &WrapperOptions) -> Vec<&'static str> {
141    let mut modes = Vec::new();
142    if options.status.is_some() {
143        modes.push("--status");
144    }
145    if options.list {
146        modes.push("--list");
147    }
148    if options.upload_log.is_some() {
149        modes.push("--upload-log");
150    }
151    if options.stop.is_some() {
152        modes.push("--stop");
153    }
154    if options.terminate.is_some() {
155        modes.push("--terminate");
156    }
157    if options.attach.is_some() {
158        modes.push("--attach");
159    }
160    if options.resume.is_some() {
161        modes.push("--resume");
162    }
163    if options.resume_all {
164        modes.push("--resume-all");
165    }
166    if options.cleanup {
167        modes.push("--cleanup");
168    }
169    modes
170}
171
172/// True when any query/control mode is active.
173pub fn has_query_mode(options: &WrapperOptions) -> bool {
174    !active_query_modes(options).is_empty()
175}
176
177/// Validate query/control option combinations.
178pub fn validate_query_options(options: &WrapperOptions) -> Result<(), String> {
179    if let Some(ref format) = options.output_format {
180        if !VALID_OUTPUT_FORMATS.contains(&format.as_str()) {
181            return Err(format!(
182                "Invalid output format: \"{}\". Valid options are: {}",
183                format,
184                VALID_OUTPUT_FORMATS.join(", ")
185            ));
186        }
187    }
188
189    let modes = active_query_modes(options);
190
191    if modes.len() > 1 {
192        return Err(
193            "Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, or --cleanup in the same invocation"
194                .to_string(),
195        );
196    }
197
198    if options.output_format.is_some()
199        && !modes.iter().any(|mode| OUTPUT_FORMAT_MODES.contains(mode))
200    {
201        return Err(
202            "--output-format option is only valid with --status, --list, --resume, or --resume-all"
203                .to_string(),
204        );
205    }
206
207    if options.running && !options.list {
208        return Err("--running option is only valid with --list".to_string());
209    }
210
211    if options.read_only && options.attach.is_none() {
212        return Err("--read-only option is only valid with --attach".to_string());
213    }
214
215    Ok(())
216}