advanced/
advanced.rs

1//
2// Copyright (c) 2023,2024 Piotr Stolarz
3// GNU-like Command line options parser.
4//
5// Distributed under the 2-clause BSD License (the License)
6// see accompanying file LICENSE for details.
7//
8// This software is distributed WITHOUT ANY WARRANTY; without even the
9// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10// See the License for more information.
11//
12
13use cmdopts::{parse_opts, CmdOpt, InfoCode, ParseError, ProcessCode};
14
15fn print_usage() {
16    eprintln!(
17        "Usage: {} [OPTION]... [--] FILE1 [FILE2]",
18        std::env::args().next().unwrap()
19    );
20    println!(
21        r"
22Available options:
23-a, --long_a        No value option
24-b, --long_b=INT    Option with integer value, range [-10..10]
25-c, --long_c=STR    Option with string value
26-d                  Short option with string value
27    --long_e=STR    Long option with string value
28-h, --help          Print this help
29
30All options are optional. FILE1 is required, FILE2 is not.
31Use -- separator to avoid ambiguity if file name starts with '-'.
32"
33    )
34}
35
36#[derive(Debug)]
37struct Options {
38    a_opt: Option<()>,
39    b_opt: Option<i32>,
40    c_opt: Option<String>,
41    d_opt: Option<String>,
42    e_opt: Option<String>,
43    file1: Option<String>,
44    file2: Option<String>,
45    help: Option<()>,
46}
47
48impl Default for Options {
49    fn default() -> Self {
50        Options {
51            a_opt: None,
52            b_opt: None,
53            c_opt: None,
54            d_opt: None,
55            e_opt: None,
56            file1: None,
57            file2: None,
58            help: None,
59        }
60    }
61}
62
63#[allow(unused_imports)]
64fn process_cmdopts(opts: &mut Options) -> Result<(), ParseError> {
65    use std::cell::Cell;
66    use CmdOpt::*;
67    use InfoCode::*;
68    use ParseError::*;
69    use ProcessCode::*;
70
71    // processed option id
72    let opt_id = Cell::new('h');
73
74    // file parsing index (1-based), 0 for option parsing mode
75    let mut file_i = 0;
76
77    parse_opts(
78        |opt, constr| match opt {
79            &Short(c) => match c {
80                'a' | 'h' => {
81                    opt_id.set(c);
82                    NoValueOpt
83                }
84                'b' | 'c' | 'd' => {
85                    opt_id.set(c);
86                    ValueOpt
87                }
88                '-' => {
89                    constr.not_in_group();
90                    opt_id.set('-');
91                    NoValueOpt
92                }
93                _ => InfoCode::InvalidOpt,
94            },
95            Long(s) => match s.as_str() {
96                "long_a" => {
97                    opt_id.set('a');
98                    NoValueOpt
99                }
100                "long_b" => {
101                    opt_id.set('b');
102                    ValueOpt
103                }
104                "long_c" => {
105                    opt_id.set('c');
106                    ValueOpt
107                }
108                "long_e" => {
109                    opt_id.set('e');
110                    ValueOpt
111                }
112                "help" => {
113                    opt_id.set('h');
114                    NoValueOpt
115                }
116                _ => InfoCode::InvalidOpt,
117            },
118        },
119        |opt, val| {
120            // if true, mode needs to be switched at the return of the handler
121            let mut switch_mode = false;
122
123            // 1st standalone value switches the parser into files parsing mode
124            if file_i <= 0 && opt.is_none() {
125                file_i = 1;
126                switch_mode = true;
127            }
128
129            if file_i <= 0 {
130                //
131                // Options parser
132                //
133
134                // Options w/o associated value
135                //
136                let mut handled = true;
137
138                match opt_id.get() {
139                    // print help and exit
140                    'h' => {
141                        opts.help = Some(());
142                        return Ok(ProcessCode::Break);
143                    }
144                    '-' => {
145                        file_i = 1;
146                        return Ok(ProcessCode::ToggleParsingMode);
147                    }
148                    'a' => {
149                        opts.a_opt = Some(());
150                    }
151                    _ => {
152                        handled = false;
153                    }
154                }
155                if handled {
156                    return Ok(ProcessCode::Continue);
157                }
158
159                // Options w/associated string value
160                //
161                let val_str = &val.as_ref().unwrap().val;
162                handled = true;
163
164                match opt_id.get() {
165                    'c' => {
166                        opts.c_opt = Some(val_str.clone());
167                    }
168                    'd' => {
169                        opts.d_opt = Some(val_str.clone());
170                    }
171                    'e' => {
172                        opts.e_opt = Some(val_str.clone());
173                    }
174                    _ => {
175                        handled = false;
176                    }
177                }
178                if handled {
179                    return Ok(ProcessCode::Continue);
180                }
181
182                // Options w/associated int value
183                //
184                let opt_ref = opt.as_ref().unwrap();
185
186                let val_i: i32 = val_str.parse().map_err(|_| {
187                    ParseError::InvalidOpt(opt_ref.clone(), "Integer expected".to_string())
188                })?;
189
190                match opt_id.get() {
191                    'b' => {
192                        if val_i >= -10 && val_i <= 10 {
193                            opts.b_opt = Some(val_i);
194                        } else {
195                            return Err(ParseError::InvalidOpt(
196                                opt_ref.clone(),
197                                "Integer in range [-10..10] required".to_string(),
198                            ));
199                        }
200                    }
201                    _ => {}
202                }
203
204                Ok(ProcessCode::Continue)
205            } else {
206                //
207                // Files parser
208                //
209                let val_str = &val.as_ref().unwrap().val;
210
211                match file_i {
212                    1 => {
213                        opts.file1 = Some(val_str.clone());
214                    }
215                    2 => {
216                        opts.file2 = Some(val_str.clone());
217                    }
218                    _ => {
219                        return Err(ParseError::GenericErr(
220                            "Invalid number of files".to_string(),
221                        ));
222                    }
223                }
224                file_i += 1;
225
226                if switch_mode {
227                    Ok(ToggleParsingMode)
228                } else {
229                    Ok(Continue)
230                }
231            }
232        },
233    )
234}
235
236pub fn main() -> Result<(), i32> {
237    // CLI provided options
238    let mut opts: Options = Default::default();
239
240    let rc = process_cmdopts(&mut opts);
241    if rc.is_ok() {
242        if std::env::args().len() <= 1 || opts.help.is_some() {
243            print_usage();
244        } else if opts.file1.is_none() {
245            eprintln!("[ERROR] FILE1 required");
246            return Err(2);
247        } else {
248            println!("{:?}", opts);
249        }
250    } else {
251        eprintln!("[ERROR] {}", rc.unwrap_err());
252        return Err(1);
253    }
254    Ok(())
255}