ctflag/
lib.rs

1// Copyright 2019 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # Capture the Flag
16//!
17//! Capture the Flag is a command-line flag parsing library aimed at producing well
18//! documented command-line interfaces with minimal boiler-plate.
19//! Flags are defined on the command-line as key-value string pairs which are parsed
20//! according to their key name and associated type.
21//! Flags can have the form `--key=value` or `--key value`.  If the flag is of type
22//! `bool`, the flag can simply use `--key`, which implies `--key=true`.
23//! If specified, a flag can have a short form which begins with a single `-`.
24//!
25//! ## How to use
26//!
27//! Define a struct where each field represents a flag to parse.
28//! The parsing code is generated by deriving the trait [`ctflag::Flags`].
29//!
30//! ```
31//! # use ctflag::Flags;
32//! ##[derive(Flags)]
33//! struct MyFlags {
34//!     enable_floopy: bool,
35//!     output: Option<String>,
36//! }
37//! # fn main() {}
38//! ```
39//!
40//! Parsing the command-line arguments is done by calling the relevant methods of the
41//! [`ctflag::Flags`] trait.
42//!
43//! ```
44//! # use ctflag::Flags;
45//! # #[derive(Flags)]
46//! # struct MyFlags {
47//! #     enable_floopy: bool,
48//! #     output: Option<String>,
49//! # }
50//! # fn main() -> ctflag::Result<()> {
51//! let (flags, other_args) = MyFlags::from_args(std::env::args())?;
52//! # Ok(())
53//! # }
54//! ```
55//!
56//! A description of the flags, suitable for use in a help message, can be obtained
57//! by calling the [`ctflag::Flags::description()`] method.
58//!
59//! The behaviour of each flag can be changed using the `#[flag(...)]` attribute.
60//!
61//! - `desc = "..."`: Provides a description of the flag, displayed in the
62//!   help text by the [`ctflag::Flags::description()`] method.
63//! - `placeholder = "..."`: Provides the text that appears in place of the
64//!   flag's value in the help text. Defaults to "VALUE".
65//! - `default = ...`: For types other than `Optional`, provides a default
66//!   value if the flag is not set on the command-line. This only works with type
67//!   literals (bool, i64, str, etc.).
68//! - `short = '...'`: A short, single character alias for the flag name.
69//!
70//! ```
71//! # use ctflag::Flags;
72//! ##[derive(Flags)]
73//! struct MyFlags {
74//!     #[flag(desc = "The floopy floops the whoop")]
75//!     enable_floopy: bool,
76//!
77//!     #[flag(short = 'o', desc = "Output file", placeholder = "PATH")]
78//!     output: Option<String>,
79//!
80//!     #[flag(
81//!         desc = "How many slomps to include",
82//!         placeholder = "INTEGER",
83//!         default = 34
84//!     )]
85//!     slomps: i64,
86//! }
87//! # fn main() {}
88//! ```
89//!
90//! The type of each field must implement the [`ctflag::FromArg`] trait.  A blanket
91//! implementation of this trait exists for any type implementing the `FromStr` trait.
92//!
93//! ```
94//! # use ctflag::{Flags, FromArg, FromArgError, FromArgResult};
95//! // A custom type.
96//! enum Fruit {
97//!     Apple,
98//!     Orange,
99//! }
100//!
101//! impl FromArg for Fruit {
102//!     fn from_arg(s: &str) -> FromArgResult<Self> {
103//!         match s {
104//!             "apple" => Ok(Fruit::Apple),
105//!             "orange" => Ok(Fruit::Orange),
106//!             _ => Err(FromArgError::with_message("bad fruit")),
107//!         }
108//!     }
109//! }
110//!
111//! impl Default for Fruit {
112//!   fn default() -> Self {
113//!       Fruit::Apple
114//!   }
115//! }
116//!
117//! ##[derive(Flags)]
118//! struct MyFlags {
119//!     fruit: Fruit,
120//! }
121//! # fn main() {}
122//! ```
123//!
124//! [`ctflag::Flags`]: trait.Flags.html
125//! [`ctflag::FromArg`]: trait.FromArg.html
126//! [`ctflag::Flags::description()`]: trait.Flags.html#tymethod.description
127
128use std::fmt;
129use std::str::FromStr;
130
131// Define the required shared macros first. Definition order is
132// important for macros.
133#[macro_use]
134mod macros;
135
136// Users of this library shouldn't need to know that the derive functionality
137// is in a different crate.
138#[doc(hidden)]
139pub use ctflag_derive::*;
140
141// Public so that generated code outside of the crate can make use of it.
142#[doc(hidden)]
143pub mod internal;
144
145#[derive(Clone, Debug)]
146pub enum FlagError {
147    ParseError(ParseErrorStruct),
148    MissingValue(String),
149    UnrecognizedArg(String),
150}
151
152#[derive(Clone, Debug)]
153pub struct ParseErrorStruct {
154    pub type_str: &'static str,
155    pub input: String,
156    pub src: FromArgError,
157}
158
159pub type Result<T> = std::result::Result<T, FlagError>;
160
161/// Provides a command-line argument parsing implementation when derived
162/// for a named-struct.
163///
164/// ```
165/// # use ctflag::Flags;
166/// ##[derive(Flags)]
167/// struct MyFlags {
168///     enable_floopy: bool,
169///     // ...
170/// }
171/// # fn main() {}
172///  ```
173///
174/// Each field in the struct must implement the [`ctflag::FromArgs`] trait.
175///
176/// # Default values
177///
178/// A flag's default value can be specified using the `#[flag(default = ...)]` attribute.
179/// This attribute only allows the default value to be a type literal.  For more
180/// control over the default value, using an `Option` type is preferred.
181/// If this attribute is not set, the type must implement the `Default` trait.
182///
183/// Implementing this trait by hand is not recommended and would defeat
184/// the purpose of this crate.
185///
186/// [`ctflag::FromArgs`]: trait.FromArgs.html
187pub trait Flags: Sized {
188    /// Consumes the command-line arguments and returns a tuple containing
189    /// the value of this type, and a list of command-line
190    /// arguments that were not consumed.  These arguments typically include
191    /// the first command-line argument (usually the program name) and any
192    /// non-flag arguments (no `-` prefix).
193    ///
194    /// # Example
195    ///
196    /// ```
197    /// # use ctflag::Flags;
198    /// ##[derive(Flags)]
199    /// struct MyFlags {
200    ///     enable_floopy: bool,
201    ///     // ...
202    /// }
203    ///
204    /// # fn main() -> ctflag::Result<()> {
205    /// let (flags, args) = MyFlags::from_args(std::env::args())?;
206    /// if flags.enable_floopy {
207    ///     // ...
208    /// }
209    /// # Ok(())
210    /// # }
211    /// ```
212    fn from_args<T>(args: T) -> Result<(Self, Vec<String>)>
213    where
214        T: IntoIterator<Item = String>;
215
216    /// Returns a String that describes the flags defined in the struct
217    /// implementing this trait.
218    ///
219    /// For a struct like:
220    ///
221    /// ```
222    /// # use ctflag::Flags;
223    /// ##[derive(Flags)]
224    /// struct MyFlags {
225    ///     #[flag(desc = "The floopy floops the whoop")]
226    ///     enable_floopy: bool,
227    ///
228    ///     #[flag(short = 'o', desc = "Output file", placeholder = "PATH")]
229    ///     output: Option<String>,
230    ///
231    ///     #[flag(
232    ///         desc = "How many slomps to include",
233    ///         placeholder = "INTEGER",
234    ///         default = 34
235    ///     )]
236    ///     slomps: i64,
237    /// }
238    /// # fn main() {}
239    /// ```
240    ///
241    /// The returned String looks something like:
242    ///
243    /// ```text
244    /// OPTIONS:
245    ///   --enable_floopy        The floopy floops the whoop
246    ///   -o, --output [PATH]    Output file
247    ///   --slomps INTEGER       How many slomps to include (defaults to 34)
248    /// ```
249    fn description() -> String;
250}
251
252#[derive(Clone, Debug)]
253pub struct FromArgError {
254    msg: Option<String>,
255}
256
257pub type FromArgResult<T> = std::result::Result<T, FromArgError>;
258
259/// Any type declared in a struct that derives [`ctflag::Flags`] must implement
260/// this trait.  A blanket implementation exists for types implementing `FromStr`.
261/// Custom types can implement this trait directly.
262///
263/// [`ctflag::Flags`]: trait.Flags.html
264///
265/// ```
266/// # use ctflag::{Flags, FromArg, FromArgError, FromArgResult};
267/// enum Fruit {
268///     Apple,
269///     Orange,
270/// }
271///
272/// impl FromArg for Fruit {
273///     fn from_arg(s: &str) -> FromArgResult<Self> {
274///         match s {
275///             "apple" => Ok(Fruit::Apple),
276///             "orange" => Ok(Fruit::Orange),
277///             _ => Err(FromArgError::with_message("bad fruit")),
278///         }
279///     }
280/// }
281/// ```
282pub trait FromArg: Sized {
283    /// Parses a string `s` to return the value of this type.
284    ///
285    /// If parsing succeeds, return the value inside an `Ok`, otherwise
286    /// return an error using [`ctflag::FromArgError::with_message`] inside
287    /// an `Err`.
288    /// [`ctflag::FromArgError::with_message`]: struct.FromArgError.html#method.with_message
289    fn from_arg(value: &str) -> FromArgResult<Self>;
290}
291
292impl<T> FromArg for T
293where
294    T: FromStr,
295{
296    fn from_arg(s: &str) -> FromArgResult<T> {
297        <T as FromStr>::from_str(s).map_err(|_| FromArgError::new())
298    }
299}
300
301impl fmt::Display for FlagError {
302    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
303        match self {
304            FlagError::ParseError(err) => {
305                write!(
306                    f,
307                    "failed to parse \"{}\" as {} type",
308                    &err.input, err.type_str
309                )?;
310                if let Some(msg) = &err.src.msg {
311                    write!(f, ": {}", msg)?;
312                }
313            }
314            FlagError::MissingValue(arg) => {
315                write!(f, "missing value for argument \"{}\"", arg)?;
316            }
317            FlagError::UnrecognizedArg(arg) => {
318                write!(f, "unrecognized argument \"{}\"", arg)?;
319            }
320        }
321        Ok(())
322    }
323}
324
325impl FromArgError {
326    fn new() -> Self {
327        FromArgError { msg: None }
328    }
329
330    pub fn with_message<T>(msg: T) -> Self
331    where
332        T: fmt::Display,
333    {
334        FromArgError {
335            msg: Some(format!("{}", msg)),
336        }
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    // Since we are inside the ctflag crate, alias crate to ctflag.
344    use crate as ctflag;
345
346    #[derive(Flags)]
347    struct Simple {
348        one: String,
349        two: Option<String>,
350        three: bool,
351        four: Option<bool>,
352        five: i32,
353        six: Option<i32>,
354        seven: CustomType,
355    }
356
357    #[derive(Debug, PartialEq, Eq)]
358    struct CustomType(&'static str);
359
360    impl ctflag::FromArg for CustomType {
361        fn from_arg(value: &str) -> ctflag::FromArgResult<Self> {
362            match value {
363                "foo" => Ok(CustomType("foo")),
364                _ => {
365                    Err(ctflag::FromArgError::with_message("expected \"foo\""))
366                }
367            }
368        }
369    }
370
371    impl Default for CustomType {
372        fn default() -> Self {
373            CustomType("default")
374        }
375    }
376
377    #[test]
378    fn test_defaults() {
379        let args = vec![String::from("prog_name")];
380        let (flags, rest) = Simple::from_args(args).unwrap();
381        assert_eq!(flags.one, "");
382        assert_eq!(flags.two, None);
383        assert_eq!(flags.three, false);
384        assert_eq!(flags.four, None);
385        assert_eq!(flags.five, 0);
386        assert_eq!(flags.six, None);
387        assert_eq!(flags.seven, CustomType("default"));
388        assert_eq!(rest.len(), 1);
389        assert_eq!(rest[0], "prog_name");
390    }
391
392    #[test]
393    fn test_using_eq() {
394        let args = vec![String::from("prog_name"), String::from("--one=hello")];
395        let (flags, _rest) = Simple::from_args(args).unwrap();
396        assert_eq!(flags.one, "hello");
397    }
398
399    #[test]
400    fn test_using_space() {
401        let args = vec![
402            String::from("prog_name"),
403            String::from("--one"),
404            String::from("hello"),
405        ];
406        let (flags, _rest) = Simple::from_args(args).unwrap();
407        assert_eq!(flags.one, "hello");
408    }
409
410    #[test]
411    fn test_bool_using_eq() {
412        let args =
413            vec![String::from("prog_name"), String::from("--three=true")];
414        let (flags, _rest) = Simple::from_args(args).unwrap();
415        assert_eq!(flags.three, true);
416    }
417
418    #[test]
419    fn test_bool_using_space() {
420        let args = vec![
421            String::from("prog_name"),
422            String::from("--three"),
423            String::from("false"),
424        ];
425        let (flags, rest) = Simple::from_args(args).unwrap();
426        assert_eq!(flags.three, true);
427        assert_eq!(rest.len(), 2);
428        assert_eq!(rest[1], "false");
429    }
430
431    #[test]
432    fn test_option_using_eq() {
433        let args = vec![String::from("prog_name"), String::from("--two=hello")];
434        let (flags, _rest) = Simple::from_args(args).unwrap();
435        assert_eq!(flags.two, Some(String::from("hello")));
436    }
437
438    #[test]
439    fn test_option_using_space() {
440        let args = vec![
441            String::from("prog_name"),
442            String::from("--two"),
443            String::from("hello"),
444        ];
445        let (flags, _rest) = Simple::from_args(args).unwrap();
446        assert_eq!(flags.two, Some(String::from("hello")));
447    }
448
449    #[test]
450    fn test_custom_type_using_eq() {
451        let args = vec![String::from("prog_name"), String::from("--seven=foo")];
452        let (flags, _rest) = Simple::from_args(args).unwrap();
453        assert_eq!(flags.seven, CustomType("foo"));
454    }
455
456    #[test]
457    fn test_custom_type_using_space() {
458        let args = vec![
459            String::from("prog_name"),
460            String::from("--seven"),
461            String::from("foo"),
462        ];
463        let (flags, _rest) = Simple::from_args(args).unwrap();
464        assert_eq!(flags.seven, CustomType("foo"));
465    }
466
467    #[test]
468    fn test_int() {
469        let args = vec![
470            String::from("prog_name"),
471            String::from("--five=23"),
472            String::from("--six=42"),
473        ];
474        let (flags, _rest) = Simple::from_args(args).unwrap();
475        assert_eq!(flags.five, 23);
476        assert_eq!(flags.six, Some(42));
477    }
478
479    #[test]
480    fn test_missing_value() {
481        let args = vec![String::from("prog_name"), String::from("--one")];
482        assert_matches!(
483            Simple::from_args(args),
484            Err(ctflag::FlagError::MissingValue(_))
485        );
486    }
487
488    #[test]
489    fn test_bad_int() {
490        let args =
491            vec![String::from("prog_name"), String::from("--five=hello")];
492        assert_matches!(
493            Simple::from_args(args),
494            Err(ctflag::FlagError::ParseError(_))
495        );
496    }
497
498    #[test]
499    fn test_bad_bool() {
500        let args = vec![String::from("prog_name"), String::from("--three=yes")];
501        assert_matches!(
502            Simple::from_args(args),
503            Err(ctflag::FlagError::ParseError(_))
504        );
505    }
506
507    #[derive(Flags)]
508    struct DefaultFlags {
509        #[flag(default = 12)]
510        one: i32,
511
512        #[flag(default = "foo")]
513        two: String,
514
515        #[flag(default = "foo")]
516        three: CustomType,
517
518        #[flag(default = "bar")]
519        four: NoDefaultCustomType,
520    }
521
522    #[derive(Debug, PartialEq, Eq)]
523    struct NoDefaultCustomType(&'static str);
524
525    impl ctflag::FromArg for NoDefaultCustomType {
526        fn from_arg(value: &str) -> ctflag::FromArgResult<Self> {
527            match value {
528                "bar" => Ok(NoDefaultCustomType("bar")),
529                _ => {
530                    Err(ctflag::FromArgError::with_message("expected \"bar\""))
531                }
532            }
533        }
534    }
535
536    #[test]
537    fn test_custom_defaults() {
538        let args = vec![String::from("prog_name")];
539        let (flags, _rest) = DefaultFlags::from_args(args).unwrap();
540        assert_eq!(flags.one, 12);
541        assert_eq!(flags.two, "foo");
542        assert_eq!(flags.three, CustomType("foo"));
543        assert_eq!(flags.four, NoDefaultCustomType("bar"));
544    }
545
546    #[allow(dead_code)]
547    #[derive(Flags)]
548    struct BadDefault {
549        #[flag(default = "bad")]
550        one: CustomType,
551    }
552
553    #[test]
554    #[should_panic]
555    fn test_bad_default() {
556        let args = vec![String::from("prog_name")];
557        let _result = BadDefault::from_args(args);
558    }
559
560    #[allow(dead_code)]
561    #[derive(Flags)]
562    struct Description {
563        #[flag(desc = "Howdy", default = "foo", placeholder = "THING")]
564        one: String,
565
566        #[flag(short = 't', desc = "Boom", placeholder = "VROOM")]
567        two: Option<i32>,
568    }
569
570    #[test]
571    fn test_description() {
572        let desc: String = Description::description();
573        assert!(
574            desc.contains("    --one THING      Howdy (defaults to \"foo\")")
575        );
576        assert!(desc.contains("-t, --two [VROOM]    Boom"));
577    }
578
579    #[derive(Flags)]
580    struct ShortFlag {
581        #[flag(short = 'o')]
582        output: String,
583    }
584
585    #[test]
586    fn test_short_name_using_eq() {
587        let args = vec![String::from("prog_name"), String::from("-o=file")];
588        let (flags, _rest) = ShortFlag::from_args(args).unwrap();
589        assert_eq!(flags.output, "file");
590    }
591
592    #[test]
593    fn test_short_name_using_space() {
594        let args = vec![
595            String::from("prog_name"),
596            String::from("-o"),
597            String::from("file"),
598        ];
599        let (flags, _rest) = ShortFlag::from_args(args).unwrap();
600        assert_eq!(flags.output, "file");
601    }
602}