gong/
options.rs

1// Copyright 2017 Lyndon Brown
2//
3// This file is part of the `gong` command-line argument processing library.
4//
5// Licensed under the MIT license or the Apache license (version 2.0), at your option. You may not
6// copy, modify, or distribute this file except in compliance with said license. You can find copies
7// of these licenses either in the LICENSE-MIT and LICENSE-APACHE files, or alternatively at
8// <http://opensource.org/licenses/MIT> and <http://www.apache.org/licenses/LICENSE-2.0>
9// respectively.
10
11//! “Available” option sets
12
13use std::convert::AsRef;
14
15#[deprecated(since = "1.2.0", note = "Use either `OptionSet` or `OptionSetEx` now, as applicable")]
16pub type Options<'a> = OptionSetEx<'a>;
17
18/// Default abbreviation support state
19pub(crate) const ABBR_SUP_DEFAULT: bool = true;
20/// Default mode
21pub(crate) const MODE_DEFAULT: OptionsMode = OptionsMode::Standard;
22
23/// Extendible option set
24///
25/// Used to supply the set of information about available options to match against
26///
27/// This is the "extendible" variant which uses `Vec`s to hold the option lists and thus is flexible
28/// in allowing addition of options, and may re-allocate as necessary.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct OptionSetEx<'a> {
31    /* NOTE: these have been left public to allow creation via macros */
32    pub long: Vec<LongOption<'a>>,
33    pub short: Vec<ShortOption>,
34    pub mode: OptionsMode,
35    pub allow_abbreviations: bool,
36}
37
38impl<'a> Default for OptionSetEx<'a> {
39    fn default() -> Self {
40        OptionSetEx::new(0, 0)
41    }
42}
43
44/// Option set
45///
46/// Used to supply the set of information about available options to match against
47///
48/// This is the non-“extendible” variant. Unlike its cousin `OptionSetEx`, this holds options lists
49/// as slice references rather than `Vec`s, and thus cannot be extended in size (hence no `add_*`
50/// methods). This is particularly useful in efficient creation of static/const option sets.
51#[derive(Default, Debug, Clone, PartialEq, Eq)]
52pub struct OptionSet<'r, 'a: 'r> {
53    /* NOTE: these have been left public to allow efficient static creation of options */
54    pub long: &'r [LongOption<'a>],
55    pub short: &'r [ShortOption],
56    pub mode: OptionsMode,
57    pub allow_abbreviations: bool,
58}
59
60impl<'r, 'a: 'r> PartialEq<OptionSet<'r, 'a>> for OptionSetEx<'a> {
61    fn eq(&self, rhs: &OptionSet<'r, 'a>) -> bool {
62        rhs.eq(&self.as_fixed())
63    }
64}
65
66impl<'r, 'a: 'r> PartialEq<OptionSetEx<'a>> for OptionSet<'r, 'a> {
67    fn eq(&self, rhs: &OptionSetEx<'a>) -> bool {
68        self.eq(&rhs.as_fixed())
69    }
70}
71
72/// Used to assert which option processing mode to use
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum OptionsMode {
75    /// Standard (default): Short (`-o`) and long (`--foo`) options, with single and double dash
76    /// prefixes respectively.
77    Standard,
78    /// Alternate: Long options only, with single dash prefix.
79    Alternate,
80}
81
82impl Default for OptionsMode {
83    fn default() -> Self {
84        MODE_DEFAULT
85    }
86}
87
88/// Description of an available long option
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct LongOption<'a> {
91    /* NOTE: these have been left public to allow efficient static creation of options */
92    /// Long option name, excluding the `--` prefix
93    pub name: &'a str,
94    /// Whether option expects a data argument
95    pub expects_data: bool,
96}
97
98/// Description of an available short option
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct ShortOption {
101    /* NOTE: these have been left public to allow efficient static creation of options */
102    /// Short option character
103    pub ch: char,
104    /// Whether option expects a data argument
105    pub expects_data: bool,
106}
107
108/// Description of a validation issue within an option in an [`OptionSet`](struct.OptionSet.html) or
109/// [`OptionSetEx`](struct.OptionSetEx.html) set.
110#[derive(Debug, PartialEq, Eq)]
111pub enum OptionFlaw<'a> {
112    /// Long option name is empty string
113    LongEmpty,
114    /// Long option name contains equals
115    LongIncludesEquals(&'a str),
116    /// Short option char is dash (`-`)
117    ShortDash,
118    /// Duplicate short option found
119    ShortDup(char),
120    /// Duplicate long option found
121    LongDup(&'a str),
122}
123
124impl<'a> OptionSetEx<'a> {
125    /// Create a new object. Takes estimations of the number of options to expect to be added (for
126    /// efficient vector allocation).
127    pub fn new(count_long: usize, count_short: usize) -> Self {
128        Self {
129            long: Vec::with_capacity(count_long),
130            short: Vec::with_capacity(count_short),
131            mode: MODE_DEFAULT,
132            allow_abbreviations: ABBR_SUP_DEFAULT,
133        }
134    }
135
136    /// Create an [`OptionSet`](struct.OptionSet.html) referencing `self`’s vectors as slices.
137    pub fn as_fixed(&self) -> OptionSet<'_, 'a> {
138        OptionSet {
139            long: &self.long[..],
140            short: &self.short[..],
141            mode: self.mode,
142            allow_abbreviations: self.allow_abbreviations,
143        }
144    }
145
146    /// Set mode
147    pub fn set_mode(&mut self, mode: OptionsMode) -> &mut Self {
148        self.mode = mode;
149        self
150    }
151
152    /// Enable/disable abbreviated matching
153    pub fn set_allow_abbreviations(&mut self, allow: bool) -> &mut Self {
154        self.allow_abbreviations = allow;
155        self
156    }
157
158    /// Checks if empty
159    pub fn is_empty(&self) -> bool {
160        self.long.is_empty() && self.short.is_empty()
161    }
162
163    /// Add a long option
164    ///
165    /// Panics (debug only) on invalid name.
166    pub fn add_long(&mut self, name: &'a str) -> &mut Self {
167        self.long.push(LongOption::new(name, false));
168        self
169    }
170
171    /// Add a short option
172    ///
173    /// Panics (debug only) on invalid `char` choice.
174    pub fn add_short(&mut self, ch: char) -> &mut Self {
175        self.short.push(ShortOption::new(ch, false));
176        self
177    }
178
179    /// Add a long option that expects data
180    ///
181    /// Panics (debug only) on invalid name.
182    pub fn add_long_data(&mut self, name: &'a str) -> &mut Self {
183        self.long.push(LongOption::new(name, true));
184        self
185    }
186
187    /// Add a short option that expects data
188    ///
189    /// Panics (debug only) on invalid `char` choice.
190    pub fn add_short_data(&mut self, ch: char) -> &mut Self {
191        self.short.push(ShortOption::new(ch, true));
192        self
193    }
194
195    /// Add an existing (ready-made) long option
196    pub fn add_existing_long(&mut self, long: LongOption<'a>) -> &mut Self {
197        self.long.push(long);
198        self
199    }
200
201    /// Add an existing (ready-made) short option
202    pub fn add_existing_short(&mut self, short: ShortOption) -> &mut Self {
203        self.short.push(short);
204        self
205    }
206
207    /// Checks validity of option set
208    ///
209    /// Returns `true` if valid.
210    ///
211    /// See also the [`validate`](#method.validate) method.
212    #[inline(always)]
213    pub fn is_valid(&self) -> bool {
214        validation::validate_set(&self.as_fixed(), false).is_ok()
215    }
216
217    /// Checks validity of option set, returning details of any problems
218    #[inline(always)]
219    pub fn validate(&self) -> Result<(), Vec<OptionFlaw<'a>>> {
220        validation::validate_set(&self.as_fixed(), true)
221    }
222
223    /// Analyses provided program arguments.
224    ///
225    /// Returns a result set describing the result of the analysis. This may include `&str`
226    /// references to strings provided in the `args` parameter and in `self`. Take note of this with
227    /// respect to object lifetimes.
228    ///
229    /// Expects `self` to be valid (see [`is_valid`](#method.is_valid)).
230    pub fn process<T>(&self, args: &'a [T]) -> super::analysis::Analysis<'a>
231        where T: AsRef<str>
232    {
233        super::engine::process(args, &self.as_fixed())
234    }
235}
236
237impl<'r, 'a: 'r> OptionSet<'r, 'a> {
238    /// Creates an “extendible” copy of `self`
239    ///
240    /// This duplicates the options in `self` into an [`OptionSetEx`](struct.OptionSetEx.html).
241    pub fn to_extendible(&self) -> OptionSetEx<'a> {
242        OptionSetEx {
243            long: self.long.iter().cloned().collect(),
244            short: self.short.iter().cloned().collect(),
245            mode: self.mode,
246            allow_abbreviations: self.allow_abbreviations,
247        }
248    }
249
250    /// Set mode
251    pub fn set_mode(&mut self, mode: OptionsMode) -> &mut Self {
252        self.mode = mode;
253        self
254    }
255
256    /// Enable/disable abbreviated matching
257    pub fn set_allow_abbreviations(&mut self, allow: bool) -> &mut Self {
258        self.allow_abbreviations = allow;
259        self
260    }
261
262    /// Checks if empty
263    pub fn is_empty(&self) -> bool {
264        self.long.is_empty() && self.short.is_empty()
265    }
266
267    /// Checks validity of option set
268    ///
269    /// Returns `true` if valid.
270    ///
271    /// See also the [`validate`](#method.validate) method.
272    #[inline(always)]
273    pub fn is_valid(&self) -> bool {
274        validation::validate_set(self, false).is_ok()
275    }
276
277    /// Checks validity of option set, returning details of any problems
278    #[inline(always)]
279    pub fn validate(&'r self) -> Result<(), Vec<OptionFlaw<'a>>> {
280        validation::validate_set(self, true)
281    }
282
283    /// Analyses provided program arguments.
284    ///
285    /// Returns a result set describing the result of the analysis. This may include `&str`
286    /// references to strings provided in the `args` parameter and in `self`. Take note of this with
287    /// respect to object lifetimes.
288    ///
289    /// Expects `self` to be valid (see [`is_valid`](#method.is_valid)).
290    pub fn process<T>(&self, args: &'a [T]) -> super::analysis::Analysis<'a>
291        where T: AsRef<str>
292    {
293        super::engine::process(args, self)
294    }
295}
296
297impl<'a> LongOption<'a> {
298    /// Create a new long option descriptor
299    ///
300    /// Panics (debug only) if the given name contains an `=` or is an empty string.
301    fn new(name: &'a str, expects_data: bool) -> Self {
302        debug_assert!(!name.is_empty(), "Long option name cannot be an empty string!");
303        debug_assert!(!name.contains('='), "Long option name cannot contain ‘=’!");
304        Self { name, expects_data, }
305    }
306}
307
308impl ShortOption {
309    /// Create a new short option descriptor
310    ///
311    /// Panics (debug only) if the given char is `-`.
312    fn new(ch: char, expects_data: bool) -> Self {
313        debug_assert_ne!('-', ch, "Dash (‘-’) is not a valid short option!");
314        Self { ch, expects_data, }
315    }
316}
317
318/// Option set validation
319mod validation {
320    use super::{OptionSet, OptionFlaw};
321
322    /// Checks validity of option set, returning details of any problems
323    ///
324    /// If `detail` is `false`, it returns early on encountering a problem (with an empty `Vec`),
325    /// useful for quick `is_valid` checks. Otherwise builds up a complete list of flaws.
326    pub fn validate_set<'r, 'a: 'r>(set: &OptionSet<'r, 'a>, detail: bool
327        ) -> Result<(), Vec<OptionFlaw<'a>>>
328    {
329        let mut flaws = Vec::new();
330
331        for candidate in set.long {
332            if candidate.name.is_empty() {
333                match detail {
334                    true => { flaws.push(OptionFlaw::LongEmpty); },
335                    false => { return Err(flaws); },
336                }
337            }
338            else if candidate.name.contains('=') {
339                match detail {
340                    true => { flaws.push(OptionFlaw::LongIncludesEquals(candidate.name)); },
341                    false => { return Err(flaws); },
342                }
343            }
344        }
345
346        for candidate in set.short {
347            if candidate.ch == '-' {
348                match detail {
349                    true => { flaws.push(OptionFlaw::ShortDash); },
350                    false => { return Err(flaws); },
351                }
352            }
353        }
354
355        let mut dupes: bool = false;
356        find_duplicates_short(set, &mut flaws, detail, &mut dupes);
357        if !detail && dupes {
358            return Err(flaws);
359        }
360        find_duplicates_long(set, &mut flaws, detail, &mut dupes);
361        if !detail && dupes {
362            return Err(flaws);
363        }
364
365        match flaws.is_empty() {
366            true => Ok(()),
367            false => Err(flaws),
368        }
369    }
370
371    fn find_duplicates_short<'r, 'a: 'r>(set: &OptionSet<'r, 'a>, flaws: &mut Vec<OptionFlaw<'a>>,
372        detail: bool, found: &mut bool)
373    {
374        let opts = set.short;
375        let mut checked: Vec<char> = Vec::with_capacity(opts.len());
376
377        let mut duplicates = Vec::new();
378        for short in opts {
379            let ch = short.ch;
380            if !duplicates.contains(&OptionFlaw::ShortDup(ch)) {
381                match checked.contains(&ch) {
382                    true => {
383                        match detail {
384                            true => { duplicates.push(OptionFlaw::ShortDup(ch)); },
385                            false => { *found = true; return; },
386                        }
387                    },
388                    false => { checked.push(ch); },
389                }
390            }
391        }
392        if !duplicates.is_empty() {
393            flaws.append(&mut duplicates);
394        }
395    }
396
397    fn find_duplicates_long<'r, 'a: 'r>(set: &OptionSet<'r, 'a>, flaws: &mut Vec<OptionFlaw<'a>>,
398        detail: bool, found: &mut bool)
399    {
400        let opts = set.long;
401        let mut checked: Vec<&'a str> = Vec::with_capacity(opts.len());
402
403        let mut duplicates = Vec::new();
404        for long in opts {
405            let name = long.name.clone();
406            if !duplicates.contains(&OptionFlaw::LongDup(name)) {
407                match checked.contains(&name) {
408                    true => {
409                        match detail {
410                            true => { duplicates.push(OptionFlaw::LongDup(name)); },
411                            false => { *found = true; return; },
412                        }
413                    },
414                    false => { checked.push(name); },
415                }
416            }
417        }
418        if !duplicates.is_empty() {
419            flaws.append(&mut duplicates);
420        }
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    /* Dash (`-`) is an invalid short option (clashes with early terminator if it were given on its
429     * own (`--`), and would be misinterpreted as a long option if given as the first in a short
430     * option set (`--abc`)). */
431
432    /// Check `ShortOption::new` rejects ‘-’
433    #[test]
434    #[cfg_attr(debug_assertions, should_panic)]
435    fn create_short_dash() {
436        let _opt = ShortOption::new('-', false); // Should panic here in debug mode!
437    }
438
439    /// Check `LongOption::new` rejects empty string
440    #[test]
441    #[cfg_attr(debug_assertions, should_panic)]
442    fn create_long_no_name() {
443        let _opt = LongOption::new("", false); // Should panic here in debug mode!
444    }
445
446    /* Long option names cannot contain an `=` (used for declaring a data sub-argument in the same
447     * argument; if names could contain an `=`, as data can, we would not know where to do the
448     * split, complicating matching. */
449
450    /// Check `LongOption::new` rejects equals (`=`) char
451    #[test]
452    #[cfg_attr(debug_assertions, should_panic)]
453    fn create_long_with_equals() {
454        let _opt = LongOption::new("a=b", false); // Should panic here in debug mode!
455    }
456}