Repository: kbknapp/clap-rs
Pull request #1039 (2017-09-04 14:07:59)

Escape special characters in zsh and fish completions
Bash and PowerShell work fine as they don't support help texts.  Test cases are
taken from real world applications. For example zsh completion for rustup is
partly broken because help texts contain backticks that are interpreted as
subcommands.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/kbknapp/clap-rs/1039)
<!-- Reviewable:end -->


coveralls: 
[![Coverage Status](https://coveralls.io/builds/13116638/badge)](https://coveralls.io/builds/13116638)

Coverage increased (+1.4%) to 93.65% when pulling **6d95772de4c4c37502558ac1d1dca621ad480ee8 on siiptuo:fix-completion-special-characters** into **f2266e23896cd9c97d7c32344caf41c68ddde0d3 on kbknapp:master**.


Patch:
diff --git a/src/completions/fish.rs b/src/completions/fish.rs
index 2e0f36ea..894f89ba 100644
--- a/src/completions/fish.rs
+++ b/src/completions/fish.rs
@@ -40,6 +40,11 @@ end
     }
 }
 
+// Escape string inside single quotes
+fn escape_string(string: &str) -> String {
+    string.replace("\\", "\\\\").replace("'", "\\'")
+}
+
 fn gen_fish_inner(root_command: &str, comp_gen: &FishGen, parent_cmds: &str, buffer: &mut String) {
     debugln!("FishGen::gen_fish_inner;");
     // example :
@@ -67,7 +72,7 @@ fn gen_fish_inner(root_command: &str, comp_gen: &FishGen, parent_cmds: &str, buf
             template.push_str(format!(" -l {}", data).as_str());
         }
         if let Some(data) = option.b.help {
-            template.push_str(format!(" -d \"{}\"", data).as_str());
+            template.push_str(format!(" -d '{}'", escape_string(data)).as_str());
         }
         if let Some(ref data) = option.v.possible_vals {
             template.push_str(format!(" -r -f -a \"{}\"", data.join(" ")).as_str());
@@ -85,7 +90,7 @@ fn gen_fish_inner(root_command: &str, comp_gen: &FishGen, parent_cmds: &str, buf
             template.push_str(format!(" -l {}", data).as_str());
         }
         if let Some(data) = flag.b.help {
-            template.push_str(format!(" -d \"{}\"", data).as_str());
+            template.push_str(format!(" -d '{}'", escape_string(data)).as_str());
         }
         buffer.push_str(template.as_str());
         buffer.push_str("\n");
@@ -96,7 +101,7 @@ fn gen_fish_inner(root_command: &str, comp_gen: &FishGen, parent_cmds: &str, buf
         template.push_str(" -f");
         template.push_str(format!(" -a \"{}\"", &subcommand.p.meta.name).as_str());
         if let Some(data) = subcommand.p.meta.about {
-            template.push_str(format!(" -d \"{}\"", &data).as_str())
+            template.push_str(format!(" -d '{}'", escape_string(&data)).as_str())
         }
         buffer.push_str(template.as_str());
         buffer.push_str("\n");
diff --git a/src/completions/zsh.rs b/src/completions/zsh.rs
index fa84342f..298b5cad 100644
--- a/src/completions/zsh.rs
+++ b/src/completions/zsh.rs
@@ -292,12 +292,20 @@ fn get_args_of(p: &Parser) -> String {
     ret.join("\n")
 }
 
+// Escape string inside single quotes and brackets
+fn escape_string(string: &str) -> String {
+    string.replace("\\", "\\\\")
+        .replace("'", "'\\''")
+        .replace("[", "\\[")
+        .replace("]", "\\]")
+}
+
 fn write_opts_of(p: &Parser) -> String {
     debugln!("write_opts_of;");
     let mut ret = vec![];
     for o in p.opts() {
         debugln!("write_opts_of:iter: o={}", o.name());
-        let help = o.help().unwrap_or("").replace("[", "\\[").replace("]", "\\]");
+        let help = o.help().map_or(String::new(), escape_string);
         let mut conflicts = get_zsh_arg_conflicts!(p, o, INTERNAL_ERROR_MSG);
         conflicts = if conflicts.is_empty() {
             String::new()
@@ -316,7 +324,7 @@ fn write_opts_of(p: &Parser) -> String {
             String::new()
         };
         if let Some(short) = o.short() {
-            let s = format!("\"{conflicts}{multiple}-{arg}+[{help}]{possible_values}\" \\",
+            let s = format!("'{conflicts}{multiple}-{arg}+[{help}]{possible_values}' \\",
                 conflicts = conflicts,
                 multiple = multiple,
                 arg = short,
@@ -327,7 +335,7 @@ fn write_opts_of(p: &Parser) -> String {
             ret.push(s);
         }
         if let Some(long) = o.long() {
-            let l = format!("\"{conflicts}{multiple}--{arg}+[{help}]{possible_values}\" \\",
+            let l = format!("'{conflicts}{multiple}--{arg}+[{help}]{possible_values}' \\",
                 conflicts = conflicts,
                 multiple = multiple,
                 arg = long,
@@ -347,7 +355,7 @@ fn write_flags_of(p: &Parser) -> String {
     let mut ret = vec![];
     for f in p.flags() {
         debugln!("write_flags_of:iter: f={}", f.name());
-        let help = f.help().unwrap_or("").replace("[", "\\[").replace("]", "\\]");
+        let help = f.help().map_or(String::new(), escape_string);
         let mut conflicts = get_zsh_arg_conflicts!(p, f, INTERNAL_ERROR_MSG);
         conflicts = if conflicts.is_empty() {
             String::new()
@@ -361,7 +369,7 @@ fn write_flags_of(p: &Parser) -> String {
             ""
         };
         if let Some(short) = f.short() {
-            let s = format!("\"{conflicts}{multiple}-{arg}[{help}]\" \\",
+            let s = format!("'{conflicts}{multiple}-{arg}[{help}]' \\",
                 multiple = multiple,
                 conflicts = conflicts,
                 arg = short,
@@ -372,7 +380,7 @@ fn write_flags_of(p: &Parser) -> String {
         }
 
         if let Some(long) = f.long() {
-            let l = format!("\"{conflicts}{multiple}--{arg}[{help}]\" \\",
+            let l = format!("'{conflicts}{multiple}--{arg}[{help}]' \\",
                 conflicts = conflicts,
                 multiple = multiple,
                 arg = long,


Test patch:
diff --git a/tests/completions.rs b/tests/completions.rs
index b429b30a..1afc6bcb 100644
--- a/tests/completions.rs
+++ b/tests/completions.rs
@@ -95,10 +95,10 @@ _myapp() {
 
     local context curcontext="$curcontext" state line
     _arguments -s -S -C \
-"-h[Prints help information]" \
-"--help[Prints help information]" \
-"-V[Prints version information]" \
-"--version[Prints version information]" \
+'-h[Prints help information]' \
+'--help[Prints help information]' \
+'-V[Prints version information]' \
+'--version[Prints version information]' \
 "1:: :_myapp_commands" \
 "*:: :->myapp" \
 && ret=0
@@ -108,19 +108,19 @@ _myapp() {
         case $line[1] in
             (test)
 _arguments -s -S -C \
-"--case+[the case to test]" \
-"-h[Prints help information]" \
-"--help[Prints help information]" \
-"-V[Prints version information]" \
-"--version[Prints version information]" \
+'--case+[the case to test]' \
+'-h[Prints help information]' \
+'--help[Prints help information]' \
+'-V[Prints version information]' \
+'--version[Prints version information]' \
 && ret=0
 ;;
 (help)
 _arguments -s -S -C \
-"-h[Prints help information]" \
-"--help[Prints help information]" \
-"-V[Prints version information]" \
-"--version[Prints version information]" \
+'-h[Prints help information]' \
+'--help[Prints help information]' \
+'-V[Prints version information]' \
+'--version[Prints version information]' \
 && ret=0
 ;;
         esac
@@ -167,15 +167,15 @@ static FISH: &'static str = r#"function __fish_using_command
     return 1
 end
 
-complete -c myapp -n "__fish_using_command myapp" -s h -l help -d "Prints help information"
-complete -c myapp -n "__fish_using_command myapp" -s V -l version -d "Prints version information"
-complete -c myapp -n "__fish_using_command myapp" -f -a "test" -d "tests things"
-complete -c myapp -n "__fish_using_command myapp" -f -a "help" -d "Prints this message or the help of the given subcommand(s)"
-complete -c myapp -n "__fish_using_command myapp test" -l case -d "the case to test"
-complete -c myapp -n "__fish_using_command myapp test" -s h -l help -d "Prints help information"
-complete -c myapp -n "__fish_using_command myapp test" -s V -l version -d "Prints version information"
-complete -c myapp -n "__fish_using_command myapp help" -s h -l help -d "Prints help information"
-complete -c myapp -n "__fish_using_command myapp help" -s V -l version -d "Prints version information"
+complete -c myapp -n "__fish_using_command myapp" -s h -l help -d 'Prints help information'
+complete -c myapp -n "__fish_using_command myapp" -s V -l version -d 'Prints version information'
+complete -c myapp -n "__fish_using_command myapp" -f -a "test" -d 'tests things'
+complete -c myapp -n "__fish_using_command myapp" -f -a "help" -d 'Prints this message or the help of the given subcommand(s)'
+complete -c myapp -n "__fish_using_command myapp test" -l case -d 'the case to test'
+complete -c myapp -n "__fish_using_command myapp test" -s h -l help -d 'Prints help information'
+complete -c myapp -n "__fish_using_command myapp test" -s V -l version -d 'Prints version information'
+complete -c myapp -n "__fish_using_command myapp help" -s h -l help -d 'Prints help information'
+complete -c myapp -n "__fish_using_command myapp help" -s V -l version -d 'Prints version information'
 "#;
 
 #[cfg(not(target_os="windows"))]
@@ -384,10 +384,10 @@ _my_app() {
 
     local context curcontext="$curcontext" state line
     _arguments -s -S -C \
-"-h[Prints help information]" \
-"--help[Prints help information]" \
-"-V[Prints version information]" \
-"--version[Prints version information]" \
+'-h[Prints help information]' \
+'--help[Prints help information]' \
+'-V[Prints version information]' \
+'--version[Prints version information]' \
 "1:: :_my_app_commands" \
 "*:: :->my_app" \
 && ret=0
@@ -397,28 +397,28 @@ _my_app() {
         case $line[1] in
             (test)
 _arguments -s -S -C \
-"--case+[the case to test]" \
-"-h[Prints help information]" \
-"--help[Prints help information]" \
-"-V[Prints version information]" \
-"--version[Prints version information]" \
+'--case+[the case to test]' \
+'-h[Prints help information]' \
+'--help[Prints help information]' \
+'-V[Prints version information]' \
+'--version[Prints version information]' \
 && ret=0
 ;;
 (some_cmd)
 _arguments -s -S -C \
-"--config+[the other case to test]" \
-"-h[Prints help information]" \
-"--help[Prints help information]" \
-"-V[Prints version information]" \
-"--version[Prints version information]" \
+'--config+[the other case to test]' \
+'-h[Prints help information]' \
+'--help[Prints help information]' \
+'-V[Prints version information]' \
+'--version[Prints version information]' \
 && ret=0
 ;;
 (help)
 _arguments -s -S -C \
-"-h[Prints help information]" \
-"--help[Prints help information]" \
-"-V[Prints version information]" \
-"--version[Prints version information]" \
+'-h[Prints help information]' \
+'--help[Prints help information]' \
+'-V[Prints version information]' \
+'--version[Prints version information]' \
 && ret=0
 ;;
         esac
@@ -473,19 +473,19 @@ static FISH_WUS: &'static str = r#"function __fish_using_command
     return 1
 end
 
-complete -c my_app -n "__fish_using_command my_app" -s h -l help -d "Prints help information"
-complete -c my_app -n "__fish_using_command my_app" -s V -l version -d "Prints version information"
-complete -c my_app -n "__fish_using_command my_app" -f -a "test" -d "tests things"
-complete -c my_app -n "__fish_using_command my_app" -f -a "some_cmd" -d "tests other things"
-complete -c my_app -n "__fish_using_command my_app" -f -a "help" -d "Prints this message or the help of the given subcommand(s)"
-complete -c my_app -n "__fish_using_command my_app test" -l case -d "the case to test"
-complete -c my_app -n "__fish_using_command my_app test" -s h -l help -d "Prints help information"
-complete -c my_app -n "__fish_using_command my_app test" -s V -l version -d "Prints version information"
-complete -c my_app -n "__fish_using_command my_app some_cmd" -l config -d "the other case to test"
-complete -c my_app -n "__fish_using_command my_app some_cmd" -s h -l help -d "Prints help information"
-complete -c my_app -n "__fish_using_command my_app some_cmd" -s V -l version -d "Prints version information"
-complete -c my_app -n "__fish_using_command my_app help" -s h -l help -d "Prints help information"
-complete -c my_app -n "__fish_using_command my_app help" -s V -l version -d "Prints version information"
+complete -c my_app -n "__fish_using_command my_app" -s h -l help -d 'Prints help information'
+complete -c my_app -n "__fish_using_command my_app" -s V -l version -d 'Prints version information'
+complete -c my_app -n "__fish_using_command my_app" -f -a "test" -d 'tests things'
+complete -c my_app -n "__fish_using_command my_app" -f -a "some_cmd" -d 'tests other things'
+complete -c my_app -n "__fish_using_command my_app" -f -a "help" -d 'Prints this message or the help of the given subcommand(s)'
+complete -c my_app -n "__fish_using_command my_app test" -l case -d 'the case to test'
+complete -c my_app -n "__fish_using_command my_app test" -s h -l help -d 'Prints help information'
+complete -c my_app -n "__fish_using_command my_app test" -s V -l version -d 'Prints version information'
+complete -c my_app -n "__fish_using_command my_app some_cmd" -l config -d 'the other case to test'
+complete -c my_app -n "__fish_using_command my_app some_cmd" -s h -l help -d 'Prints help information'
+complete -c my_app -n "__fish_using_command my_app some_cmd" -s V -l version -d 'Prints version information'
+complete -c my_app -n "__fish_using_command my_app help" -s h -l help -d 'Prints help information'
+complete -c my_app -n "__fish_using_command my_app help" -s V -l version -d 'Prints version information'
 "#;
 
 static BASH_WUS: &'static str = r#"_my_app() {
@@ -593,6 +593,68 @@ static BASH_WUS: &'static str = r#"_my_app() {
 complete -F _my_app -o bashdefault -o default my_app
 "#;
 
+static FISH_SPECIAL: &'static str = r#"function __fish_using_command
+    set cmd (commandline -opc)
+    if [ (count $cmd) -eq (count $argv) ]
+        for i in (seq (count $argv))
+            if [ $cmd[$i] != $argv[$i] ]
+                return 1
+            end
+        end
+        return 0
+    end
+    return 1
+end
+
+complete -c my_app -n "__fish_using_command my_app" -l single-quotes -d 'Can be \'always\', \'auto\', or \'never\''
+complete -c my_app -n "__fish_using_command my_app" -l double-quotes -d 'Can be "always", "auto", or "never"'
+complete -c my_app -n "__fish_using_command my_app" -l backticks -d 'For more information see `echo test`'
+complete -c my_app -n "__fish_using_command my_app" -l backslash -d 'Avoid \'\\n\''
+complete -c my_app -n "__fish_using_command my_app" -l brackets -d 'List packages [filter]'
+complete -c my_app -n "__fish_using_command my_app" -l expansions -d 'Execute the shell command with $SHELL'
+complete -c my_app -n "__fish_using_command my_app" -s h -l help -d 'Prints help information'
+complete -c my_app -n "__fish_using_command my_app" -s V -l version -d 'Prints version information'
+"#;
+
+static ZSH_SPECIAL: &'static str = r#"#compdef my_app
+
+_my_app() {
+    typeset -A opt_args
+    local ret=1
+
+    local context curcontext="$curcontext" state line
+    _arguments -s -S -C \
+'--single-quotes[Can be '\''always'\'', '\''auto'\'', or '\''never'\'']' \
+'--double-quotes[Can be "always", "auto", or "never"]' \
+'--backticks[For more information see `echo test`]' \
+'--backslash[Avoid '\''\\n'\'']' \
+'--brackets[List packages \[filter\]]' \
+'--expansions[Execute the shell command with $SHELL]' \
+'-h[Prints help information]' \
+'--help[Prints help information]' \
+'-V[Prints version information]' \
+'--version[Prints version information]' \
+&& ret=0
+    
+}
+
+(( $+functions[_my_app_commands] )) ||
+_my_app_commands() {
+    local commands; commands=(
+        
+    )
+    _describe -t commands 'my_app commands' commands "$@"
+}
+(( $+functions[_my_app_commands] )) ||
+_my_app_commands() {
+    local commands; commands=(
+        
+    )
+    _describe -t commands 'my_app commands' commands "$@"
+}
+
+_my_app "$@""#;
+
 fn compare(left: &str, right: &str) -> bool {
     let b = left == right;
     if !b {
@@ -632,6 +694,28 @@ fn build_app_with_underscore() -> App<'static, 'static> {
             .help("the other case to test")))
 }
 
+fn build_app_special() -> App<'static, 'static> {
+    App::new("my_app")
+        .arg(Arg::with_name("single-quotes")
+            .long("single-quotes")
+            .help("Can be 'always', 'auto', or 'never'"))
+        .arg(Arg::with_name("double-quotes")
+            .long("double-quotes")
+            .help("Can be \"always\", \"auto\", or \"never\""))
+        .arg(Arg::with_name("backticks")
+            .long("backticks")
+            .help("For more information see `echo test`"))
+        .arg(Arg::with_name("backslash")
+            .long("backslash")
+            .help("Avoid '\\n'"))
+        .arg(Arg::with_name("brackets")
+            .long("brackets")
+            .help("List packages [filter]"))
+        .arg(Arg::with_name("expansions")
+            .long("expansions")
+            .help("Execute the shell command with $SHELL"))
+}
+
 #[test]
 fn bash() {
     let mut app = build_app();
@@ -713,3 +797,23 @@ fn zsh_with_underscore() {
 
     assert!(compare(&*string, ZSH_WUS));
 }
+
+#[test]
+fn fish_special() {
+    let mut app = build_app_special();
+    let mut buf = vec![];
+    app.gen_completions_to("my_app", Shell::Fish, &mut buf);
+    let string = String::from_utf8(buf).unwrap();
+
+    assert!(compare(&*string, FISH_SPECIAL));
+}
+
+#[test]
+fn zsh_special() {
+    let mut app = build_app_special();
+    let mut buf = vec![];
+    app.gen_completions_to("my_app", Shell::Zsh, &mut buf);
+    let string = String::from_utf8(buf).unwrap();
+
+    assert!(compare(&*string, ZSH_SPECIAL));
+}