Skip to main content

kernel/cli/
command_parsing.rs

1use std::collections::HashMap;
2use std::str::SplitAsciiWhitespace;
3use common::logging::LogLevel;
4use common::logging::LogLevel::*;
5use common::fixture::ChannelError::{FineDegreeTooHigh, FineDegreeExists, FineDegreeOutOfRange};
6use common::fixture::FixtureError::InvalidPropertyType;
7use common::fixture::{
8    ChannelIndex, ChannelValue, ChannelParameter, PropertyType, MAX_FINE_DEGREES,
9    FloatChannelValue
10};
11use common::cli_actions::CliAction;
12use crate::cli::cli_executing::execute_cli_action;
13
14
15/// Runs a command string by parsing it into a [`CliAction`] and executing it.
16///
17/// # Arguments
18///
19/// * `is_kernel` - Flag indicating whether the command is executed from the kernel console
20/// * `command`   - The raw command string entered by the user
21pub fn run_command(is_kernel: bool, command: String) -> (LogLevel, String) {
22    match parse_cli_string(command) {
23        Ok(command) => execute_cli_action(is_kernel, &command),
24        Err(e) => (UserError, e.to_string())
25    }
26}
27
28/// Parses a raw command string into a structured [`CliAction`].
29/// See '../help.txt' for a list of available commands.
30///
31/// # Arguments
32///
33/// * `command_string` - The raw string to parse
34///
35/// # Errors
36///
37/// Returns an error string if the command is unknown, arguments are missing, or the argument count/types are invalid.
38fn parse_cli_string(command_string: String) -> Result<CliAction,String> {
39    let mut line_iter = command_string.split_ascii_whitespace();
40    let arg_count = line_iter.clone().count().saturating_sub(1);
41
42    match line_iter.next() {
43        Some("help") => Ok(CliAction::Help),
44
45        Some("new") if arg_count % 2 == 1 && arg_count > 1 => {
46            parse_new_fixture_type(line_iter)
47        }
48
49        Some("new") => {
50            Err("Error: \"new\"-Command needs a name for the new Fixture-Type, and then a list of properties with \
51            their channels.".to_string())
52        }
53
54        Some("add") if arg_count == 3 => {
55            parse_new_fixture(line_iter)
56        }
57
58        Some("add") => {
59            Err("Error: \"add\" needs a name, a fixture-type and a start-channel (including a start-universe) as \
60            arguments".to_string())
61        }
62
63        Some("move") if arg_count == 2 => {
64            parse_move_fixture(line_iter)
65        }
66
67        Some("move") => {
68            Err("Error: \"move\" needs a fixture and a start-channel (including a start-universe) as \
69            arguments".to_string())
70        }
71
72        Some("remove") if arg_count == 1 => {
73            parse_remove_fixture(line_iter)
74        }
75
76        Some("remove") => {
77            Err("Error: \"remove\" needs a fixture as argument".to_string())
78        }
79
80        Some("set") if arg_count == 3 => {
81            parse_set_value(line_iter)
82        }
83
84        Some("set") => {
85            Err("Error: \"set\" needs a fixture, a property, and a value as arguments".to_string())
86        }
87
88        Some("type") if arg_count == 1 => {
89            parse_get_type(line_iter)
90        }
91
92        Some("type") => {
93            Err("Error: \"type\" needs a fixture as argument".to_string())
94        }
95
96        Some("exit") if arg_count <= 1 => {
97            parse_exit(line_iter)
98        },
99
100        Some("exit") => {
101            Err("Error: \"exit\" accepts at most one optional argument ('save' or 'discard').".to_string())
102        },
103
104        Some(command) => {
105            let args = line_iter.collect::<Vec<_>>().join(" ");
106            Ok(CliAction::OtherCommands {
107                command: format!("{} {}",command,args)
108            })
109        }
110
111        _ => Err("Unknown command. Please enter help, to get a list of commands.".to_string())
112    }
113}
114
115/// Parses arguments to construct a [`CliAction::FixtureNew`] instance for creating a new fixture type.
116///
117/// # Arguments
118///
119/// * `args` - Iterator over the remaining whitespace-separated arguments
120///
121/// # Errors
122///
123/// Returns an error string if channel parsing fails or if the syntax for properties and channels is invalid.
124fn parse_new_fixture_type(mut args: SplitAsciiWhitespace) -> Result<CliAction,String> {
125    let name = args.next().unwrap().to_string();
126    let mut properties: HashMap<PropertyType, ChannelParameter> = HashMap::new();
127
128    while let Some(property_name) = args.next() {
129        //We can do that without throwing an Error, because args has an even number of elements at this point
130        let channel = args.next().unwrap();
131
132        let channel_index = match channel.parse::<ChannelIndex>() {
133            Ok(channel) => channel,
134            Err(_) => {
135                return Err(format!("ErrorPenis: \"{channel}\" is not a valid channel-number"));
136            }
137        };
138
139        //fine-channels
140        if let Some((property_name, suffix)) = property_name.rsplit_once("_f") {
141
142            let fine_degree = if suffix.is_empty() {
143                1
144            } else {
145                match suffix.parse::<u8>() {
146                    Ok(fine_degree) => fine_degree,
147                    Err(_) => return Err(format!("Error: \"{suffix}\" is not a valid fine degree"))
148                }
149            };
150
151            let property_type = parse_property_type(property_name)?;
152
153            match properties.get_mut(&property_type) {
154                Some(channel_object) => {
155                    match channel_object.add_fine(fine_degree.into(), channel_index, 0) {
156                        Err(FineDegreeTooHigh(f)) =>
157                            return Err(format!("Fine-degree {} is too high, add the lower ones first", f)),
158                        Err(FineDegreeExists(f)) =>
159                            return Err(format!("Fine-degree {} already exists", f)),
160                        Err(FineDegreeOutOfRange(f)) =>
161                            return Err(format!("Fine-degree {} out of range, must be lower than {}",
162                                               f, MAX_FINE_DEGREES)),
163                        Ok(()) => {}
164                        _ => unreachable!()
165                    }
166                }
167
168                None => return Err(format!(
169                    "Error: Cannot add fine channel for '{}' because the coarse channel is missing! Add '{}' first.",
170                    property_name, property_type)),
171            }
172        } else {
173            //Non-fine channels
174
175            let property_type = parse_property_type(property_name)?;
176
177            if !properties.contains_key(&property_type) {
178                properties.insert(property_type, ChannelParameter::new(channel_index, 0));
179            } else {
180                return Err(format!("{property_name} can only have one coarse Channel"))
181            }
182        }
183    }
184    Ok(CliAction::FixtureNew {
185        name,
186        channels: properties,
187    })
188
189}
190
191/// Parses arguments to construct a [`CliAction::FixtureAdd`] instance for spawning a new fixture instance.
192///
193/// # Arguments
194///
195/// * `args` - Iterator over the remaining whitespace-separated arguments
196///
197/// # Errors
198///
199/// Returns an error string if the universe or channel parameters are malformed.
200fn parse_new_fixture(mut args: SplitAsciiWhitespace) -> Result<CliAction,String> {
201    let name = args.next().unwrap().to_string();
202    let fixture_type_name = args.next().unwrap().to_string();
203    let (universe, channel) = parse_universe_and_channel(args)?;
204
205    Ok(CliAction::FixtureAdd {
206        name,
207        fixture_type_name,
208        channel,
209        universe,
210    })
211}
212
213/// Parses arguments to construct a [`CliAction::FixtureMove`] instance for relocating an existing fixture.
214///
215/// # Arguments
216///
217/// * `args` - Iterator over the remaining whitespace-separated arguments
218///
219/// # Errors
220///
221/// Returns an error string if the universe or channel parameters are malformed.
222fn parse_move_fixture(mut args: SplitAsciiWhitespace) -> Result<CliAction, String> {
223    let fixture_name = args.next().unwrap().to_string();
224    let (new_universe, new_channel) = parse_universe_and_channel(args)?;
225
226    Ok(CliAction::FixtureMove {
227        fixture_name,
228        new_channel,
229        new_universe
230    })
231}
232
233/// Parses arguments to construct a [`CliAction::FixtureRemove`] instance for removing a fixture.
234///
235/// # Arguments
236///
237/// * `args` - Iterator over the remaining whitespace-separated arguments
238fn parse_remove_fixture(mut args: SplitAsciiWhitespace) -> Result<CliAction, String> {
239    let fixture_name = args.next().unwrap().to_string();
240
241    Ok(CliAction::FixtureRemove {
242        fixture_name
243    })
244}
245
246/// Parses arguments to construct a [`CliAction::FixtureSet`] instance for updating a fixture property.
247///
248/// # Arguments
249///
250/// * `args` - Iterator over the remaining whitespace-separated arguments
251///
252/// # Errors
253///
254/// Returns an error string if the property type is unknown or the value format is invalid.
255fn parse_set_value(mut args: SplitAsciiWhitespace) -> Result<CliAction,String> {
256    let name = args.next().unwrap().to_string();
257    let property_name = args.next().unwrap().to_string();
258    let value = args.next().unwrap().to_string();
259
260    let property_type = parse_property_type(&property_name)?;
261
262    let value = parse_cli_value(&*value)?;
263
264    Ok(CliAction::FixtureSet {
265        name,
266        property_type,
267        value,
268    })
269}
270
271/// Parses arguments to construct a [`CliAction::FixtureGetType`] instance for querying a fixture type.
272///
273/// # Arguments
274///
275/// * `args` - Iterator over the remaining whitespace-separated arguments
276fn parse_get_type(mut args: SplitAsciiWhitespace) -> Result<CliAction,String> {
277    let fixture_name = args.next().unwrap().to_string();
278
279    Ok(CliAction::FixtureGetType {
280        fixture_name,
281    })
282}
283
284/// Parses arguments to construct an [`CliAction::Exit`] instance.
285///
286/// # Arguments
287///
288/// * `args` - Iterator over the remaining whitespace-separated arguments
289///
290/// # Errors
291///
292/// Returns an error string if an invalid argument is provided.
293fn parse_exit(mut args: SplitAsciiWhitespace) -> Result<CliAction,String> {
294    match args.next() {
295        Some("save") => Ok(CliAction::Exit { save_changes: Some(true) }),
296        Some("discard") => Ok(CliAction::Exit { save_changes: Some(false) }),
297        Some(invalid) => Err(format!("Error: Invalid argument '{}' for exit. Use 'save' or 'discard'.", invalid)),
298        None => Ok(CliAction::Exit { save_changes: None }),
299    }
300}
301
302/// Parses a raw channel value string supporting percentages, hex codes, or raw integers
303/// (According to [ChannelValue]).
304///
305/// # Arguments
306///
307/// * `input` - The raw string representation of the value
308pub(super) fn parse_cli_value(input: &str) -> Result<ChannelValue,String> {
309
310    let sanitized_input = input.trim().replace("_", "");
311    let input_str = sanitized_input.as_str();
312
313    if let Some(percent_string) = input_str.strip_suffix("%") {
314        match percent_string.parse::<FloatChannelValue>() {
315            Ok(p) if (0.0..=100.0).contains(&p) => {
316                let fraction = p / 100.0;
317                let raw_value = fraction * (ChannelValue::MAX as FloatChannelValue);
318
319                Ok(raw_value.round() as ChannelValue)
320            }
321            Ok(_) => Err(format!("\"{}\" must be between 0 and 100.", percent_string)),
322            Err(_) => Err(format!("Invalid percentage format {}", percent_string)),
323        }
324    } else if let Some(hex_str) = input_str.strip_prefix("#") {
325        let hex_len = hex_str.len();
326
327        match ChannelValue::from_str_radix(hex_str, 16) {
328            Ok(val) => {
329                let scaled_val = match hex_len {
330                    2 => val * 0x01010101,
331                    4 => val * 0x00010001,
332                    6 => val << 8,
333                    8 => val,
334                    _ => return Err(format!(
335                        "Unsupported hex length: {}. Valid lengths are 2, 4, 6, or 8 digits (excluding '_').",
336                        hex_len
337                    )),
338                };
339                Ok(scaled_val)
340            }
341
342            Err(_) => Err(format!("Invalid hex format {}", hex_str))
343        }
344    } else {
345        input_str.parse::<ChannelValue>()
346            .map_err(|_| format!("Invalid value {}. ", input_str))
347    }
348}
349
350
351/// Parses a universe and channel pair formatted as `[universe].[channel]` into a tuple.
352///
353/// # Arguments
354///
355/// * `args` - Iterator over the remaining whitespace-separated arguments
356///
357/// # Errors
358///
359/// Returns an error string if the format is missing a dot or if the numbers are out of valid bounds.
360fn parse_universe_and_channel(mut args: SplitAsciiWhitespace) -> Result<(usize, ChannelIndex),String> {
361    let parsed_string = args.next().unwrap();
362    let (universe, channel_string) = match parsed_string.split_once(".") {
363        Some(pair) => pair,
364        None => return Err("Error: Please specify the channel with [universe].[channel]".to_string()),
365    };
366    let universe = match universe.parse::<usize>() {
367        Ok(universe) => universe,
368        Err(_) => {
369            return Err(format!("Error: \"{universe}\" is not a valid universe-number"))
370        }
371    };
372
373    let channel = match channel_string.parse::<ChannelIndex>() {
374        Ok(channel) => channel,
375        Err(_) => {
376            return Err(format!("Error: \"{}\" is not a valid channel-number", channel_string))
377        }
378    };
379    Ok((universe, channel))
380}
381
382/// Parses and validates a property type from a string slice, mapping it to a [`PropertyType`].
383///
384/// # Arguments
385///
386/// * `property_name` - The string name of the property
387///
388/// # Errors
389///
390/// Returns an error string if the property name is not recognized.
391fn parse_property_type(property_name: &str) -> Result<PropertyType, String> {
392    Ok(match PropertyType::from_str(property_name) {
393        Ok(property_type) => property_type,
394        Err(InvalidPropertyType(property_type)) => {
395            return Err(format!("Error: \"{property_type}\" is not a valid PropertyType"))
396        }
397        Err(_) => unreachable!() //All possible Errors have been handles
398    })
399}