Repository: pacak/bpaf
Pull request #182 (2023-03-19 12:38:33)

Release 0.7.10
- better error messages
- allow to (explicitly) derive positional bool
- better help messages for multiarg arguments
- syn 2.0
- derive for `anywhere` and `boxed`


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index e7b65e2..ad7ee8a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bpaf"
-version = "0.7.9"
+version = "0.7.10"
 edition = "2021"
 categories = ["command-line-interface"]
 description = "A simple Command Line Argument Parser with parser combinators"
@@ -14,7 +14,7 @@ exclude = [".github/workflows", "tarp.sh"]
 
 
 [dependencies]
-bpaf_derive = { path = "./bpaf_derive", version = "=0.3.4", optional = true }
+bpaf_derive = { path = "./bpaf_derive", version = "=0.3.5", optional = true }
 owo-colors = { version = "3.5.0", features = ["supports-colors"], optional = true }
 roff = { version = "0.2.1", optional = true }
 
diff --git a/Changelog.md b/Changelog.md
index 05d4f23..01e5184 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,10 +1,12 @@
 # Change Log
 
-## bpaf [0.7.10], bpaf_derive [0.3.5] - unreleased
+## bpaf [0.7.10], bpaf_derive [0.3.5] - 2013-03-19
 - improve error messages for typos like `-llvm` instead of `--llvm`
 - improve error messages when a flag is accepted by a command but not directly
 - allow to derive position bool
 - derive anywhere and boxed
+- dynamic layout for --help messages
+- bump syn to 2.0
 
 ## bpaf [0.7.9], bpaf_derive [0.3.4] - 2023-02-14
 - `ParseFailure::exit_code`
diff --git a/bpaf_derive/Cargo.toml b/bpaf_derive/Cargo.toml
index bbed7f7..ee90820 100644
--- a/bpaf_derive/Cargo.toml
+++ b/bpaf_derive/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bpaf_derive"
-version = "0.3.4"
+version = "0.3.5"
 edition = "2018"
 categories = ["command-line-interface"]
 description = "Derive macros for bpaf Command Line Argument Parser"
@@ -15,6 +15,6 @@ name = "bpaf_derive"
 proc-macro = true
 
 [dependencies]
-syn = { version = "1.0.73", features = ["full", "extra-traits"] }
+syn = { version = "2.0.2", features = ["full", "extra-traits"] }
 proc-macro2 = "1.0.27"
 quote = "1.0.9"
diff --git a/bpaf_derive/src/field.rs b/bpaf_derive/src/field.rs
index 45314cd..491d227 100644
--- a/bpaf_derive/src/field.rs
+++ b/bpaf_derive/src/field.rs
@@ -1,6 +1,6 @@
 use proc_macro2::{Span, TokenStream};
 use quote::{quote, ToTokens};
-use syn::parse::{Parse, ParseStream};
+use syn::parse::ParseStream;
 use syn::{
     parenthesized, parse, Attribute, Expr, Ident, LitChar, LitStr, Path, PathArguments, Result,
     Token, Type, Visibility,
@@ -103,18 +103,6 @@ fn parse_optional_arg(input: parse::ParseStream) -> Result<LitStr> {
     }
 }
 
