Repository: clap-rs/clap (language: rust)

Issue:
Markdown links in the help message of a value enum causes ZSH completion to error
### Please complete the following tasks

- [X] I have searched the [discussions](https://github.com/clap-rs/clap/discussions)
- [X] I have searched the [open](https://github.com/clap-rs/clap/issues) and [rejected](https://github.com/clap-rs/clap/issues?q=is%3Aissue+label%3AS-wont-fix+is%3Aclosed) issues

### Rust Version

1.67.1

### Clap Version

4.1.8

### Minimal reproducible code

The complete code is here: https://github.com/KSXGitHub/bug-report-clap-completion-when-help-contains-special-characters

### Steps to reproduce the bug with the above code

The complete steps to reproduce is also here: https://github.com/KSXGitHub/bug-report-clap-completion-when-help-contains-special-characters

### Actual Behaviour

The following error messages appear:

```
(eval):1: unmatched "
_arguments:465: command not found: (
(eval):1: unmatched "
_arguments:465: command not found: (
(eval):1: unmatched "
_arguments:465: command not found: (
```

### Expected Behaviour

Completion works normally.

### Additional Context

_No response_

### Debug Output

_No response_


Discussion / hints:
Hi @epage, I would like to try to fix this bug. I confirm that I can reproduce the bug with the sample code.

For now, should we just throw an error at compile time when there is markdown code in a help message, as @KSXGitHub suggested, instead of trying to escape anything? I need to look into this a bit more to understand the root cause, but at least for now I assume that it's highly likely that there will be edge cases missed out or at least cases that cannot be handled, e.g., links to images, if we try to transform the markdown code. What do you think?
For edge cases, I think just emit an error is sufficient. If the user wants to preserve the markdown documentation, they may specify `#[clap(help)]` attribute (which should be hinted at in the error message). 
BTW, all the transformation and error message should be done at compile time, in the derive macros, because it is highly unlikely that a user would make the silly mistake of putting markdown code in the help message if they are using the arg builder directly.
So you are still in favour of at least converting hyperlinks? I'm not sure if it would lead to more confusion due to inconsistency. When an error is thrown and users are told that the help message with markdown is invalid, they might wonder why hyperlinks work but other markdown syntaxes don't.
Anyway, I'll try to throw an error when markdown is detected in a help message. We figure out if we want to allow any specific case later.
I think the user can understand that the terminal has its own limitations. If the error message explains that the terminal is incapable of displaying something, then there would be no confusion.
Any erroring off of content of the help message would be a breaking change.  I also do not want to get into the business of trying to outguess the user for what they can and cannot include in their help message.

We should make sure we are properly escaping.

Fix patch:
diff --git a/clap_complete/src/shells/zsh.rs b/clap_complete/src/shells/zsh.rs
index 3150cac9511..28c8ff85716 100644
--- a/clap_complete/src/shells/zsh.rs
+++ b/clap_complete/src/shells/zsh.rs
@@ -426,6 +426,9 @@ fn escape_help(string: &str) -> String {
         .replace('\'', "'\\''")
         .replace('[', "\\[")
         .replace(']', "\\]")
+        .replace(':', "\\:")
+        .replace('$', "\\$")
+        .replace('`', "\\`")
 }
 
 /// Escape value string inside single quotes and parentheses
@@ -433,6 +436,11 @@ fn escape_value(string: &str) -> String {
     string
         .replace('\\', "\\\\")
         .replace('\'', "'\\''")
+        .replace('[', "\\[")
+        .replace(']', "\\]")
+        .replace(':', "\\:")
+        .replace('$', "\\$")
+        .replace('`', "\\`")
         .replace('(', "\\(")
         .replace(')', "\\)")
         .replace(' ', "\\ ")
@@ -688,3 +696,26 @@ fn write_positionals_of(p: &Command) -> String {
 
     ret.join("\n")
 }
+
+#[cfg(test)]
+mod tests {
+    use crate::shells::zsh::{escape_help, escape_value};
+
+    #[test]
+    fn test_escape_value() {
+        let raw_string = "\\ [foo]() `bar https://$PATH";
+        assert_eq!(
+            escape_value(raw_string),
+            "\\\\\\ \\[foo\\]\\(\\)\\ \\`bar\\ https\\://\\$PATH"
+        )
+    }
+
+    #[test]
+    fn test_escape_help() {
+        let raw_string = "\\ [foo]() `bar https://$PATH";
+        assert_eq!(
+            escape_help(raw_string),
+            "\\\\ \\[foo\\]() \\`bar https\\://\\$PATH"
+        )
+    }
+}


Test patch:
diff --git a/clap_complete/tests/snapshots/quoting.zsh b/clap_complete/tests/snapshots/quoting.zsh
index 95ed7eb697d..7e36d721b49 100644
--- a/clap_complete/tests/snapshots/quoting.zsh
+++ b/clap_complete/tests/snapshots/quoting.zsh
@@ -17,10 +17,10 @@ _my-app() {
     _arguments "${_arguments_options[@]}" \
 '--single-quotes[Can be '\''always'\'', '\''auto'\'', or '\''never'\'']' \
 '--double-quotes[Can be "always", "auto", or "never"]' \
-'--backticks[For more information see `echo test`]' \
+'--backticks[For more information see \`echo test\`]' \
 '--backslash[Avoid '\''\\n'\'']' \
 '--brackets[List packages \[filter\]]' \
-'--expansions[Execute the shell command with $SHELL]' \
+'--expansions[Execute the shell command with \$SHELL]' \
 '-h[Print help]' \
 '--help[Print help]' \
 '-V[Print version]' \
@@ -124,10 +124,10 @@ _my-app_commands() {
     local commands; commands=(
 'cmd-single-quotes:Can be '\''always'\'', '\''auto'\'', or '\''never'\''' \
 'cmd-double-quotes:Can be "always", "auto", or "never"' \
-'cmd-backticks:For more information see `echo test`' \
+'cmd-backticks:For more information see \`echo test\`' \
 'cmd-backslash:Avoid '\''\\n'\''' \
 'cmd-brackets:List packages \[filter\]' \
-'cmd-expansions:Execute the shell command with $SHELL' \
+'cmd-expansions:Execute the shell command with \$SHELL' \
 'help:Print this message or the help of the given subcommand(s)' \
     )
     _describe -t commands 'my-app commands' commands "$@"
@@ -197,10 +197,10 @@ _my-app__help_commands() {
     local commands; commands=(
 'cmd-single-quotes:Can be '\''always'\'', '\''auto'\'', or '\''never'\''' \
 'cmd-double-quotes:Can be "always", "auto", or "never"' \
-'cmd-backticks:For more information see `echo test`' \
+'cmd-backticks:For more information see \`echo test\`' \
 'cmd-backslash:Avoid '\''\\n'\''' \
 'cmd-brackets:List packages \[filter\]' \
-'cmd-expansions:Execute the shell command with $SHELL' \
+'cmd-expansions:Execute the shell command with \$SHELL' \
 'help:Print this message or the help of the given subcommand(s)' \
     )
     _describe -t commands 'my-app help commands' commands "$@"