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