-pub struct Doc(pub String);
-impl Parse for Doc {
-    fn parse(input: parse::ParseStream) -> Result<Self> {
-        input.parse::<Token![=]>()?;
-        let mut s = input.parse::<LitStr>()?.value();
-        if s.starts_with(' ') {
-            s = s[1..].to_string();
-        }
-        Ok(Doc(s))
-    }
-}
-
 #[derive(PartialEq, Debug, Clone)]
 enum Shape {
     Optional(Type),
diff --git a/bpaf_derive/src/field/named_field.rs b/bpaf_derive/src/field/named_field.rs
index b206b5d..1068c85 100644
--- a/bpaf_derive/src/field/named_field.rs
+++ b/bpaf_derive/src/field/named_field.rs
@@ -3,15 +3,15 @@ use quote::{quote, ToTokens};
 use syn::parse::{Parse, ParseStream};
 use syn::spanned::Spanned;
 use syn::{
-    parenthesized, parse2, parse_quote, token, Attribute, Expr, Ident, LitChar, LitStr, Path,
+    parenthesized, parse_quote, token, Attribute, Expr, Ident, LitChar, LitStr, Path,
     PathArguments, PathSegment, Result, Token, Type,
 };
 
-use crate::utils::{to_kebab_case, LineIter};
+use crate::utils::{doc_comment, to_kebab_case, LineIter};
 
 use super::{
-    as_long_name, as_short_name, parse_optional_arg, split_type, ConsumerAttr, Doc, Name,
-    PostprAttr, Shape,
+    as_long_name, as_short_name, parse_optional_arg, split_type, ConsumerAttr, Name, PostprAttr,
+    Shape,
 };
 
 #[derive(Debug, Clone)]
@@ -121,9 +121,12 @@ impl Field {
 
         let mut stage = 0;
         for attr in attrs {
-            if attr.path.is_ident("doc") {
-                help.push(parse2::<Doc>(attr.tokens)?.0);
-            } else if attr.path.is_ident("bpaf") {
+            if attr.path().is_ident("doc") {
+                //help.push(attr.parse_args_with(Doc::parse)?.0);
+                if let Some(doc) = doc_comment(&attr) {
+                    help.push(doc);
+                }
+            } else if attr.path().is_ident("bpaf") {
                 #[allow(clippy::cognitive_complexity)]
                 attr.parse_args_with(|input: ParseStream| loop {
                     if input.is_empty() {
diff --git a/bpaf_derive/src/top.rs b/bpaf_derive/src/top.rs
index 42f2640..b0376d3 100644
--- a/bpaf_derive/src/top.rs
+++ b/bpaf_derive/src/top.rs
@@ -3,15 +3,15 @@ use quote::{quote, ToTokens};
 use syn::parse::{Parse, ParseStream};
 use syn::punctuated::Punctuated;
 use syn::{
-    braced, parenthesized, parse, parse2, parse_quote, token, Attribute, Expr, Ident, LitChar,
-    LitStr, Result, Token, Visibility,
+    braced, parenthesized, parse, parse_quote, token, Attribute, Expr, Ident, LitChar, LitStr,
+    Result, Token, Visibility,
 };
 
 use crate::field::{
-    parse_expr, parse_ident, parse_lit_char, parse_lit_str, parse_opt_arg, ConstrName, Doc, Field,
+    parse_expr, parse_ident, parse_lit_char, parse_lit_str, parse_opt_arg, ConstrName, Field,
     ReqFlag,
 };
-use crate::utils::{snake_case_ident, to_snake_case, LineIter};
+use crate::utils::{doc_comment, snake_case_ident, to_snake_case, LineIter};
 
 #[derive(Debug)]
 pub struct Top {
@@ -106,12 +106,11 @@ impl Parse for Fields {
         let content;
         if input.peek(token::Brace) {
             let _ = braced!(content in input);
-            let fields = content.parse_terminated(Field::parse_named)?;
+            let fields = content.parse_terminated(Field::parse_named, token::Comma)?;
             Ok(Fields::Named(fields))
         } else if input.peek(token::Paren) {
             let _ = parenthesized!(content in input);
-            let fields: Punctuated<_, Token![,]> =
-                content.parse_terminated(Field::parse_unnamed)?;
+            let fields = content.parse_terminated(Field::parse_unnamed, token::Comma)?;
             Ok(Fields::Unnamed(fields))
         } else {
             Err(input.error("Expected named or unnamed struct"))
@@ -159,15 +158,15 @@ impl Inner {
             skip: false,
         };
         for attr in attrs {
-            if attr.path.is_ident("doc") {
-                res.help.push(parse2::<Doc>(attr.tokens)?.0);
-            } else if attr.path.is_ident("bpaf") {
+            if attr.path().is_ident("doc") {
+                if let Some(doc) = doc_comment(&attr) {
+                    res.help.push(doc);
+                }
+            } else if attr.path().is_ident("bpaf") {
                 attr.parse_args_with(|input: ParseStream| loop {
                     if input.is_empty() {
                         break Ok(());
                     }
-                    /////
-
                     let input_copy = input.fork();
                     let keyword = input.parse::<Ident>()?;
 
@@ -266,9 +265,11 @@ impl Outer {
         let mut usage = None;
         let mut help = Vec::new();
         for attr in attrs {
-            if attr.path.is_ident("doc") {
-                help.push(parse2::<Doc>(attr.tokens)?.0);
-            } else if attr.path.is_ident("bpaf") {
+            if attr.path().is_ident("doc") {
+                if let Some(doc) = doc_comment(&attr) {
+                    help.push(doc);
+                }
+            } else if attr.path().is_ident("bpaf") {
                 attr.parse_args_with(|input: ParseStream| loop {
                     if input.is_empty() {
                         break Ok(());
diff --git a/bpaf_derive/src/utils.rs b/bpaf_derive/src/utils.rs
index 42e54a1..a447cc8 100644
--- a/bpaf_derive/src/utils.rs
+++ b/bpaf_derive/src/utils.rs
@@ -1,35 +1,23 @@
-use proc_macro2::{Ident, Span};
-use syn::{
-    parse::{Parse, ParseStream},
-    spanned::Spanned,
-    LitStr, Token,
-};
+use proc_macro2::Ident;
+use syn::Attribute;
 
-struct Doc(pub String);
-impl Parse for Doc {
-    fn parse(input: ParseStream) -> syn::Result<Self> {
-        input.parse::<Token![=]>()?;
-        let s = input.parse::<LitStr>()?.value();
-        Ok(Doc(s.trim_start().to_string()))
-    }
-}
-
-impl<T> Spanned for WithSpan<T> {
-    fn span(&self) -> Span {
-        self.span
-    }
-}
-
-pub struct WithSpan<T> {
-    pub value: T,
-    pub span: Span,
-}
-
-impl<T: Parse> Parse for WithSpan<T> {
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        let span = input.span();
-        let value = input.parse::<T>()?;
-        Ok(WithSpan { value, span })
+pub fn doc_comment(attr: &Attribute) -> Option<String> {
+    match &attr.meta {
+        syn::Meta::NameValue(syn::MetaNameValue {
+            value:
+                syn::Expr::Lit(syn::ExprLit {
+                    lit: syn::Lit::Str(s),
+                    ..
+                }),
+            ..
+        }) => {
+            let mut s = s.value();
+            if s.starts_with(' ') {
+                s = s[1..].to_string();
+            }
+            Some(s)
+        }
+        _ => None,
     }
 }
 
diff --git a/src/args.rs b/src/args.rs
index c5eb86d..e6fbd77 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -1,7 +1,7 @@
 use std::ffi::OsString;
 
 pub(crate) use crate::arg::*;
-use crate::{parsers::NamedArg, Error, Meta};
+use crate::{info::Info, parsers::NamedArg, Error, Meta, ParseFailure};
 
 /// Shows which branch of [`ParseOrElse`] parsed the argument
 #[derive(Debug, Clone)]
@@ -21,6 +21,17 @@ impl Conflict {
     }
 }
 
+#[derive(Clone)]
+pub struct Improve(
+    pub(crate) fn(args: &mut Args, info: &Info, inner: &Meta, err: Error) -> ParseFailure,
+);
+
+impl std::fmt::Debug for Improve {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_tuple("Improve").finish()
+    }
+}
+
 /// Hides [`Args`] internal implementation
 mod inner {
     use std::{
@@ -80,6 +91,9 @@ mod inner {
         /// set of conflicts - usize contains the offset to the rejected item,
         /// first Meta contains accepted item, second meta contains rejected item
         pub(crate) conflicts: BTreeMap<usize, Conflict>,
+
+        /// A way to customize behavior for --help and error handling
+        pub(crate) improve_error: super::Improve,
     }
 
     impl<const N: usize> From<&[&str; N]> for Args {
@@ -144,6 +158,7 @@ mod inner {
                 no_pos_ahead: false,
                 ambig,
                 conflicts: BTreeMap::new(),
+                improve_error: super::Improve(crate::help::improve_error),
             }
         }
     }
@@ -153,6 +168,43 @@ mod inner {
         cur: usize,
     }
 
+    impl Args {
+        #[inline(never)]
+        /// Get a list of command line arguments from OS
+        pub fn current_args() -> Self {
+            let mut arg_vec = Vec::new();
+            #[cfg(feature = "autocomplete")]
+            let mut complete_vec = Vec::new();
+
+            let mut args = std::env::args_os();
+
+            #[allow(unused_variables)]
+            let name = args.next().expect("no command name from args_os?");
+
+            #[cfg(feature = "autocomplete")]
+            for arg in args {
+                if arg
+                    .to_str()
+                    .map_or(false, |s| s.starts_with("--bpaf-complete-"))
+                {
+                    complete_vec.push(arg);
+                } else {
+                    arg_vec.push(arg);
+                }
+            }
+            #[cfg(not(feature = "autocomplete"))]
+            arg_vec.extend(args);
+
+            #[cfg(feature = "autocomplete")]
+            let args = crate::complete_run::args_with_complete(name, &arg_vec, &complete_vec);
+
+            #[cfg(not(feature = "autocomplete"))]
+            let args = Self::from(arg_vec.as_slice());
+
+            args
+        }
+    }
+
     impl<'a> Args {
         /// creates iterator over remaining elements
         pub(crate) fn items_iter(&'a self) -> ArgsIter<'a> {
@@ -477,7 +529,7 @@ impl Args {
     }
 
     pub(crate) fn word_parse_error(&mut self, error: &str) -> Error {
-        Error::Stderr(if let Some(os) = self.current_word() {
+        Error::Message(if let Some(os) = self.current_word() {
             format!("Couldn't parse {:?}: {}", os.to_string_lossy(), error)
         } else {
             format!("Couldn't parse: {}", error)
@@ -485,7 +537,7 @@ impl Args {
     }
 
     pub(crate) fn word_validate_error(&mut self, error: &str) -> Error {
-        Error::Stderr(if let Some(os) = self.current_word() {
+        Error::Message(if let Some(os) = self.current_word() {
             format!("{:?}: {}", os.to_string_lossy(), error)
         } else {
             error.to_owned()
@@ -539,9 +591,9 @@ impl Args {
                     let os = os.to_string_lossy();
                     format!( "`{}` requires an argument, got a flag-like `{}`, try `{}={}` to use it as an argument", arg, os, arg,os)
                 };
-                return Err(Error::Stderr(msg));
+                return Err(Error::Message(msg));
             }
-            _ => return Err(Error::Stderr(format!("{} requires an argument", arg))),
+            _ => return Err(Error::Message(format!("{} requires an argument", arg))),
         };
         let val = val.clone();
         self.current = Some(val_ix);
@@ -568,7 +620,7 @@ impl Args {
                 self.remove(ix);
                 Ok(Some((false, w)))
             }
-            Some((_, arg)) => Err(Error::Stderr(format!("Expected an argument, got {}", arg))),
+            Some((_, arg)) => Err(Error::Message(format!("Expected an argument, got {}", arg))),
             None => Ok(None),
         }
     }
diff --git a/src/buffer.rs b/src/buffer.rs
new file mode 100644
index 0000000..cc926d8
--- /dev/null
+++ b/src/buffer.rs
@@ -0,0 +1,388 @@
+//! String builder, renders a string assembled from styled blocks
+//!
+//!
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub(crate) enum Style {
+    /// Plain text, no extra decorations
+    Text,
+    Section,
+    Label,
+}
+
+#[derive(Debug, Copy, Clone)]
+pub(crate) enum Token {
+    Text { bytes: usize, style: Style },
+    LineBreak,
+    TabStop,
+    Margin(usize),
+}
+#[derive(Debug, Clone, Default)]
+pub(crate) struct Buffer {
+    /// string info saved here
+    payload: String,
+    /// string meta info tokens
+    tokens: Vec<Token>,
+    /// help listing separates keys from help info
+    /// by a single two space wide gap arranging them two columns
+    /// tab stop shows right most position of the second column start seen so far
+    tabstop: usize,
+
+    /// current char and margin are used to calculate tabstop
+    current_margin: usize,
+    current_char: usize,
+
+    /// Should the help contain full string or only up to the first newline
+    pub(crate) complete: bool,
+}
+
+impl Buffer {
+    pub(crate) fn tabstop(&mut self) {
+        let max = self.tabstop.max(self.current_char);
+        if max <= MAX_TAB {
+            self.tabstop = max;
+        }
+        self.tokens.push(Token::TabStop);
+    }
+
+    pub(crate) fn margin(&mut self, margin: usize) {
+        self.current_margin = margin;
+        self.tokens.push(Token::Margin(margin));
+    }
+
+    pub(crate) fn newline(&mut self) {
+        self.current_char = 0;
+        self.tokens.push(Token::LineBreak);
+    }
+
+    #[inline(never)]
+    pub(crate) fn write_str(&mut self, input: &str, style: Style) {
+        if self.current_char == 0 {
+            self.current_char = self.current_margin;
+        }
+
+        let bytes = input.len();
+        let chars = input.chars().count();
+
+        self.current_char += chars;
+        self.payload.push_str(input);
+        match self.tokens.last_mut() {
+            Some(Token::Text {
+                bytes: pb,
+                style: ps,
+            }) if *ps == style => {
+                *pb += bytes;
+            }
+            _ => {
+                self.tokens.push(Token::Text { bytes, style });
+            }
+        }
+    }
+
+    #[inline(never)]
+    pub(crate) fn write_char(&mut self, c: char, style: Style) {
+        self.write_str(c.encode_utf8(&mut [0; 4]), style);
+    }
+
+    pub(crate) fn checkpoint(&self) -> Checkpoint {
+        Checkpoint {
+            tokens: self.tokens.len(),
+            payload: self.payload.len(),
+        }
+    }
+
+    pub(crate) fn rollback(&mut self, checkpoint: Checkpoint) {
+        self.tokens.truncate(checkpoint.tokens);
+        self.payload.truncate(checkpoint.payload);
+    }
+    pub(crate) fn content_since(&self, checkpoint: Checkpoint) -> &str {
+        &self.payload[checkpoint.payload..]
+    }
+}
+
+#[derive(Copy, Clone)]
+pub(crate) struct Checkpoint {
+    tokens: usize,
+    payload: usize,
+}
+
+const MAX_TAB: usize = 24;
+const MAX_WIDTH: usize = 100;
+
+fn padding(f: &mut std::fmt::Formatter<'_>, width: usize) {
+    write!(f, "{:width$}", "", width = width).unwrap();
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+enum Sep {
+    Space,
+    Newline,
+    No,
+}
+
+impl std::fmt::Display for Buffer {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        // byte offset to a start of not consumed portion of a string
+        let mut byte_offset = 0;
+        // character offset frmo the beginning of the line
+        let mut line_offset = 0;
+        // current margin value
+        let mut margin = 0;
+        // are we to the right of the tabstop?
+        let mut after_tabstop = false;
+        let mut immediate_tabstop = false;
+        let mut prev = Sep::No;
+
+        for token in &self.tokens {
+            match *token {
+                Token::Text { bytes, style } => {
+                    // no matter what text should stay to the right of this position
+                    let min_offset = if after_tabstop {
+                        std::cmp::max(margin, self.tabstop + 2)
+                    } else {
+                        margin
+                    };
+
+                    if immediate_tabstop {
+                        immediate_tabstop = false;
+                        line_offset += 2;
+                        padding(f, 2);
+                    }
+
+                    // the idea is to break lines into chunks up to MAX_WIDTH and
+                    // allow parser to have longer names without affecting the layout
+                    // of the whole document:
+                    //
+                    // --very-long-name-goes-here <ARG-IS-LONG-TOO> description starts
+                    //                    here and resumes here, can contain multiple
+                    //                    lines as well - all broken automatically
+                    // -h, --help         Render help
+                    // -v, --version      Show version
+                    //
+                    // app should follow two additional rules:
+                    // - lines that start with a space retain linebreak (and consume the space)
+                    // - double line breaks split help into separate blocks, only first block
+                    //   visible without extra flags
+                    //
+                    //  "--help hello\nworld" => "--help hello world"
+                    //  "--help hello\n world" => "--help hello\nworld"
+                    //  "--help hello\n\nworld" => "--help hello" or "--help hello\nworld"
+
+                    // split a string by words, lay them out between min_offset and MAX_WIDTH
+                    for word in self.payload[byte_offset..byte_offset + bytes]
+                        .split_inclusive(|c| c == ' ' || c == '\n')
+                    {
+                        #[allow(clippy::manual_strip)]
+                        let (word, this) = if word.ends_with(' ') {
+                            (&word[..word.len() - 1], Sep::Space)
+                        } else if word.ends_with('\n') {
+                            (&word[..word.len() - 1], Sep::Newline)
+                        } else {
+                            (word, Sep::No)
+                        };
+                        let chars = word.chars().count();
+
+                        if chars == 0 && prev == Sep::Newline {
+                            if this == Sep::Newline && !self.complete {
+                                return Ok(());
+                            }
+                            writeln!(f)?;
+                            prev = Sep::No;
+                            line_offset = 0;
+                        } else {
+                            // overflow?
+                            if line_offset + chars > MAX_WIDTH {
+                                writeln!(f)?;
+                                line_offset = 0;
+                            } else if prev != Sep::No {
+                                padding(f, 1);
+                                line_offset += 1;
+                            }
+
+                            if min_offset > line_offset {
+                                padding(f, min_offset - line_offset);
+                                line_offset = min_offset;
+                            }
+
+                            match style {
+                                Style::Text => write!(f, "{}", word),
+                                Style::Section => w_section!(f, word),
+                                Style::Label => write!(f, "{}", w_flag!(word)),
+                            }?;
+                            line_offset += chars;
+                            prev = this;
+                        }
+                    }
+                    byte_offset += bytes;
+                }
+                Token::LineBreak => {
+                    line_offset = 0;
+                    writeln!(f)?;
+                    after_tabstop = false;
+                    immediate_tabstop = false;
+                }
+                Token::TabStop => {
+                    after_tabstop = true;
+                    immediate_tabstop = true;
+                }
+                Token::Margin(new_margin) => {
+                    margin = new_margin;
+                }
+            }
+        }
+        if prev == Sep::Space {
+            padding(f, 1);
+        }
+        Ok(())
+    }
+}
+
+#[test]
+fn tabstop_works() {
+    // tabstop followed by newline
+    let mut m = Buffer::default();
+    m.write_str("aa", Style::Text);
+    m.tabstop();
+    m.newline();
+    m.write_str("b", Style::Text);
+    m.tabstop();
+    m.write_str("c", Style::Text);
+    m.newline();
+    assert_eq!(m.to_string(), "aa\nb   c\n");
+
+    // plain, narrow first
+    let mut m = Buffer::default();
+    m.write_str("1", Style::Text);
+    m.tabstop();
+    m.write_str("22", Style::Text);
+    m.newline();
+    m.write_str("33", Style::Text);
+    m.tabstop();
+    m.write_str("4", Style::Text);
+    m.newline();
+    assert_eq!(m.to_string(), "1   22\n33  4\n");
+
+    // plain, wide first
+    let mut m = Buffer::default();
+    m.write_str("aa", Style::Text);
+    m.tabstop();
+    m.write_str("b", Style::Text);
+    m.newline();
+    m.write_str("c", Style::Text);
+    m.tabstop();
+    m.write_str("dd", Style::Text);
+    m.newline();
+    assert_eq!(m.to_string(), "aa  b\nc   dd\n");
+
+    // two different styles first
+    let mut m = Buffer::default();
+    m.write_str("a", Style::Text);
+    m.write_str("b", Style::Label);
+    m.tabstop();
+    m.write_str("c", Style::Label);
+    m.newline();
+    m.write_str("d", Style::Text);
+    m.tabstop();
+    m.write_str("e", Style::Label);
+    m.newline();
+    assert_eq!(m.to_string(), "ab  c\nd   e\n");
+
+    // two different styles with margin first
+    let mut m = Buffer::default();
+    m.margin(2);
+    m.write_str("a", Style::Text);
+    m.write_str("b", Style::Label);
+    m.tabstop();
+    m.write_str("c", Style::Label);
+    m.margin(0);
+    m.newline();
+    m.write_str("d", Style::Text);
+    m.tabstop();
+    m.write_str("e", Style::Label);
+    m.newline();
+    assert_eq!(m.to_string(), "  ab  c\nd     e\n");
+}
+
+#[test]
+fn margin_works() {
+    let mut m = Buffer::default();
+    m.margin(2);
+    m.write_str("a", Style::Text);
+    m.newline();
+    m.write_str("b", Style::Text);
+    m.newline();
+    m.write_str("c", Style::Text);
+    m.newline();
+    assert_eq!(m.to_string(), "  a\n  b\n  c\n");
+}
+
+#[test]
+fn linewrap_works() {
+    let mut m = Buffer::default();
+    m.write_str("--hello", Style::Label);
+    m.tabstop();
+    for _ in 0..15 {
+        m.write_str("word and word ", Style::Text)
+    }
+    m.write_str("and word", Style::Text);
+    m.newline();
+
+    let expected = "\
+--hello  word and word word and word word and word word and word word and word word and word word and
+         word word and word word and word word and word word and word word and word word and word
+         word and word word and word and word
+";
+
+    assert_eq!(m.to_string(), expected);
+}
+
+#[test]
+fn very_long_tabstop() {
+    let mut m = Buffer::default();
+    m.write_str(
+        "--this-is-a-very-long-option <DON'T DO THIS AT HOME>",
+        Style::Label,
+    );
+    m.tabstop();
+    for _ in 0..15 {
+        m.write_str("word and word ", Style::Text)
+    }
+    m.write_str("and word", Style::Text);
+    m.newline();
+
+    let expected = "\
+--this-is-a-very-long-option <DON'T DO THIS AT HOME>  word and word word and word word and word word
+  and word word and word word and word word and word word and word word and word word and word word
+  and word word and word word and word word and word word and word and word
+";
+
+    assert_eq!(m.to_string(), expected);
+}
+
+#[test]
+fn line_breaking_rules() {
+    let mut m = Buffer::default();
+    m.write_str("hello ", Style::Text);
+    assert_eq!(m.to_string(), "hello ");
+
+    let mut m = Buffer::default();
+    m.write_str("hello\n world\n", Style::Text);
+    assert_eq!(m.to_string(), "hello\nworld");
+
+    let mut m = Buffer::default();
+    m.write_str("hello\nworld", Style::Text);
+    assert_eq!(m.to_string(), "hello world");
+
+    let mut m = Buffer::default();
+    m.write_str("hello\nworld\n", Style::Text);
+    assert_eq!(m.to_string(), "hello world");
+
+    let mut m = Buffer::default();
+    m.complete = false;
+    m.write_str("hello\n\nworld", Style::Text);
+    assert_eq!(m.to_string(), "hello");
+
+    let mut m = Buffer::default();
+    m.complete = true;
+    m.write_str("hello\n\nworld", Style::Text);
+    assert_eq!(m.to_string(), "hello\nworld");
+}
diff --git a/src/color.rs b/src/color.rs
index e6fc5fd..63e635d 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -1,6 +1,6 @@
 #[cfg(feature = "bright-color")]
 macro_rules! w_section {
-    ($buf:ident, $pat:literal) => {
+    ($buf:ident, $pat:expr) => {
         write!(
             $buf,
             "{}",
@@ -15,7 +15,7 @@ macro_rules! w_section {
 
 #[cfg(not(feature = "bright-color"))]
 macro_rules! w_section {
-    ($buf:ident, $pat:literal) => {
+    ($buf:ident, $pat:expr) => {
         write!(
             $buf,
             "{}",
diff --git a/src/complete_gen.rs b/src/complete_gen.rs
index dc7dc72..3cc42e5 100644
--- a/src/complete_gen.rs
+++ b/src/complete_gen.rs
@@ -19,7 +19,7 @@ use crate::{
     complete_shell::{write_shell, Shell},
     item::ShortLong,
     parsers::NamedArg,
-    Args, CompleteDecor, Error, ShellComp,
+    Args, CompleteDecor, ShellComp,
 };
 use std::ffi::OsStr;
 
@@ -367,10 +367,10 @@ impl Args {
     /// before calling this method we run parser in "complete" mode and collect live heads inside
     /// `self.comp`, this part goes over collected heads and generates possible completions from
     /// that
-    pub(crate) fn check_complete(&self) -> Result<(), Error> {
+    pub(crate) fn check_complete(&self) -> Option<String> {
         let comp = match self.comp_ref() {
             Some(comp) => comp,
-            None => return Ok(()),
+            None => return None,
         };
 
         let mut items = self
@@ -383,9 +383,10 @@ impl Args {
         // try get a current item to complete - must be non-virtual right most one
         // value must be present here, and can fail only for non-utf8 values
         // can't do much completing with non-utf8 values since bpaf needs to print them to stdout
-        let (arg, lit) = items
-            .next()
-            .ok_or_else(|| Error::Stdout("\n".to_string()))?;
+        let (arg, lit) = match items.next() {
+            Some(a) => a,
+            None => return Some("\n".to_owned()),
+        };
 
         let pos_only = items.clone().any(|i| matches!(i.0, Arg::PosWord(_)));
 
@@ -393,11 +394,13 @@ impl Args {
         if let Arg::Ambiguity(..) = arg {
             // don't bother trying to expand -vvvv for now:
             // -vvv<TAB> => -vvv _
-            return Err(Error::Stdout(format!("{}\n", lit)));
+            return Some(format!("{}\n", lit));
         }
 
-        let res = comp.complete(lit, arg.is_word(), pos_only)?;
-        Err(Error::Stdout(res))
+        let res = comp
+            .complete(lit, arg.is_word(), pos_only)
+            .expect("format error?");
+        Some(res)
     }
 }
 
@@ -683,10 +686,3 @@ fn render_3456(
 
     Ok(res)
 }
-
-// to allow using ? inside check_complete
-impl From<std::fmt::Error> for Error {
-    fn from(_: std::fmt::Error) -> Self {
-        Error::Stderr("Couldn't render completion info".to_string())
-    }
-}
diff --git a/src/help.rs b/src/help.rs
new file mode 100644
index 0000000..67773a4
--- /dev/null
+++ b/src/help.rs
@@ -0,0 +1,85 @@
+//! Improve non-parse cases
+//!
+//! covers `--help`, `--version` etc.
+
+use crate::{info::Info, meta_help::render_help, short, Args, Error, Meta, ParseFailure, Parser};
+
+struct ParseExtraParams {
+    version: Option<&'static str>,
+}
+
+impl Parser<ExtraParams> for ParseExtraParams {
+    fn eval(&self, args: &mut Args) -> Result<ExtraParams, Error> {
+        if let Ok(ok) = ParseExtraParams::help().eval(args) {
+            return Ok(ok);
+        }
+
+        match self.version {
+            Some(ver) => Self::ver(ver).eval(args),
+            None => Err(Error::Message(String::from("Not a version or help flag"))),
+        }
+    }
+
+    fn meta(&self) -> Meta {
+        match self.version {
+            Some(ver) => Meta::And(vec![Self::help().meta(), Self::ver(ver).meta()]),
+            None => Self::help().meta(),
+        }
+    }
+}
+
+impl ParseExtraParams {
+    #[inline(never)]
+    fn help() -> impl Parser<ExtraParams> {
+        short('h')
+            .long("help")
+            .help("Prints help information")
+            .req_flag(ExtraParams::Help)
+    }
+    #[inline(never)]
+    fn ver(version: &'static str) -> impl Parser<ExtraParams> {
+        short('V')
+            .long("version")
+            .help("Prints version information")
+            .req_flag(ExtraParams::Version(version))
+    }
+}
+
+#[derive(Clone, Debug)]
+enum ExtraParams {
+    Help,
+    Version(&'static str),
+}
+
+impl Info {
+    fn help_parser(&self) -> impl Parser<ExtraParams> {
+        ParseExtraParams {
+            version: self.version,
+        }
+    }
+}
+
+pub(crate) fn improve_error(
+    args: &mut Args,
+    info: &Info,
+    inner: &Meta,
+    err: Error,
+) -> ParseFailure {
+    match info.help_parser().eval(args) {
+        Ok(ExtraParams::Help) => {
+            let msg = render_help(info, inner, &info.help_parser().meta());
+            return ParseFailure::Stdout(msg);
+        }
+        Ok(ExtraParams::Version(v)) => {
+            return ParseFailure::Stdout(format!("Version: {}\n", v));
+        }
+        Err(_) => {}
+    }
+
+    if crate::meta_youmean::should_suggest(&err) {
+        if let Some(msg) = crate::meta_youmean::suggest(args, inner) {
+            return ParseFailure::Stderr(msg);
+        }
+    }
+    ParseFailure::from(err)
+}
diff --git a/src/info.rs b/src/info.rs
index ed200f3..28a52a7 100644
--- a/src/info.rs
+++ b/src/info.rs
@@ -6,8 +6,6 @@ use std::marker::PhantomData;
 use crate::{
     args::{Args, Conflict},
     item::Item,
-    meta_help::render_help,
-    params::short,
     parsers::ParseCommand,
     Meta, ParseFailure, Parser,
 };
@@ -15,10 +13,10 @@ use crate::{
 /// Unsuccessful command line parsing outcome, internal representation
 #[derive(Debug)]
 pub enum Error {
-    /// Terminate and print this to stdout
-    Stdout(String),
-    /// Terminate and print this to stderr
-    Stderr(String),
+    /// Parsing failed, it is still possible to improve the error message
+    Message(String),
+    /// Parsing failed and this is the final output
+    ParseFailure(ParseFailure),
     /// Expected one of those values
     ///
     /// Used internally to generate better error messages
@@ -31,12 +29,12 @@ impl Error {
         #[allow(clippy::match_same_arms)]
         match (self, other) {
             // help output takes priority
-            (a @ Error::Stdout(_), _) => a,
-            (_, b @ Error::Stdout(_)) => b,
+            (a @ Error::ParseFailure(_), _) => a,
+            (_, b @ Error::ParseFailure(_)) => b,
 
             // parsing failure takes priority
-            (a @ Error::Stderr(_), _) => a,
-            (_, b @ Error::Stderr(_)) => b,
+            (a @ Error::Message(_), _) => a,
+            (_, b @ Error::Message(_)) => b,
 
             // combine missing elements
             (Error::Missing(mut a), Error::Missing(mut b)) => {
@@ -65,20 +63,6 @@ pub struct Info {
     pub usage: Option<&'static str>,
 }
 
-impl Info {
-    fn help_parser(&self) -> impl Parser<ExtraParams> {
-        ParseExtraParams {
-            version: self.version,
-        }
-    }
-}
-
-#[derive(Clone, Debug)]
-enum ExtraParams {
-    Help,
-    Version(&'static str),
-}
-
 fn check_unexpected(args: &Args) -> Result<(), Error> {
     match args.items_iter().next() {
         None => Ok(()),
@@ -88,7 +72,7 @@ fn check_unexpected(args: &Args) -> Result<(), Error> {
                 use std::fmt::Write;
                 write!(msg, ": {} cannot be used at the same time as {}", rej, acc).unwrap();
             }
-            Err(Error::Stderr(msg))
+            Err(Error::Message(msg))
         }
     }
 }
@@ -129,7 +113,7 @@ impl<T> OptionParser<T> {
     where
         Self: Sized,
     {
-        match self.try_run() {
+        match self.run_inner(Args::current_args()) {
             Ok(t) => t,
             Err(err) => std::process::exit(err.exit_code()),
         }
@@ -176,39 +160,12 @@ impl<T> OptionParser<T> {
     ///
     /// [`ParseFailure`] represents parsing errors, autocomplete results and generated `--help`
     /// output.
+    #[deprecated = "You should switch to equivalent parser.run_inner(Args::current_args())"]
     pub fn try_run(self) -> Result<T, ParseFailure>
     where
         Self: Sized,
     {
-        let mut arg_vec = Vec::new();
-        #[cfg(feature = "autocomplete")]
-        let mut complete_vec = Vec::new();
-
-        let mut args = std::env::args_os();
-
-        #[allow(unused_variables)]
-        let name = args.next().expect("no command name from args_os?");
-
-        #[cfg(feature = "autocomplete")]
-        for arg in args {
-            if arg
-                .to_str()
-                .map_or(false, |s| s.starts_with("--bpaf-complete-"))
-            {
-                complete_vec.push(arg);
-            } else {
-                arg_vec.push(arg);
-            }
-        }
-        #[cfg(not(feature = "autocomplete"))]
-        arg_vec.extend(args);
-
-        #[cfg(feature = "autocomplete")]
-        let args = crate::complete_run::args_with_complete(name, &arg_vec, &complete_vec);
-        #[cfg(not(feature = "autocomplete"))]
-        let args = Args::from(arg_vec.as_slice());
-
-        self.run_inner(args)
+        self.run_inner(Args::current_args())
     }
 
     /// Execute the [`OptionParser`] and produce a values for unit tests or manual processing
@@ -269,23 +226,32 @@ impl<T> OptionParser<T> {
         match self.run_subparser(&mut args) {
             Ok(t) if args.is_empty() => Ok(t),
             Ok(_) => Err(ParseFailure::Stderr(format!("unexpected {:?}", args))),
-            Err(err) => match report_missing_items(err) {
-                Error::Stdout(msg) => Err(ParseFailure::Stdout(msg)),
-                Error::Stderr(msg) => Err(ParseFailure::Stderr(msg)),
-                Error::Missing(_) => unreachable!(),
-            },
+            Err(err) => Err(err),
         }
     }
 
     /// Run subparser, implementation detail
-    pub(crate) fn run_subparser(&self, args: &mut Args) -> Result<T, Error> {
-        let depth = args.depth;
+    pub(crate) fn run_subparser(&self, args: &mut Args) -> Result<T, ParseFailure> {
+        // process should work like this:
+        // - inner parser is evaluated, it returns Error
+        // - if error is finalized (ParseFailure) - it is simply propagated outwards,
+        //   otherwise we are making a few attempts at improving it after dealing with
+        //   autocomplete/help
+        //
+        // - generate autocomplete, if enabled
+        // - produce --help, --version
+        // - Try to improve error message and finalize it otherwise
+        //
+        // outer parser gets value in ParseFailure format
+
         let res = self.inner.eval(args);
-        if let Err(Error::Stdout(_)) = &res {
-            return res;
+        if let Err(Error::ParseFailure(failure)) = res {
+            return Err(failure);
         }
         #[cfg(feature = "autocomplete")]
-        args.check_complete()?;
+        if let Some(comp) = args.check_complete() {
+            return Err(ParseFailure::Stdout(comp));
+        }
 
         let err = match res {
             Ok(r) => {
@@ -295,38 +261,14 @@ impl<T> OptionParser<T> {
                     return Ok(r);
                 }
             }
-
-            // Stderr means nested parser couldn't parse something, store it,
-            // report it if parsing --help and --version also fails
-            Err(Error::Stderr(e)) => Error::Stderr(e),
-
-            // Stdout usually means a happy path such as calling --help or --version on one of
-            // the nested commands
-            Err(Error::Stdout(e)) => return Err(Error::Stdout(e)),
             Err(err) => err,
         };
-
-        match self.info.help_parser().eval(args) {
-            Ok(ExtraParams::Help) => {
-                let msg = render_help(
-                    &self.info,
-                    &self.inner.meta(),
-                    &self.info.help_parser().meta(),
-                )
-                .expect("Couldn't render help");
-                return Err(Error::Stdout(msg));
-            }
-            Ok(ExtraParams::Version(v)) => {
-                return Err(Error::Stdout(format!("Version: {}\n", v)));
-            }
-            Err(_) => {}
-        }
-
-        if crate::meta_youmean::should_suggest(&err) && args.depth == depth {
-            crate::meta_youmean::suggest(args, &self.inner.meta())?;
-        }
-
-        Err(report_missing_items(err))
+        Err((args.improve_error.0)(
+            args,
+            &self.info,
+            &self.inner.meta(),
+            err,
+        ))
     }
 
     /// Get first line of description if Available
@@ -641,13 +583,16 @@ impl<T> OptionParser<T> {
     }
 }
 
-fn report_missing_items(err: Error) -> Error {
-    match err {
-        Error::Stdout(_) | Error::Stderr(_) => err,
-        Error::Missing(items) => Error::Stderr(format!(
-            "Expected {}, pass --help for usage information",
-            Meta::Or(items.into_iter().map(Meta::Item).collect::<Vec<_>>())
-        )),
+impl From<Error> for ParseFailure {
+    fn from(value: Error) -> Self {
+        match value {
+            Error::Message(msg) => ParseFailure::Stderr(msg),
+            Error::ParseFailure(pf) => pf,
+            Error::Missing(items) => ParseFailure::Stderr(format!(
+                "Expected {}, pass --help for usage information",
+                Meta::Or(items.into_iter().map(Meta::Item).collect::<Vec<_>>())
+            )),
+        }
     }
 }
 
@@ -671,49 +616,11 @@ fn perform_invariant_check(meta: &Meta, fresh: bool) {
         }
         Meta::Item(i) => match i {
             Item::Command { meta, .. } => perform_invariant_check(meta, true),
-            Item::Positional { .. } | Item::Flag { .. } | Item::Argument { .. } => {}
+            Item::Positional { .. }
+            | Item::Flag { .. }
+            | Item::Argument { .. }
+            | Item::MultiArg { .. } => {}
         },
         Meta::Skip => {}
     }
 }
-
-struct ParseExtraParams {
-    version: Option<&'static str>,
-}
-
-impl Parser<ExtraParams> for ParseExtraParams {
-    fn eval(&self, args: &mut Args) -> Result<ExtraParams, Error> {
-        if let Ok(ok) = ParseExtraParams::help().eval(args) {
-            return Ok(ok);
-        }
-
-        match self.version {
-            Some(ver) => Self::ver(ver).eval(args),
-            None => Err(Error::Stderr(String::from("Not a version or help flag"))),
-        }
-    }
-
-    fn meta(&self) -> Meta {
-        match self.version {
-            Some(ver) => Meta::And(vec![Self::help().meta(), Self::ver(ver).meta()]),
-            None => Self::help().meta(),
-        }
-    }
-}
-
-impl ParseExtraParams {
-    #[inline(never)]
-    fn help() -> impl Parser<ExtraParams> {
-        short('h')
-            .long("help")
-            .help("Prints help information")
-            .req_flag(ExtraParams::Help)
-    }
-    #[inline(never)]
-    fn ver(version: &'static str) -> impl Parser<ExtraParams> {
-        short('V')
-            .long("version")
-            .help("Prints version information")
-            .req_flag(ExtraParams::Version(version))
-    }
-}
diff --git a/src/item.rs b/src/item.rs
index 80bc966..9beadfa 100644
--- a/src/item.rs
+++ b/src/item.rs
@@ -27,6 +27,12 @@ pub enum Item {
         env: Option<&'static str>,
         help: Option<String>,
     },
+    MultiArg {
+        name: ShortLong,
+        shorts: Vec<char>,
+        help: Option<String>,
+        fields: Vec<(&'static str, Option<String>)>,
+    },
 }
 
 #[doc(hidden)]
@@ -48,42 +54,6 @@ impl From<&NamedArg> for ShortLong {
     }
 }
 
-/// {} renders a version for short usage string
-/// supports padding of the help by some max width
-impl std::fmt::Display for Item {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Item::Positional {
-                metavar,
-                help: _,
-                strict: _,
-            } => metavar.fmt(f),
-            Item::Command { .. } => write!(f, "COMMAND ..."),
-            Item::Flag {
-                name,
-                help: _,
-                shorts: _,
-            } => write!(f, "{}", name),
-            Item::Argument {
-                name,
-                metavar,
-                help: _,
-                env: _,
-                shorts: _,
-            } => write!(f, "{} {}", name, metavar),
-        }
-    }
-}
-
-impl std::fmt::Display for ShortLong {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            ShortLong::Short(short) | ShortLong::ShortLong(short, _) => write!(f, "-{}", short),
-            ShortLong::Long(long) => write!(f, "--{}", long),
-        }
-    }
-}
-
 impl Item {
     #[must_use]
     pub(crate) fn required(self, required: bool) -> Meta {
diff --git a/src/lib.rs b/src/lib.rs
index 2b49b56..2f9a804 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -389,12 +389,14 @@ mod arg;
 mod args;
 #[cfg(feature = "batteries")]
 pub mod batteries;
+mod buffer;
 #[cfg(feature = "autocomplete")]
 mod complete_gen;
 #[cfg(feature = "autocomplete")]
 mod complete_run;
 #[cfg(feature = "autocomplete")]
 mod complete_shell;
+mod help;
 mod info;
 mod item;
 #[cfg(feature = "manpage")]
@@ -1665,6 +1667,7 @@ impl ParseFailure {
     /// Run an action appropriate to the failure and produce the exit code
     ///
     /// Prints a message to `stdout` or `stderr` and returns the exit code
+    #[allow(clippy::must_use_candidate)]
     pub fn exit_code(self) -> i32 {
         match self {
             ParseFailure::Stdout(msg) => {
diff --git a/src/manpage.rs b/src/manpage.rs
index 17f2c67..e44adf0 100644
--- a/src/manpage.rs
+++ b/src/manpage.rs
@@ -2,7 +2,7 @@ use roff::{Inline, Roff};
 
 use crate::{
     item::ShortLong,
-    meta_help::{HelpItem, HelpItems, ShortLongHelp},
+    meta_help::{HelpItem, HelpItems},
     OptionParser,
 };
 
@@ -152,8 +152,8 @@ impl Line<'_> {
         self
     }
 
-    fn shortlong(&mut self, name: ShortLongHelp) -> &mut Self {
-        match name.0 {
+    fn shortlong(&mut self, name: ShortLong) -> &mut Self {
+        match name {
             ShortLong::Short(s) => self.line.push(bold(format!("-{}", s))),
             ShortLong::Long(l) => self.line.push(bold(format!("--{}", l))),
             ShortLong::ShortLong(s, l) => {
@@ -218,13 +218,13 @@ impl Line<'_> {
                 self.norm('-').bold(*name);
             }
             UsageMeta::ShortArg(name, metavar) => {
-                self.norm('-').bold(*name).norm('=').italic(*metavar);
+                self.norm('-').bold(*name).norm('=').italic(metavar);
             }
             UsageMeta::LongFlag(name) => {
                 self.norm("--").bold(*name);
             }
             UsageMeta::LongArg(name, metavar) => {
-                self.norm("--").bold(*name).norm('=').italic(*metavar);
+                self.norm("--").bold(*name).norm('=').italic(metavar);
             }
             UsageMeta::Pos(x) | UsageMeta::StrictPos(x) => {
                 self.metavar(x);
@@ -316,11 +316,7 @@ fn help_item(manpage: &mut Manpage, item: HelpItem, command_path: Option<&str>)
         HelpItem::BlankDecor => {
             manpage.text([]);
         }
-        HelpItem::Positional {
-            strict: _,
-            metavar,
-            help,
-        } => {
+        HelpItem::Positional { metavar, help } => {
             manpage.label(|l| {
                 l.metavar(metavar.0);
             });
@@ -365,6 +361,21 @@ fn help_item(manpage: &mut Manpage, item: HelpItem, command_path: Option<&str>)
                 }
             });
 
+            if let Some(help) = help {
+                manpage.text([norm(help)]);
+            }
+        }
+        HelpItem::MultiArg { name, help, fields } => {
+            manpage.label(|l| {
+                l.shortlong(name).norm("=");
+                for (ix, (m, _help)) in fields.iter().enumerate() {
+                    if ix > 0 {
+                        l.norm(" ");
+                    }
+                    l.metavar(m);
+                }
+            });
+
             if let Some(help) = help {
                 manpage.text([norm(help)]);
             }
diff --git a/src/meta.rs b/src/meta.rs
index fbfc78a..0af7c39 100644
--- a/src/meta.rs
+++ b/src/meta.rs
@@ -53,7 +53,9 @@ impl Meta {
             Meta::Item(m) => match m {
                 Item::Positional { .. } | Item::Command { .. } => {}
                 Item::Flag { shorts, .. } => flags.extend(shorts),
-                Item::Argument { shorts, .. } => args.extend(shorts),
+                Item::MultiArg { shorts, .. } | Item::Argument { shorts, .. } => {
+                    args.extend(shorts);
+                }
             },
             Meta::HideUsage(m) | Meta::Optional(m) | Meta::Many(m) | Meta::Decorated(m, _) => {
                 m.collect_shorts(flags, args);
diff --git a/src/meta_help.rs b/src/meta_help.rs
index 66eb65a..f1235ab 100644
--- a/src/meta_help.rs
+++ b/src/meta_help.rs
@@ -3,6 +3,7 @@
 use std::collections::BTreeSet;
 
 use crate::{
+    buffer::{Buffer, Checkpoint, Style},
     info::Info,
     item::{Item, ShortLong},
     Meta,
@@ -28,7 +29,6 @@ pub(crate) enum HelpItem<'a> {
     },
     BlankDecor,
     Positional {
-        strict: bool,
         metavar: Metavar,
         help: Option<&'a str>,
     },
@@ -42,15 +42,20 @@ pub(crate) enum HelpItem<'a> {
         info: &'a Info,
     },
     Flag {
-        name: ShortLongHelp,
+        name: ShortLong,
         help: Option<&'a str>,
     },
     Argument {
-        name: ShortLongHelp,
+        name: ShortLong,
         metavar: Metavar,
         env: Option<&'static str>,
         help: Option<&'a str>,
     },
+    MultiArg {
+        name: ShortLong,
+        help: Option<&'a str>,
+        fields: &'a [(&'static str, Option<String>)],
+    },
 }
 
 #[derive(Default, Debug)]
@@ -69,10 +74,10 @@ impl HelpItem<'_> {
     }
 }
 
-fn dedup(items: &mut BTreeSet<String>, payload: &mut String, prev: usize) {
-    let new = payload[prev..payload.len()].to_owned();
+fn dedup(items: &mut BTreeSet<String>, buf: &mut Buffer, cp: Checkpoint) {
+    let new = buf.content_since(cp).to_owned();
     if !items.insert(new) {
-        payload.truncate(prev);
+        buf.rollback(cp);
     }
 }
 
@@ -80,20 +85,19 @@ impl<'a> HelpItems<'a> {
     #[inline(never)]
     pub(crate) fn classify_item(&mut self, item: &'a Item) {
         match item {
-            crate::item::Item::Positional {
+            Item::Positional {
                 metavar,
                 help,
-                strict,
+                strict: _,
             } => {
                 if help.is_some() {
                     self.psns.push(HelpItem::Positional {
                         metavar: Metavar(metavar),
                         help: help.as_deref(),
-                        strict: *strict,
                     });
                 }
             }
-            crate::item::Item::Command {
+            Item::Command {
                 name,
                 short,
                 help,
@@ -117,26 +121,36 @@ impl<'a> HelpItems<'a> {
                     help: help.as_deref(),
                 });
             }
-            crate::item::Item::Flag {
+            Item::Flag {
                 name,
                 help,
                 shorts: _,
             } => self.flgs.push(HelpItem::Flag {
-                name: ShortLongHelp(*name),
+                name: *name,
                 help: help.as_deref(),
             }),
-            crate::item::Item::Argument {
+            Item::Argument {
                 name,
                 metavar,
                 env,
                 help,
                 shorts: _,
             } => self.flgs.push(HelpItem::Argument {
-                name: ShortLongHelp(*name),
+                name: *name,
                 metavar: Metavar(metavar),
                 env: *env,
                 help: help.as_deref(),
             }),
+            Item::MultiArg {
+                name,
+                shorts: _,
+                help,
+                fields,
+            } => self.flgs.push(HelpItem::MultiArg {
+                name: *name,
+                help: help.as_deref(),
+                fields,
+            }),
         }
     }
 
@@ -179,23 +193,6 @@ impl<'a> HelpItems<'a> {
     }
 }
 
-#[derive(Debug, Copy, Clone)]
-/// A variant of [`ShortLong`] with different Display properties
-///
-/// - `ShortLong` is used to display synopsys line and prefers to use short name
-/// - `ShortLongHelp`is used to display help message and tries to keep all the names
-pub(crate) struct ShortLongHelp(pub(crate) ShortLong);
-
-impl ShortLongHelp {
-    #[inline]
-    fn full_width(&self) -> usize {
-        match self.0 {
-            ShortLong::Short(_) => 2,
-            ShortLong::Long(l) | ShortLong::ShortLong(_, l) => 6 + l.len(),
-        }
-    }
-}
-
 pub(crate) struct Long<'a>(pub(crate) &'a str);
 impl std::fmt::Display for Long<'_> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -213,106 +210,18 @@ impl std::fmt::Display for Short {
     }
 }
 
-impl std::fmt::Display for ShortLongHelp {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self.0 {
-            ShortLong::Short(short) => write!(f, "{}", w_flag!(Short(short))),
-            ShortLong::Long(long) => write!(f, "    {}", w_flag!(Long(long))),
-            ShortLong::ShortLong(short, long) => {
-                write!(f, "{}, {}", w_flag!(Short(short)), w_flag!(Long(long)))
-            }
-        }
-    }
-}
-
-/// supports padding of the help by some max width
-impl std::fmt::Display for HelpItem<'_> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            HelpItem::Flag { name, help: _ } => write!(f, "    {}", name),
-            HelpItem::Argument {
-                name,
-                metavar,
-                help,
-                env,
-            } => {
-                write!(f, "    {} {}", name, w_flag!(metavar))?;
-
-                let width = f.width().unwrap();
-                if let Some(env) = env {
-                    let pad = width - self.full_width();
-
-                    let m_os = std::env::var_os(env);
-                    let val = match &m_os {
-                        Some(s) => std::borrow::Cow::from(format!(" = {:?}", s.to_string_lossy())),
-                        None => std::borrow::Cow::Borrowed(": N/A"),
-                    };
-                    let next_pad = 4 + self.full_width();
-                    write!(f, "{:pad$}  [env:{}{}]", "", env, val, pad = pad,)?;
-                    if help.is_some() {
-                        write!(f, "\n{:width$}", "", width = next_pad)?;
-                    }
-                }
-                Ok(())
-            }
-            HelpItem::Decor { help } => write!(f, "  {}", help),
-            HelpItem::BlankDecor => Ok(()),
-            HelpItem::Positional {
-                metavar,
-                help: _,
-                strict,
-            } => {
-                if *strict {
-                    write!(f, "    -- {}", w_flag!(metavar))
-                } else {
-                    write!(f, "    {}", w_flag!(metavar))
-                }
-            }
-            HelpItem::Command {
-                name,
-                help: _,
-                short,
-                #[cfg(feature = "manpage")]
-                    meta: _,
-                #[cfg(feature = "manpage")]
-                    info: _,
-            } => match short {
-                Some(s) => write!(f, "    {}, {}", w_flag!(name), w_flag!(s)),
-                None => write!(f, "    {}", w_flag!(name)),
-            },
-        }?;
-
-        // alt view requires width, so unwrap should just work;
-        let width = f.width().unwrap();
-        if let Some(help) = self.help() {
-            let pad = width - self.full_width();
-            for (ix, line) in help.split('\n').enumerate() {
-                {
-                    if ix == 0 {
-                        write!(f, "{:pad$}  {}", "", line, pad = pad)
-                    } else {
-                        write!(f, "\n{:pad$}      {}", "", line, pad = width)
-                    }
-                }?;
-            }
-        }
-        Ok(())
-    }
-}
-
 impl<'a> From<&'a crate::item::Item> for HelpItem<'a> {
     fn from(item: &'a crate::item::Item) -> Self {
         match item {
-            crate::item::Item::Positional {
+            Item::Positional {
                 metavar,
                 help,
-                strict,
+                strict: _,
             } => Self::Positional {
                 metavar: Metavar(metavar),
-                strict: *strict,
                 help: help.as_deref(),
             },
-            crate::item::Item::Command {
+            Item::Command {
                 name,
                 short,
                 help,
@@ -333,145 +242,237 @@ impl<'a> From<&'a crate::item::Item> for HelpItem<'a> {
                 #[cfg(feature = "manpage")]
                 info,
             },
-            crate::item::Item::Flag {
+            Item::Flag {
                 name,
                 help,
                 shorts: _,
             } => Self::Flag {
-                name: ShortLongHelp(*name),
+                name: *name,
                 help: help.as_deref(),
             },
-            crate::item::Item::Argument {
+            Item::Argument {
                 name,
                 metavar,
                 env,
                 help,
                 shorts: _,
             } => Self::Argument {
-                name: ShortLongHelp(*name),
+                name: *name,
                 metavar: Metavar(metavar),
                 env: *env,
                 help: help.as_deref(),
             },
+            Item::MultiArg {
+                name,
+                shorts: _,
+                help,
+                fields,
+            } => Self::MultiArg {
+                name: *name,
+                help: help.as_deref(),
+                fields,
+            },
         }
     }
 }
 
-impl HelpItem<'_> {
-    #[must_use]
-    /// Full width for the name, including implicit short flag, space and comma
-    /// betwen short and log parameters and metavar variable if present
-    fn full_width(&self) -> usize {
-        match self {
-            HelpItem::Decor { .. } | HelpItem::BlankDecor { .. } => 0,
-            HelpItem::Flag { name, .. } => name.full_width(),
-            HelpItem::Argument { name, metavar, .. } => name.full_width() + metavar.0.len() + 3,
-            HelpItem::Positional { metavar, .. } => metavar.0.len() + 2,
-            HelpItem::Command {
-                name, short: None, ..
-            } => name.len(),
-            HelpItem::Command {
-                name,
-                short: Some(_),
-                ..
-            } => name.len() + 3,
+fn write_metavar(buf: &mut Buffer, metavar: Metavar) {
+    buf.write_char('<', Style::Label);
+    buf.write_str(metavar.0, Style::Label);
+    buf.write_char('>', Style::Label);
+}
+
+fn write_help_item(buf: &mut Buffer, item: &HelpItem) {
+    match item {
+        HelpItem::Decor { help } => {
+            buf.margin(2);
+            buf.write_str(help, Style::Text);
+        }
+        HelpItem::BlankDecor => {}
+        HelpItem::Positional { metavar, help } => {
+            buf.margin(4);
+            write_metavar(buf, *metavar);
+            if let Some(help) = help {
+                buf.tabstop();
+                buf.write_str(help, Style::Text);
+            }
+        }
+        HelpItem::Command {
+            name,
+            short,
+            help,
+            #[cfg(feature = "manpage")]
+                meta: _,
+            #[cfg(feature = "manpage")]
+                info: _,
+        } => {
+            buf.margin(4);
+            buf.write_str(name, Style::Label);
+            if let Some(short) = short {
+                buf.write_str(", ", Style::Text);
+                buf.write_char(*short, Style::Label);
+            }
+            buf.tabstop();
+            if let Some(help) = help {
+                buf.write_str(help, Style::Text);
+            }
+        }
+        HelpItem::Flag { name, help } => {
+            buf.margin(4);
+            write_shortlong(buf, *name);
+            buf.tabstop();
+            if let Some(help) = help {
+                buf.write_str(help, Style::Text);
+            }
+        }
+        HelpItem::Argument {
+            name,
+            metavar,
+            env,
+            help,
+        } => {
+            buf.margin(4);
+            write_shortlong(buf, *name);
+            buf.write_str(" ", Style::Label);
+            write_metavar(buf, *metavar);
+            buf.tabstop();
+
+            if let Some(env) = env {
+                let val = match std::env::var_os(env) {
+                    Some(s) => std::borrow::Cow::from(format!(" = {:?}", s.to_string_lossy())),
+                    None => std::borrow::Cow::Borrowed(": N/A"),
+                };
+                buf.write_str(&format!("[env:{}{}]", env, val), Style::Text);
+                if help.is_some() {
+                    buf.newline();
+                    buf.tabstop();
+                }
+            }
+            if let Some(help) = help {
+                buf.write_str(help, Style::Text);
+            }
+        }
+        HelpItem::MultiArg { name, help, fields } => {
+            buf.margin(4);
+            write_shortlong(buf, *name);
+            for (field, _) in fields.iter() {
+                buf.write_str(" ", Style::Label);
+                write_metavar(buf, Metavar(field));
+            }
+
+            if let Some(help) = help {
+                buf.tabstop();
+                buf.write_str(help, Style::Text);
+            }
+            buf.margin(12);
+            for (field, help) in fields.iter() {
+                if let Some(help) = help {
+                    buf.newline();
+                    write_metavar(buf, Metavar(field));
+                    buf.tabstop();
+                    buf.write_str(help, Style::Text);
+                }
+            }
         }
     }
+    buf.newline();
+}
 
-    fn help(&self) -> Option<&str> {
-        match self {
-            HelpItem::Decor { .. } | HelpItem::BlankDecor => None,
-            HelpItem::Command { help, .. }
-            | HelpItem::Flag { help, .. }
-            | HelpItem::Argument { help, .. }
-            | HelpItem::Positional { help, .. } => *help,
+fn write_shortlong(buf: &mut Buffer, name: ShortLong) {
+    match name {
+        ShortLong::Short(s) => {
+            buf.write_char('-', Style::Label);
+            buf.write_char(s, Style::Label);
+        }
+        ShortLong::Long(l) => {
+            buf.write_str("    --", Style::Label);
+            buf.write_str(l, Style::Label);
+        }
+        ShortLong::ShortLong(s, l) => {
+            buf.write_char('-', Style::Label);
+            buf.write_char(s, Style::Label);
+            buf.write_str(", ", Style::Text);
+            buf.write_str("--", Style::Label);
+            buf.write_str(l, Style::Label);
         }
     }
 }
 
-pub(crate) fn render_help(
-    info: &Info,
-    parser_meta: &Meta,
-    help_meta: &Meta,
-) -> Result<String, std::fmt::Error> {
-    use std::fmt::Write;
+fn write_as_lines(buf: &mut Buffer, line: &str) {
+    for line in line.lines() {
+        buf.write_str(line, Style::Text);
+        buf.newline();
+    }
+}
+
+fn write_items(items: &[HelpItem], descr: &str) -> String {
+    if items.is_empty() {
+        String::new()
+    } else {
+        let mut buf = Buffer::default();
+        let mut dedup_cache: BTreeSet<String> = BTreeSet::new();
+        buf.newline();
+        buf.margin(0);
+        buf.write_str(descr, Style::Section);
+        buf.newline();
+        for i in items {
+            let cp = buf.checkpoint();
+            write_help_item(&mut buf, i);
+            dedup(&mut dedup_cache, &mut buf, cp);
+        }
+        buf.to_string()
+    }
+}
 
+pub fn render_help(info: &Info, parser_meta: &Meta, help_meta: &Meta) -> String {
     let mut res = String::new();
+    let mut buf = Buffer::default();
+
     if let Some(t) = info.descr {
-        write!(res, "{}\n\n", t)?;
+        write_as_lines(&mut buf, t);
+        buf.newline();
     }
 
     let auto = parser_meta.to_usage_meta().map(|u| u.to_string());
     if let Some(custom_usage) = info.usage {
         match auto {
-            Some(auto_usage) => write!(
-                res,
-                "{}\n",
-                custom_usage.replacen("{usage}", &auto_usage, 1)
+            Some(auto_usage) => buf.write_str(
+                custom_usage.replacen("{usage}", &auto_usage, 1).as_str(),
+                Style::Text,
             ),
-            None => write!(res, "{}\n", custom_usage),
-        }?;
+            None => buf.write_str(custom_usage, Style::Text),
+        };
+        buf.newline();
     } else if let Some(usage) = auto {
-        write!(res, "Usage: {}\n", usage)?;
+        buf.write_str("Usage: ", Style::Text);
+        buf.write_str(&usage, Style::Text);
+        buf.newline();
     }
 
     if let Some(t) = info.header {
-        write!(res, "\n{}\n", t)?;
+        buf.newline();
+        buf.margin(0);
+
+        write_as_lines(&mut buf, t);
     }
 
     let mut items = HelpItems::default();
     items.classify(parser_meta);
     items.classify(help_meta);
-    let mut dedup_cache = BTreeSet::new();
-    if !items.psns.is_empty() {
-        let max_width = items
-            .psns
-            .iter()
-            .map(HelpItem::full_width)
-            .max()
-            .unwrap_or(0);
-        w_section!(res, "\nAvailable positional items:\n")?;
-        for i in &items.psns {
-            let len = res.len();
-            write!(res, "{:padding$}\n", i, padding = max_width)?;
-            dedup(&mut dedup_cache, &mut res, len);
-        }
-    }
 
-    if !items.flgs.is_empty() {
-        let max_width = items
-            .flgs
-            .iter()
-            .map(HelpItem::full_width)
-            .max()
-            .unwrap_or(0);
-        w_section!(res, "\nAvailable options:\n")?;
-        for i in &items.flgs {
-            let len = res.len();
-            write!(res, "{:padding$}\n", i, padding = max_width)?;
-            dedup(&mut dedup_cache, &mut res, len);
-        }
-    }
+    res.push_str(&buf.to_string());
 
-    if !items.cmds.is_empty() {
-        w_section!(res, "\nAvailable commands:\n")?;
-        let max_width = items
-            .cmds
-            .iter()
-            .map(HelpItem::full_width)
-            .max()
-            .unwrap_or(0);
-        for i in &items.cmds {
-            let len = res.len();
-            write!(res, "{:padding$}\n", i, padding = max_width)?;
-            dedup(&mut dedup_cache, &mut res, len);
-        }
-    }
-    if let Some(t) = info.footer {
-        write!(res, "\n{}", t)?;
-    }
-    if !res.ends_with('\n') {
-        res.push('\n');
+    res.push_str(&write_items(&items.psns, "Available positional items:"));
+    res.push_str(&write_items(&items.flgs, "Available options:"));
+    res.push_str(&write_items(&items.cmds, "Available commands:"));
+
+    let mut buf = Buffer::default();
+    if let Some(footer) = info.footer {
+        buf.margin(0);
+        buf.newline();
+        write_as_lines(&mut buf, footer);
     }
-    Ok(res)
+
+    res.push_str(&buf.to_string());
+    res
 }
diff --git a/src/meta_usage.rs b/src/meta_usage.rs
index 309463b..1c5d685 100644
--- a/src/meta_usage.rs
+++ b/src/meta_usage.rs
@@ -11,9 +11,9 @@ pub(crate) enum UsageMeta {
     Optional(Box<Self>),
     Many(Box<Self>),
     ShortFlag(char),
-    ShortArg(char, &'static str),
+    ShortArg(char, String),
     LongFlag(&'static str),
-    LongArg(&'static str, &'static str),
+    LongArg(&'static str, String),
     Pos(&'static str),
     StrictPos(&'static str),
     Command,
@@ -91,6 +91,7 @@ fn collect_usage_meta(meta: &Meta, is_pos: &mut bool) -> Option<UsageMeta> {
                     let mut top_pos = *is_pos;
                     let usage_meta = collect_usage_meta(x, &mut top_pos)?;
                     any_pos |= top_pos;
+                    #[allow(clippy::equatable_if_let)]
                     if let UsageMeta::Command = &usage_meta {
                         if saw_command {
                             None
@@ -145,10 +146,25 @@ fn collect_usage_meta(meta: &Meta, is_pos: &mut bool) -> Option<UsageMeta> {
             },
             Item::Argument { name, metavar, .. } => match name {
                 ShortLong::Short(s) | ShortLong::ShortLong(s, _) => {
-                    UsageMeta::ShortArg(*s, metavar)
+                    UsageMeta::ShortArg(*s, (*metavar).to_string())
                 }
-                ShortLong::Long(l) => UsageMeta::LongArg(l, metavar),
+                ShortLong::Long(l) => UsageMeta::LongArg(l, (*metavar).to_string()),
             },
+            Item::MultiArg { name, fields, .. } => {
+                let mut args = String::new();
+                for (var, _) in fields.iter() {
+                    if !args.is_empty() {
+                        args.push(' ');
+                    }
+                    args.push_str(var);
+                }
+                match name {
+                    ShortLong::Short(s) | ShortLong::ShortLong(s, _) => {
+                        UsageMeta::ShortArg(*s, args)
+                    }
+                    ShortLong::Long(l) => UsageMeta::LongArg(l, args),
+                }
+            }
         },
     };
     Some(r)
diff --git a/src/meta_youmean.rs b/src/meta_youmean.rs
index bbedabd..b9e1fe9 100644
--- a/src/meta_youmean.rs
+++ b/src/meta_youmean.rs
@@ -7,7 +7,8 @@ use crate::{
 
 pub(crate) fn should_suggest(err: &Error) -> bool {
     match err {
-        Error::Stdout(_) | Error::Stderr(_) => true,
+        Error::Message(_) => true,
+        Error::ParseFailure(_) => false,
         Error::Missing(xs) => {
             let mut hi = crate::meta_help::HelpItems::default();
             for x in xs.iter() {
@@ -19,10 +20,10 @@ pub(crate) fn should_suggest(err: &Error) -> bool {
 }
 
 /// Looks for potential typos
-pub(crate) fn suggest(args: &Args, meta: &Meta) -> Result<(), Error> {
+pub(crate) fn suggest(args: &Args, meta: &Meta) -> Option<String> {
     let arg = match args.peek() {
         Some(arg) => arg,
-        None => return Ok(()),
+        None => return None,
     };
 
     if args.items.iter().filter(|&a| a == arg).count() > 1 {
@@ -30,7 +31,7 @@ pub(crate) fn suggest(args: &Args, meta: &Meta) -> Result<(), Error> {
         // several of those or parser accepts only limited number of them.
         // Or a different branch handles them. Give up and produce a default
         // "not expected in this context" error
-        return Ok(());
+        return None;
     }
 
     let mut variants = Vec::new();
@@ -45,17 +46,17 @@ pub(crate) fn suggest(args: &Args, meta: &Meta) -> Result<(), Error> {
                 actual,
                 w_flag!(cmd),
             );
-            return Err(Error::Stderr(best));
+            return Some(best);
         } else if l > 0 {
             let best = format!(
                 "No such {:?}, did you mean `{}`?",
                 actual,
                 w_flag!(expected)
             );
-            return Err(Error::Stderr(best));
+            return Some(best);
         }
     }
-    Ok(())
+    None
 }
 
 #[derive(Copy, Clone)]
@@ -140,14 +141,16 @@ fn inner_item<'a>(
                 ins(I::ShortCmd(*s), actual, variants);
             }
         }
-        Item::Flag { name, .. } | Item::Argument { name, .. } => match name {
-            ShortLong::Short(s) => ins(I::ShortFlag(*s), actual, variants),
-            ShortLong::Long(l) => ins(I::LongFlag(l), actual, variants),
-            ShortLong::ShortLong(s, l) => {
-                ins(I::ShortFlag(*s), actual, variants);
-                ins(I::LongFlag(l), actual, variants);
+        Item::Flag { name, .. } | Item::Argument { name, .. } | Item::MultiArg { name, .. } => {
+            match name {
+                ShortLong::Short(s) => ins(I::ShortFlag(*s), actual, variants),
+                ShortLong::Long(l) => ins(I::LongFlag(l), actual, variants),
+                ShortLong::ShortLong(s, l) => {
+                    ins(I::ShortFlag(*s), actual, variants);
+                    ins(I::LongFlag(l), actual, variants);
+                }
             }
-        },
+        }
     }
 }
 
diff --git a/src/no_color.rs b/src/no_color.rs
index 77fc239..7a279ac 100644
--- a/src/no_color.rs
+++ b/src/no_color.rs
@@ -1,5 +1,5 @@
 macro_rules! w_section {
-    ($buf:ident, $pat:literal) => {
+    ($buf:ident, $pat:expr) => {
         write!($buf, "{}", &$pat,)
     };
 }
diff --git a/src/params.rs b/src/params.rs
index 8d046a5..3cdfe6a 100644
--- a/src/params.rs
+++ b/src/params.rs
@@ -574,7 +574,10 @@ impl<T> Parser<T> for ParseCommand<T> {
             args.depth += 1;
             // `or_else` would prefer failures past this point to preceeding levels
             #[allow(clippy::let_and_return)]
-            let res = self.subparser.run_subparser(args);
+            let res = self
+                .subparser
+                .run_subparser(args)
+                .map_err(Error::ParseFailure);
             res
         } else {
             #[cfg(feature = "autocomplete")]
@@ -872,7 +875,7 @@ fn parse_word(
             #[cfg(feature = "autocomplete")]
             args.push_value("--", &Some("-- Positional only items".to_owned()), false);
 
-            return Err(Error::Stderr(format!(
+            return Err(Error::Message(format!(
                 "Expected <{}> to be on the right side of --",
                 metavar,
             )));
diff --git a/src/structs.rs b/src/structs.rs
index 4877b18..380a8e3 100644
--- a/src/structs.rs
+++ b/src/structs.rs
@@ -23,10 +23,10 @@ where
     fn eval(&self, args: &mut Args) -> Result<T, Error> {
         match self.inner.eval(args) {
             Ok(ok) => Ok(ok),
-            e @ Err(Error::Stderr(_) | Error::Stdout(_)) => e,
+            e @ Err(Error::Message(_) | Error::ParseFailure(_)) => e,
             Err(Error::Missing(_)) => match (self.fallback)() {
                 Ok(ok) => Ok(ok),
-                Err(e) => Err(Error::Stderr(e.to_string())),
+                Err(e) => Err(Error::Message(e.to_string())),
             },
         }
     }
@@ -99,7 +99,7 @@ where
         }
 
         if res.is_empty() {
-            Err(Error::Stderr(self.message.to_string()))
+            Err(Error::Message(self.message.to_string()))
         } else {
             Ok(res)
         }
@@ -375,7 +375,7 @@ where
                 std::mem::swap(args, &mut clone);
                 Ok(ok)
             }
-            e @ Err(Error::Stderr(_) | Error::Stdout(_)) => e,
+            e @ Err(Error::Message(_) | Error::ParseFailure(_)) => e,
             Err(Error::Missing(_)) => {
                 #[cfg(feature = "autocomplete")]
                 args.swap_comps(&mut clone);
@@ -493,8 +493,8 @@ where
             }
 
             match err {
-                Error::Stderr(_) if catch => Ok(None),
-                Error::Stderr(_) | Error::Stdout(_) => Err(err),
+                Error::Message(_) if catch => Ok(None),
+                Error::Message(_) | Error::ParseFailure(_) => Err(err),
                 Error::Missing(_) => {
                     if args.len() == orig_args.len() {
                         Ok(None)
@@ -554,7 +554,7 @@ impl<T: Clone + 'static, F: Fn() -> Result<T, E>, E: ToString> Parser<T>
     fn eval(&self, _args: &mut Args) -> Result<T, Error> {
         match (self.0)() {
             Ok(ok) => Ok(ok),
-            Err(e) => Err(Error::Stderr(e.to_string())),
+            Err(e) => Err(Error::Message(e.to_string())),
         }
     }
 
@@ -571,7 +571,7 @@ pub struct ParseFail<T> {
 impl<T> Parser<T> for ParseFail<T> {
     fn eval(&self, args: &mut Args) -> Result<T, Error> {
         args.current = None;
-        Err(Error::Stderr(self.field1.to_owned()))
+        Err(Error::Message(self.field1.to_owned()))
     }
 
     fn meta(&self) -> Meta {
@@ -802,6 +802,41 @@ where
     }
 
     fn meta(&self) -> Meta {
-        self.inner.meta()
+        classify_anywhere(self.inner.meta())
+    }
+}
+
+// currently supported anywhere meta patterns:
+// - multi value options consisting of a required option followed by one or more positional items
+
+// anything else is
+fn classify_anywhere(meta: Meta) -> Meta {
+    use crate::item::Item;
+    if let Meta::And(xs) = &meta {
+        let mut fields = Vec::new();
+        let mut iter = xs.iter();
+
+        let (name, shorts, help) = match iter.next() {
+            Some(Meta::Item(Item::Flag { name, shorts, help })) => (name, shorts, help),
+            _ => return meta,
+        };
+
+        while let Some(Meta::Item(Item::Positional {
+            metavar,
+            strict: _,
+            help,
+        })) = iter.next()
+        {
+            fields.push((*metavar, help.clone()));
+        }
+        if iter.next().is_none() {
+            return Meta::Item(Item::MultiArg {
+                name: *name,
+                shorts: shorts.clone(),
+                help: help.clone(),
+                fields,
+            });
+        }
     }
+    meta
 }


Test patch:
diff --git a/tests/derive.rs b/tests/derive.rs
index 894f591..ca3eea7 100644
--- a/tests/derive.rs
+++ b/tests/derive.rs
@@ -96,8 +96,7 @@ Available options:
 
 Available commands:
     add        Add a new TODO item
-    no_action  Does nothing
-               in two lines
+    no_action  Does nothing in two lines
 ";
     assert_eq!(expected_help, help);
 }
diff --git a/tests/help_format.rs b/tests/help_format.rs
index 030a4cb..2146a7a 100644
--- a/tests/help_format.rs
+++ b/tests/help_format.rs
@@ -195,3 +195,67 @@ Available options:
 ";
     assert_eq!(r, expected);
 }
+
+#[test]
+fn anywhere_invariant_check() {
+    #[derive(Debug, Clone, Bpaf)]
+    #[allow(dead_code)]
+    #[bpaf(anywhere)]
+    struct Foo {
+        tag: (),
+        #[bpaf(positional)]
+        /// help
+        name: String,
+        #[bpaf(positional)]
+        val: String,
+    }
+
+    let a = short('a').switch();
+    let b = short('b').switch();
+    let parser = construct!(a, foo(), b).to_options();
+
+    parser.check_invariants(true);
+
+    let expected = "\
+Usage: [-a] --tag ARG ARG [-b]
+
+Available options:
+    -a
+        --tag <ARG> <ARG>
+            <ARG>  help
+    -b
+    -h, --help     Prints help information
+";
+    let r = parser
+        .run_inner(Args::from(&["--help"]))
+        .unwrap_err()
+        .unwrap_stdout();
+    assert_eq!(r, expected);
+}
+
+#[test]
+fn multi_arg_help() {
+    let a = long("flag").help("flag help").req_flag(());
+    let b = positional::<String>("NAME").help("pos1 help");
+    let c = positional::<bool>("STATE").help("pos2 help");
+    let combo = construct!(a, b, c).anywhere();
+    let verbose = short('v').long("verbose").help("verbose").switch();
+    let parser = construct!(verbose, combo).to_options();
+
+    let r = parser
+        .run_inner(Args::from(&["--help"]))
+        .unwrap_err()
+        .unwrap_stdout();
+
+    let expected = "\
+Usage: [-v] --flag NAME STATE
+
+Available options:
+    -v, --verbose    verbose
+        --flag <NAME> <STATE>  flag help
+            <NAME>   pos1 help
+            <STATE>  pos2 help
+    -h, --help       Prints help information
+";
+    assert_eq!(r, expected);
+}
diff --git a/tests/positionals.rs b/tests/positionals.rs
index f808a03..a7acdee 100644
--- a/tests/positionals.rs
+++ b/tests/positionals.rs
@@ -15,8 +15,7 @@ fn positional_with_help() {
 Usage: <USER> <API_KEY>
 
 Available positional items:
-    <USER>     github user
-               in two lines
+    <USER>     github user in two lines
     <API_KEY>  api key to use
 
 Available options:
@@ -40,10 +39,8 @@ fn help_for_positional() {
 Usage: <C> <DDD>
 
 Available positional items:
-    <C>    help for
-           c
-    <DDD>  help for
-           ddd
+    <C>    help for c
+    <DDD>  help for ddd
 
 Available options:
     -h, --help  Prints help information
diff --git a/tests/tests.rs b/tests/tests.rs
index 2b4b0ea..37c4258 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -404,7 +404,7 @@ Available options:
     -d, --d-very-long-flag-with <ARG>
     -e, --e-very-long-flag-with <ARG>
     -f, --f-very-long-flag-with <ARG>
-    -h, --help                         Prints help information
+    -h, --help  Prints help information
 ";
 
     assert_eq!(expected_help, help);
@@ -841,8 +841,7 @@ this is a test
 Usage: [-a] [-b]
 
 Available options:
-    -a, --AAAAA  two lines
-                 of help
+    -a, --AAAAA  two lines of help
     -b
     -h, --help   Prints help information
 ";
@@ -877,8 +876,7 @@ Usage: --key KEY
 
 Available options:
         --key <KEY>  [env:BPAF_SECRET_API_KEY: N/A]
-                     use this secret key
-                     two lines
+                     use this secret key two lines
     -h, --help       Prints help information
 ";
     assert_eq!(expected_help, help);
@@ -893,8 +891,7 @@ Usage: --key KEY
 
 Available options:
         --key <KEY>  [env:BPAF_SECRET_API_KEY = \"top s3cr3t\"]
-                     use this secret key
-                     two lines
+                     use this secret key two lines
     -h, --help       Prints help information
 ";
     assert_eq!(expected_help, help);
@@ -1032,14 +1029,11 @@ fn help_for_options() {
 Usage: [-a] -c B --bbbbb CCC
 
 Available options:
-    -a                 help for
-                       a
+    -a                 help for a
     -c <B>             [env:BbBbB: N/A]
-                       help for
-                       b
+                       help for b
         --bbbbb <CCC>  [env:ccccCCccc: N/A]
-                       help for
-                       ccc
+                       help for ccc
     -h, --help         Prints help information
 ";
 
@@ -1067,10 +1061,8 @@ Available options:
     -h, --help  Prints help information
 
 Available commands:
-    thing_d     help for d
-                two lines
-    thing_e, e  help for e
-                two lines
+    thing_d     help for d two lines
+    thing_e, e  help for e two lines
     thing_h
 ";
     assert_eq!(expected_help, help);