Skip to content

Commit

Permalink
fix: escape special characters in zsh and fish completions
Browse files Browse the repository at this point in the history
  • Loading branch information
siiptuo committed Sep 12, 2017
1 parent f2266e2 commit 87e019f
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 62 deletions.
11 changes: 8 additions & 3 deletions src/completions/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down Expand Up @@ -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());
Expand All @@ -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");
Expand All @@ -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");
Expand Down
20 changes: 14 additions & 6 deletions src/completions/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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()
Expand All @@ -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,
Expand All @@ -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,
Expand Down
210 changes: 157 additions & 53 deletions tests/completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"))]
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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));
}

0 comments on commit 87e019f

Please sign in to comment.