argwerk/
helpers.rs

1//! Helper module for the macros.
2//!
3//! Unless something is specifically re-exported, all implementation details are
4//! expected to be private and might change between minor releases.
5
6use std::error;
7use std::ffi::{OsStr, OsString};
8use std::fmt;
9
10/// Default width to use when wrapping lines.
11///
12/// See [HelpFormat::width].
13const WIDTH: usize = 80;
14
15/// Default padding to use between switch summary and help text.
16///
17/// See [HelpFormat::padding].
18const PADDING: usize = 2;
19
20/// Default max usage width to use for switches and arguments.
21///
22/// See [HelpFormat::max_usage].
23const MAX_USAGE: usize = 24;
24
25/// A boxed error type.
26type BoxError = Box<dyn error::Error + Send + Sync + 'static>;
27
28/// Helper for converting a value into a result.
29///
30/// This is used to convert the value of a branch into a result.
31#[doc(hidden)]
32#[inline]
33pub fn into_result<T>(value: T) -> Result<(), BoxError>
34where
35    T: IntoResult,
36{
37    value.into_result()
38}
39
40#[doc(hidden)]
41pub trait IntoResult {
42    fn into_result(self) -> Result<(), BoxError>;
43}
44
45impl IntoResult for () {
46    #[inline]
47    fn into_result(self) -> Result<(), BoxError> {
48        Ok(())
49    }
50}
51
52impl<E> IntoResult for Result<(), E>
53where
54    BoxError: From<E>,
55{
56    #[inline]
57    fn into_result(self) -> Result<(), BoxError> {
58        Ok(self?)
59    }
60}
61
62/// Documentation over a single switch.
63pub struct Switch {
64    /// The usage summary of the switch.
65    ///
66    /// Like `--file, -f <path>`.
67    pub usage: &'static str,
68    /// Documentation comments associated with the switch.
69    pub docs: &'static [&'static str],
70}
71
72/// Helper that can be formatted into documentation text.
73pub struct Help {
74    /// The verbatim usage summary specified when invoking the macro.
75    pub usage: &'static str,
76    /// Documentation comments associated with the command.
77    pub docs: &'static [&'static str],
78    /// Switches associated with the command.
79    pub switches: &'static [Switch],
80}
81
82impl Help {
83    /// Format the help with the given config.
84    ///
85    /// # Examples
86    ///
87    /// ```rust
88    /// argwerk::define! {
89    ///     /// A simple test command.
90    ///     #[usage = "command [-h]"]
91    ///     struct Args {
92    ///         help: bool,
93    ///     }
94    ///     /// Prints the help.
95    ///     ///
96    ///     /// This includes:
97    ///     ///    * All the available switches.
98    ///     ///    * All the available positional arguments.
99    ///     ///    * Whatever else the developer decided to put in here! We even support wrapping comments which are overly long.
100    ///     ["-h" | "--help"] => {
101    ///         help = true;
102    ///     }
103    /// }
104    ///
105    /// # fn main() -> Result<(), argwerk::Error> {
106    /// let formatted = format!("{}", Args::help().format().width(120));
107    ///
108    /// assert!(formatted.split('\n').any(|line| line.len() > 80));
109    /// assert!(formatted.split('\n').all(|line| line.len() < 120));
110    /// # Ok(()) }
111    /// ```
112    pub fn format(&self) -> HelpFormat {
113        HelpFormat {
114            help: self,
115            width: WIDTH,
116            padding: PADDING,
117            max_usage: MAX_USAGE,
118        }
119    }
120}
121
122impl fmt::Display for Help {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        self.format().fmt(f)
125    }
126}
127
128/// A wrapper to format the help message with custom parameters.
129///
130/// Constructed through [Help::format].
131pub struct HelpFormat<'a> {
132    help: &'a Help,
133    width: usize,
134    padding: usize,
135    max_usage: usize,
136}
137
138impl HelpFormat<'_> {
139    /// Configure the width to use for help text.
140    pub fn width(self, width: usize) -> Self {
141        Self { width, ..self }
142    }
143
144    /// Configure the padding to use when formatting help.
145    ///
146    /// This determines the indentation of options and the distances between
147    /// options and help text.
148    pub fn padding(self, padding: usize) -> Self {
149        Self { padding, ..self }
150    }
151
152    /// Configure the max usage width to use when formatting help.
153    ///
154    /// This determines how wide a usage help is allowed to be before it forces
155    /// the associated documentation to flow to the next line.
156    ///
157    /// Usage help is the `--file, -f <path>` part of each switch and argument.
158    pub fn max_usage(self, max_usage: usize) -> Self {
159        Self { max_usage, ..self }
160    }
161}
162
163impl<'a> fmt::Display for HelpFormat<'a> {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        writeln!(f, "Usage: {name}", name = self.help.usage)?;
166
167        if !self.help.docs.is_empty() {
168            writeln!(f, "{}", TextWrap::new("", self.help.docs, self.width, 0))?;
169        }
170
171        writeln!(f)?;
172
173        let usage_len = self
174            .help
175            .switches
176            .iter()
177            .map(|s| {
178                usize::min(
179                    self.max_usage,
180                    if s.docs.is_empty() {
181                        s.usage.len()
182                    } else {
183                        s.usage.len() + self.padding
184                    },
185                )
186            })
187            .max()
188            .unwrap_or(self.max_usage);
189
190        if !self.help.switches.is_empty() {
191            writeln!(f, "Options:")?;
192            let mut first = true;
193
194            let mut it = self.help.switches.iter().peekable();
195
196            while let Some(d) = it.next() {
197                let first = std::mem::take(&mut first);
198                let more = it.peek().is_some();
199
200                let wrap = TextWrap {
201                    init: d.usage,
202                    docs: d.docs,
203                    width: self.width,
204                    padding: self.padding,
205                    init_len: Some(usage_len),
206                    first,
207                    more,
208                };
209
210                writeln!(f, "{}", wrap)?;
211            }
212        }
213
214        Ok(())
215    }
216}
217
218/// Helper to wrap documentation text.
219struct TextWrap<'a> {
220    init: &'a str,
221    docs: &'a [&'static str],
222    width: usize,
223    padding: usize,
224    /// The maximum init length permitted.
225    init_len: Option<usize>,
226    /// If this is the first element.
227    first: bool,
228    /// If there are more elements coming after this.
229    more: bool,
230}
231
232impl<'a> TextWrap<'a> {
233    fn new(init: &'a str, docs: &'a [&'static str], width: usize, padding: usize) -> Self {
234        Self {
235            init,
236            docs,
237            width,
238            padding,
239            init_len: None,
240            first: true,
241            more: false,
242        }
243    }
244
245    fn wrap(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        let mut it = self.docs.iter().peekable();
247
248        // No documentation lines.
249        if it.peek().is_none() {
250            fill_spaces(f, self.padding)?;
251            f.write_str(self.init)?;
252            return Ok(());
253        }
254
255        let init_len = self.init_len.unwrap_or(self.init.len());
256
257        let (long, mut init) = if self.init.len() + self.padding > init_len {
258            (true, None)
259        } else {
260            (false, Some(&self.init))
261        };
262
263        // NB: init line is broader than maximum permitted init length.
264        if long {
265            // If we're not the first line, add a newline to visually separate
266            // the line with the long usage.
267            if !self.first {
268                writeln!(f)?;
269            }
270
271            fill_spaces(f, self.padding)?;
272            writeln!(f, "{}", self.init)?;
273        }
274
275        let fill = init_len + self.padding;
276
277        let trim = it.peek().map(|line| chars_count(line, |c| c == ' '));
278
279        while let Some(line) = it.next() {
280            let mut line = *line;
281
282            // Trim the line by skipping the whitespace common to all lines..
283            if let Some(trim) = trim {
284                line = skip_chars(line, trim);
285
286                // Whole line was trimmed.
287                if line.is_empty() {
288                    writeln!(f)?;
289                    continue;
290                }
291            }
292
293            // Whitespace prefix currently in use.
294            let ws_fill = next_index(line, char::is_alphanumeric).unwrap_or_default();
295            let mut line_first = true;
296
297            loop {
298                let fill = if !std::mem::take(&mut line_first) {
299                    fill + ws_fill
300                } else {
301                    fill
302                };
303
304                let mut space_span = None;
305
306                loop {
307                    let c = space_span.map(|(_, e)| e).unwrap_or_default();
308
309                    let (start, leap) = match line[c..].find(' ') {
310                        Some(i) => {
311                            let leap = next_index(&line[c + i..], |c| c != ' ').unwrap_or(1);
312                            (c + i, leap)
313                        }
314                        None => {
315                            // if the line fits within the current target fill,
316                            // include all of it.
317                            if line.len() + fill <= self.width {
318                                space_span = None;
319                            }
320
321                            break;
322                        }
323                    };
324
325                    if start + fill > self.width {
326                        break;
327                    }
328
329                    space_span = Some((start, start + leap));
330                }
331
332                let init_len = if let Some(init) = init.take() {
333                    fill_spaces(f, self.padding)?;
334                    f.write_str(init)?;
335                    self.padding + init.len()
336                } else {
337                    0
338                };
339
340                fill_spaces(f, fill.saturating_sub(init_len))?;
341
342                if let Some((start, end)) = space_span {
343                    writeln!(f, "{}", &line[..start])?;
344                    line = &line[end..];
345                    continue;
346                }
347
348                f.write_str(line)?;
349                break;
350            }
351
352            if it.peek().is_some() {
353                writeln!(f)?;
354            }
355        }
356
357        // If we're not the first line, add a newline to visually separate the
358        // line with the long usage.
359        if long && !self.first && self.more {
360            writeln!(f)?;
361        }
362
363        return Ok(());
364
365        /// Get the next index that is alphanumeric.
366        fn next_index(s: &str, p: fn(char) -> bool) -> Option<usize> {
367            Some(s.char_indices().find(|&(_, c)| p(c))?.0)
368        }
369
370        /// Count the number of spaces in the string, and return the first index that is not a space.
371        fn chars_count(s: &str, p: fn(char) -> bool) -> usize {
372            s.chars().take_while(|c| p(*c)).count()
373        }
374
375        /// Skip the given number of characters.
376        fn skip_chars(s: &str, count: usize) -> &str {
377            let e = s
378                .char_indices()
379                .skip(count)
380                .map(|(i, _)| i)
381                .next()
382                .unwrap_or(s.len());
383
384            &s[e..]
385        }
386
387        fn fill_spaces(f: &mut fmt::Formatter<'_>, mut count: usize) -> fmt::Result {
388            // Static buffer for quicker whitespace filling.
389            static BUF: &str = "                                                                ";
390
391            while count > 0 {
392                f.write_str(&BUF[..usize::min(count, BUF.len())])?;
393                count = count.saturating_sub(BUF.len());
394            }
395
396            Ok(())
397        }
398    }
399}
400
401impl fmt::Display for TextWrap<'_> {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        self.wrap(f)
404    }
405}
406
407/// Helpers around an iterator.
408pub struct Input<I>
409where
410    I: Iterator,
411{
412    it: I,
413    buf: Option<I::Item>,
414}
415
416impl<I> Input<I>
417where
418    I: Iterator,
419{
420    /// Construct a new input wrapper.
421    pub fn new(it: I) -> Self {
422        Self { it, buf: None }
423    }
424}
425
426impl<I> Input<I>
427where
428    I: Iterator,
429    I::Item: TryIntoInput,
430{
431    /// Get the next item in the parser.
432    // XXX For now, shut up Clippy. Eventually,
433    // change the public interface or impl
434    // iterator.
435    #[allow(clippy::should_implement_trait)]
436    pub fn next(&mut self) -> Result<Option<String>, InputError> {
437        if let Some(item) = self.buf.take() {
438            return Ok(Some(item.try_into_string()?));
439        }
440
441        let item = match self.it.next() {
442            Some(item) => item,
443            None => return Ok(None),
444        };
445
446        Ok(Some(item.try_into_string()?))
447    }
448
449    /// Get the next os string from the input.
450    pub fn next_os(&mut self) -> Option<OsString> {
451        if let Some(item) = self.buf.take() {
452            return Some(item.into_os_string());
453        }
454
455        let item = match self.it.next() {
456            Some(item) => item,
457            None => return None,
458        };
459
460        Some(item.into_os_string())
461    }
462
463    /// Take the next argument unless it looks like a switch.
464    pub fn next_unless_switch(&mut self) -> Result<Option<String>, InputError> {
465        match self.peek() {
466            Some(s) if s.starts_with('-') => Ok(None),
467            _ => self.next(),
468        }
469    }
470
471    /// Take the next argument unless it looks like a switch.
472    pub fn next_unless_switch_os(&mut self) -> Option<OsString> {
473        match self.peek() {
474            Some(s) if s.starts_with('-') => None,
475            _ => self.next_os(),
476        }
477    }
478
479    /// Get the rest of available items.
480    pub fn rest(&mut self) -> Result<Vec<String>, InputError> {
481        let mut buf = Vec::new();
482
483        if let Some(item) = self.buf.take() {
484            buf.push(item.try_into_string()?);
485        }
486
487        for item in &mut self.it {
488            buf.push(item.try_into_string()?);
489        }
490
491        Ok(buf)
492    }
493
494    /// Get the rest of available items as raw strings.
495    pub fn rest_os(&mut self) -> Vec<OsString> {
496        let mut buf = Vec::new();
497
498        if let Some(item) = self.buf.take() {
499            buf.push(item.into_os_string());
500        }
501
502        for item in &mut self.it {
503            buf.push(item.into_os_string());
504        }
505
506        buf
507    }
508
509    fn peek(&mut self) -> Option<&str> {
510        if self.buf.is_none() {
511            self.buf = self.it.next();
512        }
513
514        let item = match self.buf.as_ref() {
515            Some(item) => item,
516            None => return None,
517        };
518
519        item.try_as_str().ok()
520    }
521}
522
523#[derive(Debug)]
524pub struct InputError(());
525
526impl fmt::Display for InputError {
527    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
528        write!(f, "encounted non-valid unicode in input")
529    }
530}
531
532impl error::Error for InputError {}
533
534/// Trait implemented by types that can be parsed to the `parse` function of an
535/// arguments structure.
536///
537/// This trait is sealed, so that it cannot be implemented outside of the
538/// argwerk crate.
539///
540/// See [define][crate::define] for how its used.
541pub trait TryIntoInput: self::internal::Sealed {
542    #[doc(hidden)]
543    fn try_as_str(&self) -> Result<&str, InputError>;
544
545    #[doc(hidden)]
546    fn try_into_string(self) -> Result<String, InputError>;
547
548    #[doc(hidden)]
549    fn into_os_string(self) -> OsString;
550}
551
552impl TryIntoInput for String {
553    #[inline]
554    fn try_as_str(&self) -> Result<&str, InputError> {
555        Ok(self.as_str())
556    }
557
558    #[inline]
559    fn try_into_string(self) -> Result<String, InputError> {
560        Ok(self)
561    }
562
563    #[inline]
564    fn into_os_string(self) -> OsString {
565        OsString::from(self)
566    }
567}
568
569impl TryIntoInput for &str {
570    #[inline]
571    fn try_as_str(&self) -> Result<&str, InputError> {
572        Ok(*self)
573    }
574
575    #[inline]
576    fn try_into_string(self) -> Result<String, InputError> {
577        Ok(self.to_owned())
578    }
579
580    #[inline]
581    fn into_os_string(self) -> OsString {
582        OsString::from(self)
583    }
584}
585
586impl TryIntoInput for OsString {
587    #[inline]
588    fn try_as_str(&self) -> Result<&str, InputError> {
589        self.to_str().ok_or(InputError(()))
590    }
591
592    #[inline]
593    fn try_into_string(self) -> Result<String, InputError> {
594        self.into_string().map_err(|_| InputError(()))
595    }
596
597    #[inline]
598    fn into_os_string(self) -> OsString {
599        self
600    }
601}
602
603impl TryIntoInput for &OsStr {
604    #[inline]
605    fn try_as_str(&self) -> Result<&str, InputError> {
606        self.to_str().ok_or(InputError(()))
607    }
608
609    #[inline]
610    fn try_into_string(self) -> Result<String, InputError> {
611        Ok(self.to_str().ok_or(InputError(()))?.to_owned())
612    }
613
614    #[inline]
615    fn into_os_string(self) -> OsString {
616        self.to_owned()
617    }
618}
619
620mod internal {
621    pub trait Sealed {}
622
623    impl<T> Sealed for T where T: super::TryIntoInput {}
624}