avila_cli/
lib.rs

1//! Ávila CLI Parser
2//!
3//! Zero-dependency command-line argument parser with stack-allocated data structures.
4//! Provides compile-time type safety and constant-time argument lookups via HashMap.
5//!
6//! Features:
7//! - Zero dependencies (pure Rust std)
8//! - Colored output (ANSI escape codes)
9//! - Shell completion generation (bash, zsh, fish)
10//! - Argument groups and validation
11//! - Custom validators
12//! - Performance optimized (O(1) lookups)
13
14use std::collections::HashMap;
15use std::env;
16
17/// ANSI color codes for terminal output
18mod colors {
19    pub const RESET: &str = "\x1b[0m";
20    pub const BOLD: &str = "\x1b[1m";
21    pub const RED: &str = "\x1b[31m";
22    pub const GREEN: &str = "\x1b[32m";
23    pub const YELLOW: &str = "\x1b[33m";
24    pub const BLUE: &str = "\x1b[34m";
25    pub const CYAN: &str = "\x1b[36m";
26    pub const GRAY: &str = "\x1b[90m";
27
28    pub fn colorize(text: &str, color: &str) -> String {
29        if is_color_supported() {
30            format!("{}{}{}", color, text, RESET)
31        } else {
32            text.to_string()
33        }
34    }
35
36    fn is_color_supported() -> bool {
37        std::env::var("NO_COLOR").is_err()
38            && (std::env::var("TERM").map(|t| t != "dumb").unwrap_or(false)
39                || std::env::var("COLORTERM").is_ok())
40    }
41}
42
43/// Argument group for mutual exclusion or requirements
44#[derive(Clone)]
45pub struct ArgGroup {
46    name: String,
47    args: Vec<String>,
48    required: bool,
49    multiple: bool,
50}
51
52impl ArgGroup {
53    pub fn new(name: impl Into<String>) -> Self {
54        Self {
55            name: name.into(),
56            args: Vec::new(),
57            required: false,
58            multiple: false,
59        }
60    }
61
62    pub fn args(mut self, args: &[&str]) -> Self {
63        self.args = args.iter().map(|s| s.to_string()).collect();
64        self
65    }
66
67    pub fn required(mut self, req: bool) -> Self {
68        self.required = req;
69        self
70    }
71
72    pub fn multiple(mut self, mult: bool) -> Self {
73        self.multiple = mult;
74        self
75    }
76}
77
78/// Custom validator function type
79pub type Validator = fn(&str) -> Result<(), String>;
80
81/// Shell completion type
82#[derive(Debug, Clone, Copy, PartialEq)]
83pub enum Shell {
84    Bash,
85    Zsh,
86    Fish,
87    PowerShell,
88}
89
90/// Command-line application parser
91///
92/// Stack-allocated structure that defines the command-line interface schema.
93/// All fields use heap-allocated collections for dynamic argument counts,
94/// but the parser itself is deterministic and type-safe.
95pub struct App {
96    name: String,
97    version: String,
98    about: String,
99    author: Option<String>,
100    commands: Vec<Command>,
101    global_args: Vec<Arg>,
102    groups: Vec<ArgGroup>,
103    colored_help: bool,
104}
105
106impl App {
107    pub fn new(name: impl Into<String>) -> Self {
108        Self {
109            name: name.into(),
110            version: "1.0.0".to_string(),
111            about: String::new(),
112            author: None,
113            commands: Vec::new(),
114            global_args: Vec::new(),
115            groups: Vec::new(),
116            colored_help: true,
117        }
118    }
119
120    pub fn version(mut self, version: impl Into<String>) -> Self {
121        self.version = version.into();
122        self
123    }
124
125    pub fn about(mut self, about: impl Into<String>) -> Self {
126        self.about = about.into();
127        self
128    }
129
130    pub fn author(mut self, author: impl Into<String>) -> Self {
131        self.author = Some(author.into());
132        self
133    }
134
135    pub fn command(mut self, cmd: Command) -> Self {
136        self.commands.push(cmd);
137        self
138    }
139
140    pub fn arg(mut self, arg: Arg) -> Self {
141        self.global_args.push(arg);
142        self
143    }
144
145    pub fn group(mut self, group: ArgGroup) -> Self {
146        self.groups.push(group);
147        self
148    }
149
150    pub fn colored_help(mut self, colored: bool) -> Self {
151        self.colored_help = colored;
152        self
153    }
154
155    /// Generate shell completion script
156    pub fn generate_completion(&self, shell: Shell) -> String {
157        match shell {
158            Shell::Bash => self.generate_bash_completion(),
159            Shell::Zsh => self.generate_zsh_completion(),
160            Shell::Fish => self.generate_fish_completion(),
161            Shell::PowerShell => self.generate_powershell_completion(),
162        }
163    }
164
165    fn generate_bash_completion(&self) -> String {
166        let mut script = format!("_{}_completion() {{\n", self.name);
167        script.push_str("    local cur prev opts\n");
168        script.push_str("    COMPREPLY=()\n");
169        script.push_str("    cur=\"${COMP_WORDS[COMP_CWORD]}\"\n");
170        script.push_str("    prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n\n");
171        
172        // Add options
173        script.push_str("    opts=\"");
174        for arg in &self.global_args {
175            script.push_str(&format!("--{} ", arg.long));
176            if let Some(short) = &arg.short {
177                script.push_str(&format!("-{} ", short));
178            }
179        }
180        script.push_str("\"\n\n");
181        
182        // Add subcommands
183        if !self.commands.is_empty() {
184            script.push_str("    local commands=\"");
185            for cmd in &self.commands {
186                script.push_str(&format!("{} ", cmd.name));
187            }
188            script.push_str("\"\n\n");
189        }
190        
191        script.push_str("    COMPREPLY=( $(compgen -W \"${opts} ${commands}\" -- ${cur}) )\n");
192        script.push_str("    return 0\n");
193        script.push_str("}\n\n");
194        script.push_str(&format!("complete -F _{}_completion {}\n", self.name, self.name));
195        
196        script
197    }
198
199    fn generate_zsh_completion(&self) -> String {
200        let mut script = format!("#compdef {}\n\n", self.name);
201        script.push_str(&format!("_{}_completion() {{\n", self.name));
202        script.push_str("    local -a opts\n");
203        script.push_str("    opts=(\n");
204        
205        for arg in &self.global_args {
206            let help = arg.help.replace('\"', "'");
207            if let Some(short) = &arg.short {
208                script.push_str(&format!("        '(-{})--{}[{}]'\n", short, arg.long, help));
209            } else {
210                script.push_str(&format!("        '--{}[{}]'\n", arg.long, help));
211            }
212        }
213        
214        script.push_str("    )\n");
215        script.push_str("    _arguments $opts\n");
216        script.push_str("}\n\n");
217        script.push_str(&format!("_{}_completion\n", self.name));
218        
219        script
220    }
221
222    fn generate_fish_completion(&self) -> String {
223        let mut script = String::new();
224        
225        for arg in &self.global_args {
226            script.push_str(&format!("complete -c {} -l {} -d '{}'\n", 
227                self.name, arg.long, arg.help.replace('\'', "\\'")));
228            
229            if let Some(short) = &arg.short {
230                script.push_str(&format!("complete -c {} -s {} -d '{}'\n", 
231                    self.name, short, arg.help.replace('\'', "\\'")));
232            }
233        }
234        
235        for cmd in &self.commands {
236            script.push_str(&format!("complete -c {} -f -a '{}' -d '{}'\n",
237                self.name, cmd.name, cmd.about.replace('\'', "\\'")));
238        }
239        
240        script
241    }
242
243    fn generate_powershell_completion(&self) -> String {
244        let mut script = format!("Register-ArgumentCompleter -CommandName {} -ScriptBlock {{\n", self.name);
245        script.push_str("    param($commandName, $wordToComplete, $commandAst, $fakeBoundParameter)\n\n");
246        script.push_str("    $completions = @(\n");
247        
248        for arg in &self.global_args {
249            script.push_str(&format!("        @{{ CompletionText = '--{}'; ListItemText = '--{}'; ToolTip = '{}' }},\n",
250                arg.long, arg.long, arg.help.replace('\"', "'")));
251        }
252        
253        for cmd in &self.commands {
254            script.push_str(&format!("        @{{ CompletionText = '{}'; ListItemText = '{}'; ToolTip = '{}' }},\n",
255                cmd.name, cmd.name, cmd.about.replace('\"', "'")));
256        }
257        
258        script.push_str("    )\n\n");
259        script.push_str("    $completions | Where-Object { $_.CompletionText -like \"$wordToComplete*\" } | \n");
260        script.push_str("        ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.CompletionText, $_.ListItemText, 'ParameterValue', $_.ToolTip) }\n");
261        script.push_str("}\n");
262        
263        script
264    }
265
266    pub fn parse(self) -> Matches {
267        let args: Vec<String> = env::args().skip(1).collect();
268        self.parse_args(&args)
269    }
270
271    fn parse_args(self, args: &[String]) -> Matches {
272        let mut matches = Matches {
273            command: None,
274            args: HashMap::new(),
275            values: Vec::new(),
276        };
277
278        if args.is_empty() {
279            return matches;
280        }
281
282        // Check for help/version
283        if args[0] == "--help" || args[0] == "-h" {
284            self.print_help();
285            std::process::exit(0);
286        }
287        if args[0] == "--version" || args[0] == "-V" {
288            println!("{} {}", self.name, self.version);
289            std::process::exit(0);
290        }
291
292        // Parse command
293        if let Some(cmd) = self.commands.iter().find(|c| c.name == args[0]) {
294            matches.command = Some(args[0].clone());
295            matches.parse_command_args(cmd, &args[1..]);
296        } else {
297            matches.parse_args_list(&self.global_args, args);
298        }
299
300        // Apply defaults and validate
301        self.apply_defaults_and_validate(&mut matches);
302
303        matches
304    }
305
306    fn apply_defaults_and_validate(&self, matches: &mut Matches) {
307        for arg in &self.global_args {
308            // Apply default value if not provided
309            if !matches.is_present(&arg.name) && arg.default_value.is_some() {
310                matches.args.insert(
311                    arg.name.clone(),
312                    arg.default_value.clone(),
313                );
314            }
315
316            // Check required
317            if arg.required && !matches.is_present(&arg.name) {
318                let msg = if self.colored_help {
319                    format!("Error: {} is required", colors::colorize(&format!("--{}", arg.long), colors::RED))
320                } else {
321                    format!("Error: --{} is required", arg.long)
322                };
323                eprintln!("{}", msg);
324                std::process::exit(1);
325            }
326
327            // Validate possible values
328            if !arg.possible_values.is_empty() {
329                if let Some(value) = matches.value_of(&arg.name) {
330                    if !arg.possible_values.iter().any(|v| v == value) {
331                        let msg = if self.colored_help {
332                            format!(
333                                "Error: invalid value {} for {}",
334                                colors::colorize(&format!("'{}'", value), colors::RED),
335                                colors::colorize(&format!("--{}", arg.long), colors::CYAN)
336                            )
337                        } else {
338                            format!("Error: invalid value '{}' for --{}", value, arg.long)
339                        };
340                        eprintln!("{}", msg);
341                        eprintln!("Possible values: {}", arg.possible_values.join(", "));
342                        std::process::exit(1);
343                    }
344                }
345            }
346
347            // Execute custom validator if present
348            if let Some(validator) = &arg.validator {
349                if let Some(value) = matches.value_of(&arg.name) {
350                    if let Err(err) = validator(value) {
351                        let msg = if self.colored_help {
352                            format!(
353                                "Error: validation failed for {}: {}",
354                                colors::colorize(&format!("--{}", arg.long), colors::CYAN),
355                                colors::colorize(&err, colors::RED)
356                            )
357                        } else {
358                            format!("Error: validation failed for --{}: {}", arg.long, err)
359                        };
360                        eprintln!("{}", msg);
361                        std::process::exit(1);
362                    }
363                }
364            }
365        }
366
367        // Validate argument groups
368        for group in &self.groups {
369            let present_args: Vec<String> = group.args.iter()
370                .filter(|arg_name| matches.is_present(arg_name))
371                .map(|s| s.clone())
372                .collect();
373
374            // Check if required group has at least one arg
375            if group.required && present_args.is_empty() {
376                let msg = if self.colored_help {
377                    format!(
378                        "Error: at least one of {} is required",
379                        colors::colorize(&format!("[{}]", group.args.join(", ")), colors::YELLOW)
380                    )
381                } else {
382                    format!("Error: at least one of [{}] is required", group.args.join(", "))
383                };
384                eprintln!("{}", msg);
385                std::process::exit(1);
386            }
387
388            // Check mutual exclusion (only one arg allowed)
389            if !group.multiple && present_args.len() > 1 {
390                let msg = if self.colored_help {
391                    format!(
392                        "Error: arguments {} are mutually exclusive",
393                        colors::colorize(&present_args.join(", "), colors::RED)
394                    )
395                } else {
396                    format!("Error: arguments {} are mutually exclusive", present_args.join(", "))
397                };
398                eprintln!("{}", msg);
399                std::process::exit(1);
400            }
401        }
402    }
403
404    fn print_help(&self) {
405        let name = if self.colored_help {
406            colors::colorize(&self.name, colors::BOLD)
407        } else {
408            self.name.clone()
409        };
410        println!("{}", name);
411        
412        if !self.about.is_empty() {
413            println!("{}\n", self.about);
414        }
415        
416        let usage = if self.colored_help {
417            format!("{}: {} [OPTIONS] [COMMAND]", 
418                colors::colorize("Usage", colors::BOLD),
419                self.name.to_lowercase()
420            )
421        } else {
422            format!("Usage: {} [OPTIONS] [COMMAND]", self.name.to_lowercase())
423        };
424        println!("{}\n", usage);
425
426        if !self.commands.is_empty() {
427            let header = if self.colored_help {
428                colors::colorize("Commands:", colors::BOLD)
429            } else {
430                "Commands:".to_string()
431            };
432            println!("{}", header);
433            
434            for cmd in &self.commands {
435                let cmd_name = if self.colored_help {
436                    colors::colorize(&cmd.name, colors::CYAN)
437                } else {
438                    cmd.name.clone()
439                };
440                println!("  {:<12} {}", cmd_name, cmd.about);
441            }
442            println!();
443        }
444
445        let options_header = if self.colored_help {
446            colors::colorize("Options:", colors::BOLD)
447        } else {
448            "Options:".to_string()
449        };
450        println!("{}", options_header);
451        
452        let help_text = if self.colored_help {
453            format!("  {}, {}     Print help", 
454                colors::colorize("-h", colors::GREEN),
455                colors::colorize("--help", colors::GREEN)
456            )
457        } else {
458            "  -h, --help     Print help".to_string()
459        };
460        println!("{}", help_text);
461        
462        let version_text = if self.colored_help {
463            format!("  {}, {}  Print version",
464                colors::colorize("-V", colors::GREEN),
465                colors::colorize("--version", colors::GREEN)
466            )
467        } else {
468            "  -V, --version  Print version".to_string()
469        };
470        println!("{}", version_text);
471
472        for arg in &self.global_args {
473            let short = arg.short.as_ref().map(|s| format!("-{}, ", s)).unwrap_or_default();
474            let long_with_color = if self.colored_help {
475                colors::colorize(&format!("--{}", arg.long), colors::GREEN)
476            } else {
477                format!("--{}", arg.long)
478            };
479            
480            let required_marker = if arg.required && self.colored_help {
481                format!(" {}", colors::colorize("[required]", colors::RED))
482            } else if arg.required {
483                " [required]".to_string()
484            } else {
485                String::new()
486            };
487            
488            println!("  {}{:<12} {}{}", short, long_with_color, arg.help, required_marker);
489        }
490    }
491}
492
493/// Subcommand definition
494///
495/// Represents a distinct command with its own argument schema.
496/// Commands are parsed from the first positional argument.
497pub struct Command {
498    name: String,
499    about: String,
500    args: Vec<Arg>,
501}
502
503impl Command {
504    pub fn new(name: impl Into<String>) -> Self {
505        Self {
506            name: name.into(),
507            about: String::new(),
508            args: Vec::new(),
509        }
510    }
511
512    pub fn about(mut self, about: impl Into<String>) -> Self {
513        self.about = about.into();
514        self
515    }
516
517    pub fn arg(mut self, arg: Arg) -> Self {
518        self.args.push(arg);
519        self
520    }
521}
522
523/// Command-line argument specification
524///
525/// Defines a flag or option with optional short/long forms.
526/// Can be boolean (flag) or value-taking (option).
527pub struct Arg {
528    name: String,
529    long: String,
530    short: Option<String>,
531    help: String,
532    takes_value: bool,
533    required: bool,
534    default_value: Option<String>,
535    possible_values: Vec<String>,
536    validator: Option<Validator>,
537}
538
539impl Arg {
540    pub fn new(name: impl Into<String>) -> Self {
541        let name = name.into();
542        Self {
543            long: name.clone(),
544            name,
545            short: None,
546            help: String::new(),
547            takes_value: false,
548            required: false,
549            default_value: None,
550            possible_values: Vec::new(),
551            validator: None,
552        }
553    }
554
555    pub fn long(mut self, long: impl Into<String>) -> Self {
556        self.long = long.into();
557        self
558    }
559
560    pub fn short(mut self, short: char) -> Self {
561        self.short = Some(short.to_string());
562        self
563    }
564
565    pub fn help(mut self, help: impl Into<String>) -> Self {
566        self.help = help.into();
567        self
568    }
569
570    pub fn takes_value(mut self, takes: bool) -> Self {
571        self.takes_value = takes;
572        self
573    }
574
575    pub fn required(mut self, req: bool) -> Self {
576        self.required = req;
577        self
578    }
579
580    /// Set a default value for the argument
581    ///
582    /// # Example
583    /// ```no_run
584    /// # use avila_cli::Arg;
585    /// Arg::new("port")
586    ///     .takes_value(true)
587    ///     .default_value("8080");
588    /// ```
589    pub fn default_value(mut self, value: impl Into<String>) -> Self {
590        self.default_value = Some(value.into());
591        self
592    }
593
594    /// Restrict possible values
595    ///
596    /// # Example
597    /// ```no_run
598    /// # use avila_cli::Arg;
599    /// Arg::new("format")
600    ///     .takes_value(true)
601    ///     .possible_values(&["json", "yaml", "toml"]);
602    /// ```
603    pub fn possible_values(mut self, values: &[&str]) -> Self {
604        self.possible_values = values.iter().map(|s| s.to_string()).collect();
605        self
606    }
607
608    /// Add a custom validator function
609    ///
610    /// # Example
611    /// ```no_run
612    /// # use avila_cli::Arg;
613    /// Arg::new("port")
614    ///     .takes_value(true)
615    ///     .validator(|v| {
616    ///         v.parse::<u16>()
617    ///             .map(|_| ())
618    ///             .map_err(|_| "must be a valid port number".to_string())
619    ///     });
620    /// ```
621    pub fn validator(mut self, f: Validator) -> Self {
622        self.validator = Some(f);
623        self
624    }
625}
626
627/// Parse result containing matched arguments
628///
629/// Uses HashMap for O(1) argument lookups.
630/// Stores the active subcommand and all parsed argument values.
631pub struct Matches {
632    command: Option<String>,
633    args: HashMap<String, Option<String>>,
634    values: Vec<String>,
635}
636
637impl Matches {
638    pub fn subcommand(&self) -> Option<&str> {
639        self.command.as_deref()
640    }
641
642    pub fn is_present(&self, name: &str) -> bool {
643        self.args.contains_key(name)
644    }
645
646    pub fn value_of(&self, name: &str) -> Option<&str> {
647        self.args.get(name)?.as_deref()
648    }
649
650    pub fn values(&self) -> &[String] {
651        &self.values
652    }
653
654    /// Get parsed value as specific type
655    ///
656    /// # Example
657    /// ```no_run
658    /// # use avila_cli::*;
659    /// # fn main() {
660    /// # // Assume matches is already created from App::parse()
661    /// # }
662    /// ```
663    pub fn value_as<T>(&self, name: &str) -> Option<T>
664    where
665        T: std::str::FromStr,
666    {
667        self.value_of(name)?.parse().ok()
668    }
669
670    /// Check if any of the given argument names is present
671    ///
672    /// # Example
673    /// ```no_run
674    /// # use avila_cli::*;
675    /// # fn main() {
676    /// # // Assume matches is already created from App::parse()
677    /// # }
678    /// ```
679    pub fn any_present(&self, names: &[&str]) -> bool {
680        names.iter().any(|name| self.is_present(name))
681    }
682
683    /// Check if all of the given argument names are present
684    pub fn all_present(&self, names: &[&str]) -> bool {
685        names.iter().all(|name| self.is_present(name))
686    }
687
688    /// Get value or return a default
689    pub fn value_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
690        self.value_of(name).unwrap_or(default)
691    }
692
693    /// Get the number of positional arguments
694    pub fn values_count(&self) -> usize {
695        self.values.len()
696    }
697
698    fn parse_command_args(&mut self, cmd: &Command, args: &[String]) {
699        self.parse_args_list(&cmd.args, args);
700    }
701
702    fn parse_args_list(&mut self, arg_defs: &[Arg], args: &[String]) {
703        let mut i = 0;
704        while i < args.len() {
705            let arg = &args[i];
706
707            if arg.starts_with("--") {
708                let key = &arg[2..];
709                if let Some(arg_def) = arg_defs.iter().find(|a| a.long == key) {
710                    if arg_def.takes_value && i + 1 < args.len() {
711                        self.args.insert(arg_def.name.clone(), Some(args[i + 1].clone()));
712                        i += 2;
713                    } else {
714                        self.args.insert(arg_def.name.clone(), None);
715                        i += 1;
716                    }
717                } else {
718                    i += 1;
719                }
720            } else if arg.starts_with('-') && arg.len() == 2 {
721                let short = &arg[1..];
722                if let Some(arg_def) = arg_defs.iter().find(|a| a.short.as_deref() == Some(short)) {
723                    if arg_def.takes_value && i + 1 < args.len() {
724                        self.args.insert(arg_def.name.clone(), Some(args[i + 1].clone()));
725                        i += 2;
726                    } else {
727                        self.args.insert(arg_def.name.clone(), None);
728                        i += 1;
729                    }
730                } else {
731                    i += 1;
732                }
733            } else {
734                self.values.push(arg.clone());
735                i += 1;
736            }
737        }
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744
745    #[test]
746    fn test_arg_creation() {
747        let arg = Arg::new("test")
748            .long("test")
749            .short('t')
750            .help("Test argument")
751            .takes_value(true);
752
753        assert_eq!(arg.name, "test");
754        assert_eq!(arg.long, "test");
755        assert_eq!(arg.short, Some("t".to_string()));
756    }
757
758    #[test]
759    fn test_command_creation() {
760        let cmd = Command::new("test")
761            .about("Test command")
762            .arg(Arg::new("arg1"));
763
764        assert_eq!(cmd.name, "test");
765        assert_eq!(cmd.args.len(), 1);
766    }
767
768    #[test]
769    fn test_value_as_parsing() {
770        let mut matches = Matches {
771            command: None,
772            args: HashMap::new(),
773            values: Vec::new(),
774        };
775        matches.args.insert("port".to_string(), Some("8080".to_string()));
776
777        let port: u16 = matches.value_as("port").unwrap();
778        assert_eq!(port, 8080);
779    }
780
781    #[test]
782    fn test_any_present() {
783        let mut matches = Matches {
784            command: None,
785            args: HashMap::new(),
786            values: Vec::new(),
787        };
788        matches.args.insert("verbose".to_string(), None);
789
790        assert!(matches.any_present(&["verbose", "debug"]));
791        assert!(!matches.any_present(&["quiet", "silent"]));
792    }
793
794    #[test]
795    fn test_all_present() {
796        let mut matches = Matches {
797            command: None,
798            args: HashMap::new(),
799            values: Vec::new(),
800        };
801        matches.args.insert("verbose".to_string(), None);
802        matches.args.insert("debug".to_string(), None);
803
804        assert!(matches.all_present(&["verbose", "debug"]));
805        assert!(!matches.all_present(&["verbose", "debug", "trace"]));
806    }
807
808    #[test]
809    fn test_value_or_default() {
810        let matches = Matches {
811            command: None,
812            args: HashMap::new(),
813            values: Vec::new(),
814        };
815
816        assert_eq!(matches.value_or("port", "8080"), "8080");
817    }
818
819    #[test]
820    fn test_values_count() {
821        let matches = Matches {
822            command: None,
823            args: HashMap::new(),
824            values: vec!["file1".to_string(), "file2".to_string()],
825        };
826
827        assert_eq!(matches.values_count(), 2);
828    }
829}