cli_command/
parse.rs

1use crate::cli_error::{CliError, CliErrorKind};
2use crate::command::Command;
3use std::collections::HashMap;
4use std::env;
5use std::iter::Peekable;
6
7/// Parses command line arguments from `std::env::args()`.
8///
9/// This function automatically reads the command line arguments passed to the program
10/// and parses them into a `Command` struct. It skips the program name (first argument).
11///
12/// # Returns
13/// * `Ok(Command)` - If parsing succeeds
14/// * `Err(CliError)` - If parsing fails
15///
16/// # Examples
17/// ```rust
18/// use cli_command::parse_command_string;
19///
20/// // When called with: myapp serve --port 8080 --host localhost
21/// let cmd = parse_command_string("serve --port 8080 --host localhost").unwrap();
22/// assert_eq!(cmd.name, "serve");
23/// assert_eq!(cmd.get_argument("port"), Some("8080"));
24/// assert_eq!(cmd.get_argument("host"), Some("localhost"));
25/// ```
26pub fn parse_command_line() -> Result<Command, CliError> {
27    let command_line_arguments: Vec<String> = env::args().skip(1).collect::<Vec<String>>();
28    let mut command_line_arguments = command_line_arguments.iter().map(|s| &s[..]).peekable();
29    parse_command_line_args(&mut command_line_arguments)
30}
31
32/// Parses command line arguments from a string.
33///
34/// This function is useful for testing or when you have command line arguments
35/// stored in a string rather than from the actual command line.
36///
37/// # Arguments
38/// * `string` - The command line string to parse
39///
40/// # Returns
41/// * `Ok(Command)` - If parsing succeeds
42/// * `Err(CliError)` - If parsing fails
43///
44/// # Examples
45/// ```rust
46/// use cli_command::parse_command_string;
47///
48/// let cmd = parse_command_string("serve --port 8080 --host localhost").unwrap();
49/// assert_eq!(cmd.name, "serve");
50/// assert_eq!(cmd.get_argument("port"), Some("8080"));
51/// assert_eq!(cmd.get_argument("host"), Some("localhost"));
52/// ```
53pub fn parse_command_string(string: &str) -> Result<Command, CliError> {
54    parse_command_line_args(&mut string.split(" ").into_iter().peekable())
55}
56
57pub fn parse_command_line_args<'a, I>(args: &mut Peekable<I>) -> Result<Command, CliError>
58where
59    I: Iterator<Item = &'a str>,
60{
61    let mut arguments = HashMap::new();
62    let command = match args.peek() {
63        None => "",
64        Some(&value) => {
65            if !value.starts_with("-") {
66                args.next();
67                value
68            } else {
69                ""
70            }
71        }
72    };
73
74    loop {
75        match parse_command_argument(args)? {
76            None => break,
77            Some((name, values)) => {
78                arguments.insert(name, values);
79            }
80        }
81    }
82    Ok(Command {
83        name: command.to_string(),
84        arguments,
85    })
86}
87
88fn parse_command_argument<'a, I>(
89    args: &mut Peekable<I>,
90) -> Result<Option<(String, Box<[String]>)>, CliError>
91where
92    I: Iterator<Item = &'a str>,
93{
94    match args.next() {
95        None => Ok(None),
96        Some(ref value) if value.starts_with("-") => {
97            let values = parse_argument_values(args);
98            let key_index: usize = if value.starts_with("--") { 2 } else { 1 };
99            Ok(Some(((&value[key_index..]).to_string(), values)))
100        }
101        Some(_) => Err(CliErrorKind::ParseCommandLine.into()),
102    }
103}
104
105fn parse_argument_values<'a, I>(args: &mut Peekable<I>) -> Box<[String]>
106where
107    I: Iterator<Item = &'a str>,
108{
109    let mut result = vec![];
110    loop {
111        match args.peek() {
112            Some(&value) => {
113                if !value.starts_with("-") {
114                    result.push(args.next().unwrap().to_string());
115                } else {
116                    break;
117                }
118            }
119            _ => break,
120        }
121    }
122
123    result.into_boxed_slice()
124}