xacli_core/application/
parser.rs

1//! Command-line argument parser
2//!
3//! This module implements a tokenizer-based parser that handles:
4//! - Subcommand routing with multi-level support
5//! - Long options (`--name` or `--name=value`)
6//! - Short options (`-n` or `-n value`)
7//! - Positional arguments (ordered, last one can be multiple)
8//! - `--` separator to force remaining tokens as positional
9//! - Parent command argument inheritance (child overrides parent)
10//! - Special flags detection (`--help`, `--version`)
11
12use std::collections::HashMap;
13
14use super::{value::InputValue, App};
15use crate::{ArgInfo, ArgKind, ArgType, Command, Error, Result};
16
17/// Parsed command-line arguments result
18#[derive(Debug, Clone, PartialEq, Default)]
19pub struct Parsed {
20    /// Command path (e.g., ["app", "get", "pod"])
21    commands: Vec<String>,
22    /// Parsed argument values by name
23    args: HashMap<String, InputValue>,
24    /// Whether --help or -h was requested
25    help_requested: bool,
26    /// Whether --version or -V was requested
27    version_requested: bool,
28}
29
30impl Parsed {
31    /// Get the command path
32    pub fn commands(&self) -> &[String] {
33        &self.commands
34    }
35
36    /// Get an argument value by name
37    pub fn get(&self, name: &str) -> Option<&InputValue> {
38        self.args.get(name)
39    }
40
41    /// Check if help was requested
42    pub fn is_help(&self) -> bool {
43        self.help_requested
44    }
45
46    /// Check if version was requested
47    pub fn is_version(&self) -> bool {
48        self.version_requested
49    }
50
51    /// Get all parsed arguments
52    pub fn args(&self) -> &HashMap<String, InputValue> {
53        &self.args
54    }
55}
56
57/// Internal parser state
58pub struct Parser<'a> {
59    /// Input tokens
60    tokens: Vec<String>,
61    /// Current position in tokens
62    position: usize,
63    /// App definition reference
64    app: &'a App,
65}
66
67impl<'a> Parser<'a> {
68    pub fn new(app: &'a App, tokens: Vec<String>) -> Self {
69        Self {
70            tokens,
71            position: 1, // Skip the first token (program name)
72            app,
73        }
74    }
75
76    /// Consume and return the next token
77    fn next(&mut self) -> Option<String> {
78        if self.position < self.tokens.len() {
79            let token = self.tokens[self.position].clone();
80            self.position += 1;
81            Some(token)
82        } else {
83            None
84        }
85    }
86
87    /// Check if there are more tokens
88    fn has_more(&self) -> bool {
89        self.position < self.tokens.len()
90    }
91
92    /// Main parsing entry point
93    pub fn parse(&mut self) -> Result<Parsed> {
94        let mut commands = vec![self.app.info.name.clone()];
95        let mut args: HashMap<String, InputValue> = HashMap::new();
96        let mut positional_candidates: Vec<String> = Vec::new();
97        let mut help_requested = false;
98        let mut version_requested = false;
99        let mut force_positional = false;
100
101        // Track command path indices instead of references to avoid borrow issues
102        // command_path[0] is index in app.root.subcommands, command_path[1] is index in subcommands, etc.
103        let mut command_path: Vec<usize> = Vec::new();
104
105        // Collect merged args from command path (child overrides parent)
106        let mut merged_args = self.collect_args_by_path(&command_path);
107
108        while self.has_more() {
109            let token = self.next().unwrap();
110
111            // Handle -- separator: all remaining tokens are positional
112            if token == "--" {
113                force_positional = true;
114                continue;
115            }
116
117            if force_positional {
118                positional_candidates.push(token);
119                continue;
120            }
121
122            // Check for special flags first
123            if token == "--help" || token == "-h" {
124                help_requested = true;
125                continue;
126            }
127            if token == "--version" || token == "-V" {
128                version_requested = true;
129                continue;
130            }
131
132            // Handle long options (--name or --name=value)
133            if token.starts_with("--") {
134                let (name, inline_value) = self.parse_long_option(&token);
135                let arg_info = self.find_arg_by_long(&merged_args, &name)?;
136
137                let value = self.resolve_option_value(&arg_info, inline_value)?;
138                self.insert_arg_value(&mut args, &arg_info, value)?;
139                continue;
140            }
141
142            // Handle short options (-x or -x value)
143            if token.starts_with('-') && token.len() == 2 {
144                let short_char = token.chars().nth(1).unwrap();
145                let arg_info = self.find_arg_by_short(&merged_args, short_char)?;
146
147                let value = self.resolve_option_value(&arg_info, None)?;
148                self.insert_arg_value(&mut args, &arg_info, value)?;
149                continue;
150            }
151
152            // Try to match as command/subcommand
153            let matched_idx = if command_path.is_empty() {
154                // Try to match from App.root.subcommands (top-level commands)
155                self.find_top_level_command_idx(&token)
156            } else {
157                // Try to match from current command's subcommands
158                self.find_subcommand_idx(&command_path, &token)
159            };
160
161            if let Some(idx) = matched_idx {
162                commands.push(token);
163                command_path.push(idx);
164                // Re-merge args with new command context
165                merged_args = self.collect_args_by_path(&command_path);
166                continue;
167            }
168
169            // Otherwise treat as positional argument candidate
170            positional_candidates.push(token);
171        }
172
173        // Fill positional arguments in order
174        self.fill_positional_args(&merged_args, &positional_candidates, &mut args)?;
175
176        // Apply defaults and validate required args (skip if help/version requested)
177        if !help_requested && !version_requested {
178            self.apply_defaults_and_validate(&merged_args, &mut args)?;
179        }
180
181        Ok(Parsed {
182            commands,
183            args,
184            help_requested,
185            version_requested,
186        })
187    }
188
189    /// Collect args from command path, child definitions override parent
190    /// Returns args in definition order (important for positional args)
191    fn collect_args_by_path(&self, command_path: &[usize]) -> Vec<ArgInfo> {
192        // Use Vec to maintain definition order
193        let mut result: Vec<ArgInfo> = Vec::new();
194        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
195
196        // If command_path is empty, collect from root command
197        if command_path.is_empty() {
198            for arg in &self.app.root.args {
199                if !seen.contains(&arg.info.name) {
200                    result.push(arg.info.clone());
201                    seen.insert(arg.info.name.clone());
202                }
203            }
204        } else {
205            // Traverse command path and collect args
206            if let Some(cmd) = self.get_command_by_path(command_path) {
207                for arg in &cmd.args {
208                    if !seen.contains(&arg.info.name) {
209                        result.push(arg.info.clone());
210                        seen.insert(arg.info.name.clone());
211                    }
212                }
213            }
214        }
215
216        result
217    }
218
219    /// Get command reference by path indices
220    fn get_command_by_path(&self, path: &[usize]) -> Option<&Command> {
221        if path.is_empty() {
222            return None;
223        }
224
225        let mut cmd = self.app.root.subcommands.get(path[0])?;
226        for &idx in &path[1..] {
227            cmd = cmd.subcommands.get(idx)?;
228        }
229        Some(cmd)
230    }
231
232    /// Find top-level command index by name or alias
233    fn find_top_level_command_idx(&self, name: &str) -> Option<usize> {
234        self.app
235            .root
236            .subcommands
237            .iter()
238            .position(|cmd| cmd.info.name == name || cmd.info.aliases.contains(&name.to_string()))
239    }
240
241    /// Find subcommand index by name or alias
242    fn find_subcommand_idx(&self, parent_path: &[usize], name: &str) -> Option<usize> {
243        let parent = self.get_command_by_path(parent_path)?;
244        parent
245            .subcommands
246            .iter()
247            .position(|sub| sub.info.name == name || sub.info.aliases.contains(&name.to_string()))
248    }
249
250    /// Parse --name or --name=value format
251    fn parse_long_option(&self, token: &str) -> (String, Option<String>) {
252        let without_prefix = &token[2..]; // Remove "--"
253        if let Some(eq_pos) = without_prefix.find('=') {
254            let name = without_prefix[..eq_pos].to_string();
255            let value = without_prefix[eq_pos + 1..].to_string();
256            (name, Some(value))
257        } else {
258            (without_prefix.to_string(), None)
259        }
260    }
261
262    /// Find argument by long name or alias
263    fn find_arg_by_long(&self, args: &[ArgInfo], name: &str) -> Result<ArgInfo> {
264        for arg in args {
265            match &arg.kind {
266                ArgKind::Flag { long, aliases, .. } | ArgKind::Option { long, aliases, .. } => {
267                    if long == name || aliases.contains(&name.to_string()) {
268                        return Ok(arg.clone());
269                    }
270                }
271                ArgKind::Positional => {}
272            }
273        }
274        Err(Error::InvalidFlag(format!("Unknown option: --{}", name)))
275    }
276
277    /// Find argument by short character
278    fn find_arg_by_short(&self, args: &[ArgInfo], short_char: char) -> Result<ArgInfo> {
279        for arg in args {
280            match &arg.kind {
281                ArgKind::Flag { short, .. } | ArgKind::Option { short, .. } => {
282                    if *short == Some(short_char) {
283                        return Ok(arg.clone());
284                    }
285                }
286                ArgKind::Positional => {}
287            }
288        }
289        Err(Error::InvalidFlag(format!(
290            "Unknown option: -{}",
291            short_char
292        )))
293    }
294
295    /// Resolve value for flag or option
296    fn resolve_option_value(
297        &mut self,
298        arg: &ArgInfo,
299        inline_value: Option<String>,
300    ) -> Result<InputValue> {
301        match &arg.kind {
302            ArgKind::Flag { .. } => {
303                // Flags are always bool, inline value not supported
304                Ok(InputValue::Bool(true))
305            }
306            ArgKind::Option { .. } => {
307                let value_str = if let Some(v) = inline_value {
308                    v
309                } else {
310                    // Consume next token as value
311                    self.next().ok_or_else(|| Error::InvalidFlagValue {
312                        flag: arg.name.clone(),
313                        message: "Missing value".to_string(),
314                    })?
315                };
316                self.parse_value(&value_str, &arg.schema.ty)
317            }
318            ArgKind::Positional => unreachable!(),
319        }
320    }
321
322    /// Parse string value according to ArgType
323    fn parse_value(&self, value: &str, ty: &ArgType) -> Result<InputValue> {
324        match ty {
325            ArgType::String => Ok(InputValue::String(value.to_string())),
326            ArgType::Int => value.parse::<i64>().map(InputValue::Int).map_err(|_| {
327                Error::InvalidArgumentValue(format!("Cannot parse '{}' as integer", value))
328            }),
329            ArgType::Float => value.parse::<f64>().map(InputValue::Float).map_err(|_| {
330                Error::InvalidArgumentValue(format!("Cannot parse '{}' as float", value))
331            }),
332            ArgType::Bool => value.parse::<bool>().map(InputValue::Bool).map_err(|_| {
333                Error::InvalidArgumentValue(format!("Cannot parse '{}' as boolean", value))
334            }),
335        }
336    }
337
338    /// Insert value into args map, handling multiple values
339    fn insert_arg_value(
340        &self,
341        args: &mut HashMap<String, InputValue>,
342        arg: &ArgInfo,
343        value: InputValue,
344    ) -> Result<()> {
345        if arg.schema.multiple {
346            // Append to array
347            let arr = args
348                .entry(arg.name.clone())
349                .or_insert_with(|| InputValue::Array(Vec::new()));
350            if let InputValue::Array(vec) = arr {
351                vec.push(Box::new(value));
352            }
353        } else {
354            args.insert(arg.name.clone(), value);
355        }
356        Ok(())
357    }
358
359    /// Fill positional arguments in definition order
360    fn fill_positional_args(
361        &self,
362        merged_args: &[ArgInfo],
363        candidates: &[String],
364        args: &mut HashMap<String, InputValue>,
365    ) -> Result<()> {
366        // Get positional args in definition order
367        let positional_args: Vec<&ArgInfo> = merged_args
368            .iter()
369            .filter(|a| matches!(a.kind, ArgKind::Positional))
370            .collect();
371
372        if positional_args.is_empty() {
373            if !candidates.is_empty() {
374                return Err(Error::TooManyArguments);
375            }
376            return Ok(());
377        }
378
379        let last_idx = positional_args.len() - 1;
380        let mut candidate_idx = 0;
381
382        for (idx, arg) in positional_args.iter().enumerate() {
383            if idx == last_idx && arg.schema.multiple {
384                // Last positional with multiple: collect all remaining
385                let remaining: Vec<InputValue> = candidates[candidate_idx..]
386                    .iter()
387                    .map(|s| self.parse_value(s, &arg.schema.ty))
388                    .collect::<Result<Vec<_>>>()?;
389                if !remaining.is_empty() {
390                    args.insert(
391                        arg.name.clone(),
392                        InputValue::Array(remaining.into_iter().map(Box::new).collect()),
393                    );
394                }
395                candidate_idx = candidates.len(); // Mark all consumed
396            } else {
397                // Single value positional
398                if candidate_idx < candidates.len() {
399                    let value = self.parse_value(&candidates[candidate_idx], &arg.schema.ty)?;
400                    args.insert(arg.name.clone(), value);
401                    candidate_idx += 1;
402                }
403            }
404        }
405
406        // Check for extra positional arguments
407        if candidate_idx < candidates.len() {
408            return Err(Error::TooManyArguments);
409        }
410
411        Ok(())
412    }
413
414    /// Apply default values and validate required args
415    fn apply_defaults_and_validate(
416        &self,
417        merged_args: &[ArgInfo],
418        args: &mut HashMap<String, InputValue>,
419    ) -> Result<()> {
420        for arg in merged_args {
421            if args.contains_key(&arg.name) {
422                continue;
423            }
424
425            // Try environment variable
426            if let Some(env_var) = &arg.env {
427                if let Ok(env_value) = std::env::var(env_var) {
428                    let value = self.parse_value(&env_value, &arg.schema.ty)?;
429                    args.insert(arg.name.clone(), value);
430                    continue;
431                }
432            }
433
434            // Try default value
435            if let Some(default) = &arg.schema.default_value {
436                let value = self.parse_value(default, &arg.schema.ty)?;
437                args.insert(arg.name.clone(), value);
438                continue;
439            }
440
441            // Check required
442            if arg.schema.required {
443                return Err(Error::MissingArgument(arg.name.clone()));
444            }
445        }
446
447        Ok(())
448    }
449}