Repository: google/argh (language: rust)

Issue:
Undocumented/unintended that doc comment strings can contain format arguments
```rust
/// a. {this_is_not_ok}
#[derive(argh::FromArgs)]
struct Demo {
    /// b. {nor_is_this}
    #[argh(option)]
    value: u8,
}
```

```toml
[dependencies]
argh = "0.1.7"
```

```
error: there is no argument named `this_is_not_ok`
 --> src/main.rs:2:10
  |
2 | #[derive(argh::FromArgs)]
  |          ^^^^^^^^^^^^^^
  |
  = note: did you intend to capture a variable `this_is_not_ok` from the surrounding scope?
  = note: to avoid ambiguity, `format_args!` cannot capture variables when the format string is expanded from a macro
  = note: this error originates in the derive macro `argh::FromArgs` (in Nightly builds, run with -Z macro-backtrace for more info)

error: there is no argument named `nor_is_this`
 --> src/main.rs:2:10
  |
2 | #[derive(argh::FromArgs)]
  |          ^^^^^^^^^^^^^^
  |
  = note: did you intend to capture a variable `nor_is_this` from the surrounding scope?
  = note: to avoid ambiguity, `format_args!` cannot capture variables when the format string is expanded from a macro
  = note: this error originates in the derive macro `argh::FromArgs` (in Nightly builds, run with -Z macro-backtrace for more info)
```


Discussion / hints:

Fix patch:
diff --git a/argh_derive/src/parse_attrs.rs b/argh_derive/src/parse_attrs.rs
index f968ca7..70af45e 100644
--- a/argh_derive/src/parse_attrs.rs
+++ b/argh_derive/src/parse_attrs.rs
@@ -500,7 +500,7 @@ fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Desc
         return;
     };
 
-    // Don't replace an existing description.
+    // Don't replace an existing explicit description.
     if slot.as_ref().map(|d| d.explicit).unwrap_or(false) {
         return;
     }
@@ -509,14 +509,43 @@ fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Desc
         let lit_str = if let Some(previous) = slot {
             let previous = &previous.content;
             let previous_span = previous.span();
-            syn::LitStr::new(&(previous.value() + &*lit_str.value()), previous_span)
+            syn::LitStr::new(&(previous.value() + &unescape_doc(lit_str.value())), previous_span)
         } else {
-            lit_str.clone()
+            syn::LitStr::new(&unescape_doc(lit_str.value()), lit_str.span())
         };
         *slot = Some(Description { explicit: false, content: lit_str });
     }
 }
 
+/// Replaces escape sequences in doc-comments with the characters they represent.
+///
+/// Rustdoc understands CommonMark escape sequences consisting of a backslash followed by an ASCII
+/// punctuation character. Any other backslash is treated as a literal backslash.
+fn unescape_doc(s: String) -> String {
+    let mut result = String::with_capacity(s.len());
+
+    let mut characters = s.chars().peekable();
+    while let Some(mut character) = characters.next() {
+        if character == '\\' {
+            if let Some(next_character) = characters.peek() {
+                if next_character.is_ascii_punctuation() {
+                    character = *next_character;
+                    characters.next();
+                }
+            }
+        }
+
+        // Braces must be escaped as this string will be used as a format string
+        if character == '{' || character == '}' {
+            result.push(character);
+        }
+
+        result.push(character);
+    }
+
+    result
+}
+
 fn parse_attr_description(errors: &Errors, m: &syn::MetaNameValue, slot: &mut Option<Description>) {
     let lit_str =
         if let Some(lit_str) = errors.expect_lit_str(&m.value) { lit_str } else { return };


Test patch:
diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs
index dbd3b48..0f3998a 100644
--- a/argh/tests/lib.rs
+++ b/argh/tests/lib.rs
@@ -303,6 +303,30 @@ Options:
     );
 }
 
+#[test]
+fn escaped_doc_comment_description() {
+    #[derive(FromArgs)]
+    /// A \description\:
+    /// \!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~\
+    struct Cmd {
+        #[argh(switch)]
+        /// a \description\:
+        /// \!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~\
+        _s: bool,
+    }
+
+    assert_help_string::<Cmd>(
+        r###"Usage: test_arg_0 [--s]
+
+A \description: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~\
+
+Options:
+  --s               a \description: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~\
+  --help            display usage information
+"###,
+    );
+}
+
 #[test]
 fn explicit_long_value_for_option() {
     #[derive(FromArgs, Debug)]