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}