diff options
author | Fred Cox <mcfedr@gmail.com> | 2021-11-01 14:18:45 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-01 22:18:45 +0100 |
commit | c1f2d345aac0b0241ea1b6d99977fea20fa3f5bb (patch) | |
tree | b6828442ee1aaef179b30d65007d79b9f47bfe9e | |
parent | 73277d37c62e6d3bff15e0125796a9cef6346318 (diff) |
fix(escaping): move escaping to individual variables (#3107)
68 files changed, 259 insertions, 175 deletions
diff --git a/docs/config/README.md b/docs/config/README.md index 579467e94..7739d1b61 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -3319,6 +3319,19 @@ If you have an interesting example not covered there, feel free to share it ther ::: +::: warning Command output is printed unescaped to the prompt + +Whatever output the command generates is printed unmodified in the prompt. This means if the output +contains special sequences that are interpreted by your shell they will be expanded when displayed. +These special sequences are shell specific, e.g. you can write a command module that writes bash sequences, +e.g. `\h`, but this module will not work in a fish or zsh shell. + +Format strings can also contain shell specific prompt sequences, e.g. +[Bash](https://www.gnu.org/software/bash/manual/html_node/Controlling-the-Prompt.html), +[Zsh](https://zsh.sourceforge.io/Doc/Release/Prompt-Expansion.html). + +::: + ### Options | Option | Default | Description | diff --git a/src/formatter/string_formatter.rs b/src/formatter/string_formatter.rs index 7778bb6ad..fb890b29f 100644 --- a/src/formatter/string_formatter.rs +++ b/src/formatter/string_formatter.rs @@ -7,6 +7,7 @@ use std::error::Error; use std::fmt; use crate::config::parse_style_string; +use crate::context::{Context, Shell}; use crate::segment::Segment; use super::model::*; @@ -15,6 +16,7 @@ use super::parser::{parse, Rule}; #[derive(Clone)] enum VariableValue<'a> { Plain(Cow<'a, str>), + NoEscapingPlain(Cow<'a, str>), Styled(Vec<Segment>), Meta(Vec<FormatElement<'a>>), } @@ -123,6 +125,27 @@ impl<'a> StringFormatter<'a> { self } + /// Maps variable name into a value which is wrapped to prevent escaping later + /// + /// This should be used for variables that should not be escaped before inclusion in the prompt + /// + /// See `StringFormatter::map` for description on the parameters. + /// + pub fn map_no_escaping<T, M>(mut self, mapper: M) -> Self + where + T: Into<Cow<'a, str>>, + M: Fn(&str) -> Option<Result<T, StringFormatterError>> + Sync, + { + self.variables + .par_iter_mut() + .filter(|(_, value)| value.is_none()) + .for_each(|(key, value)| { + *value = mapper(key) + .map(|var| var.map(|var| VariableValue::NoEscapingPlain(var.into()))); + }); + self + } + /// Maps a meta-variable to a format string containing other variables. /// /// This function should be called **before** other map methods so that variables found in @@ -206,11 +229,16 @@ impl<'a> StringFormatter<'a> { /// /// - Format string in meta variables fails to parse /// - Variable mapper returns an error. - pub fn parse(self, default_style: Option<Style>) -> Result<Vec<Segment>, StringFormatterError> { + pub fn parse( + self, + default_style: Option<Style>, + context: Option<&Context>, + ) -> Result<Vec<Segment>, StringFormatterError> { fn parse_textgroup<'a>( textgroup: TextGroup<'a>, variables: &'a VariableMapType<'a>, style_variables: &'a StyleVariableMapType<'a>, + context: Option<&Context>, ) -> Result<Vec<Segment>, StringFormatterError> { let style = parse_style(textgroup.style, style_variables); parse_format( @@ -218,6 +246,7 @@ impl<'a> StringFormatter<'a> { style.transpose()?, variables, style_variables, + context, ) } @@ -252,6 +281,7 @@ impl<'a> StringFormatter<'a> { style: Option<Style>, variables: &'a VariableMapType<'a>, style_variables: &'a StyleVariableMapType<'a>, + context: Option<&Context>, ) -> Result<Vec<Segment>, StringFormatterError> { let results: Result<Vec<Vec<Segment>>, StringFormatterError> = format .into_iter() @@ -263,7 +293,7 @@ impl<'a> StringFormatter<'a> { format: textgroup.format, style: textgroup.style, }; - parse_textgroup(textgroup, variables, style_variables) + parse_textgroup(textgroup, variables, style_variables, context) } FormatElement::Variable(name) => variables .get(name.as_ref()) @@ -278,14 +308,26 @@ impl<'a> StringFormatter<'a> { segment }) .collect()), - VariableValue::Plain(text) => Ok(Segment::from_text(style, text)), + VariableValue::Plain(text) => Ok(Segment::from_text( + style, + shell_prompt_escape( + text, + match context { + None => Shell::Unknown, + Some(c) => c.shell, + }, + ), + )), + VariableValue::NoEscapingPlain(text) => { + Ok(Segment::from_text(style, text)) + } VariableValue::Meta(format) => { let formatter = StringFormatter { format, variables: clone_without_meta(variables), style_variables: style_variables.clone(), }; - formatter.parse(style) + formatter.parse(style, context) } }) .unwrap_or_else(|| Ok(Vec::new())), @@ -320,6 +362,9 @@ impl<'a> StringFormatter<'a> { VariableValue::Plain(plain_value) => { !plain_value.is_empty() } + VariableValue::NoEscapingPlain( + no_escaping_plain_value, + ) => !no_escaping_plain_value.is_empty(), VariableValue::Styled(segments) => segments .iter() .any(|x| !x.value().is_empty()), @@ -331,7 +376,7 @@ impl<'a> StringFormatter<'a> { let should_show: bool = should_show_elements(&format, variables); if should_show { - parse_format(format, style, variables, style_variables) + parse_format(format, style, variables, style_variables, context) } else { Ok(Vec::new()) } @@ -347,6 +392,7 @@ impl<'a> StringFormatter<'a> { default_style, &self.variables, &self.style_variables, + context, ) } } @@ -380,6 +426,28 @@ fn clone_without_meta<'a>(variables: &VariableMapType<'a>) -> VariableMapType<'a .collect() } +/// Escape interpretable characters for the shell prompt +pub fn shell_prompt_escape<T>(text: T, shell: Shell) -> String +where + T: Into<String>, +{ + // Handle other interpretable characters + match shell { + // Bash might interepret baskslashes, backticks and $ + // see #658 for more details + Shell::Bash => text + .into() + .replace('\\', r"\\") + .replace('$', r"\$") + .replace('`', r"\`"), + Shell::Zsh => { + // % is an escape in zsh, see PROMPT in `man zshmisc` + text.into().replace('%', "%%") + } + _ => text.into(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -404,7 +472,7 @@ mod tests { let style = Some(Color::Red.bold()); let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper); - let result = formatter.parse(style).unwrap(); + let result = formatter.parse(style, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "text", style); } @@ -413,7 +481,7 @@ mod tests { fn test_textgroup_text_only() { const FORMAT_STR: &str = "[text](red bold)"; let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "text", Some(Color::Red.bold())); } @@ -428,7 +496,7 @@ mod tests { "var1" => Some(Ok("text1".to_owned())), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "text1", None); } @@ -444,7 +512,7 @@ mod tests { "style" => Some(Ok("red bold".to_owned())), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "root", root_style); } @@ -456,7 +524,7 @@ mod tests { let formatter = StringFormatter::new(FORMAT_STR) .unwrap() .map(|variable| Some(Ok(format!("${{{}}}", variable)))); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "${env:PWD}", None); } @@ -466,7 +534,7 @@ mod tests { const FORMAT_STR: &str = r#"\\\[\$text\]\(red bold\)"#; let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, r#"\[$text](red bold)"#, None); } @@ -479,7 +547,7 @@ mod tests { let inner_style = Some(Color::Blue.normal()); let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper); - let result = formatter.parse(outer_style).unwrap(); + let result = formatter.parse(outer_style, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "outer ", outer_style); match_next!(result_iter, "middle ", middle_style); @@ -497,7 +565,7 @@ mod tests { "var" => Some(Ok("text".to_owned())), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "text", var_style); } @@ -523,7 +591,7 @@ mod tests { "var" => Some(Ok(segments.clone())), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "styless", var_style); match_next!(result_iter, "styled", styled_style); @@ -546,7 +614,7 @@ mod tests { "b" => Some(Ok("$b")), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "$a", None); match_next!(result_iter, "$b", None); @@ -568,7 +636,7 @@ mod tests { "c" => Some(Ok("$c")), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "$a", None); match_next!(result_iter, "$b", None); @@ -585,7 +653,7 @@ mod tests { "some" => Some(Ok("$some")), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "$some", None); match_next!(result_iter, " should render but ", None); @@ -602,7 +670,7 @@ mod tests { "empty" => Some(Ok("")), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); assert_eq!(result.len(), 0); } @@ -616,7 +684,7 @@ mod tests { "empty" => Some(Ok("")), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); assert_eq!(result.len(), 0); } @@ -630,7 +698,7 @@ mod tests { "some" => Some(Ok("$some")), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, "$some", None); match_next!(result_iter, " ", None); @@ -649,7 +717,7 @@ mod tests { "all" => Some("$some"), _ => None, }); - let result = formatter.parse(None).unwrap(); + let result = formatter.parse(None, None).unwrap(); let mut result_iter = result.iter(); match_next!(result_iter, " ", None); } @@ -703,8 +771,50 @@ mod tests { "never" => Some(Err(never_error.clone())), _ => None, }) - .parse(None) + .parse(None, None) }); assert!(segments.is_err()); } + + #[test] + fn test_bash_escape() { + let test = "$(echo a)"; + assert_eq!( + shell_prompt_escape(test.to_owned(), Shell::Bash), + r"\$(echo a)" + ); + assert_eq!( + shell_prompt_escape(test.to_owned(), Shell::PowerShell), + test + ); + + let test = r"\$(echo a)"; + assert_eq!( + shell_prompt_escape(test.to_owned(), Shell::Bash), + r"\\\$(echo a)" + ); + assert_eq!( + shell_prompt_escape(test.to_owned(), Shell::PowerShell), + test + ); + + let test = r"`echo a`"; + assert_eq!( + shell_prompt_escape(test.to_owned(), Shell::Bash), + r"\`echo a\`" + ); + assert_eq!( + shell_prompt_escape(test.to_owned(), Shell::PowerShell), + test + ); + } + #[test] + fn test_zsh_escape() { + let test = "10%"; + assert_eq!(shell_prompt_escape(test.to_owned(), Shell::Zsh), "10%%"); + assert_eq!( + shell_prompt_escape(test.to_owned(), Shell::PowerShell), + test + ); + } } diff --git a/src/formatter/version.rs b/src/formatter/version.rs index 2f26d22c0..b88821c17 100644 --- a/src/formatter/version.rs +++ b/src/formatter/version.rs @@ -51,7 +51,7 @@ impl<'a> VersionFormatter<'a> { }, _ => None, }) - .parse(None); + .parse(None, None); formatted.map(|segments| { segments diff --git a/src/modules/aws.rs b/src/modules/aws.rs index e86abb3fb..7abc79715 100644 --- a/src/modules/aws.rs +++ b/src/modules/aws.rs @@ -164,7 +164,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { "duration" => duration.as_ref().map(Ok), _ => None, }) - .parse(None) + .parse(None, Some(context)) }); module.set_segments(match parsed { diff --git a/src/modules/battery.rs b/src/modules/battery.rs index 95cd8aa30..90cbaef35 100644 --- a/src/modules/battery.rs +++ b/src/modules/battery.rs @@ -52,7 +52,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { _ => None, }); - match formatter.parse(None) { + match formatter.parse(None, Some(context)) { Ok(format_string) => { module.set_segments(format_string); Some(module) diff --git a/src/modules/character.rs b/src/modules/character.rs index 6ab74a490..bf60f7729 100644 --- a/src/modules/character.rs +++ b/src/modules/character.rs @@ -53,7 +53,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { "symbol" => Some(symbol), _ => None, }) - .parse(None) + .parse(None, Some(context)) }); module.set_segments(match parsed { diff --git a/src/modules/cmake.rs b/src/modules/cmake.rs index 237fd53c2..c5c58a337 100644 --- a/src/modules/cmake.rs +++ b/src/modules/cmake.rs @@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { } _ => None, }) - .parse(None) + .parse(None, Some(context)) }); module.set_segments(match parsed { diff --git a/src/modules/cmd_duration.rs b/src/modules/cmd_duration.rs index a6556ea62..f15d349db 100644 --- a/src/modules/cmd_duration.rs +++ b/src/modules/cmd_duration.rs @@ -37,7 +37,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { "duration" => Some(Ok(render_time(elapsed, config.show_milliseconds))), _ => None, }) - .parse(None) + .parse(None, Some(context)) }); module.set_segments(match parsed { diff --git a/src/modules/cobol.rs b/src/modules/cobol.rs index 7f0642c35..a2ebe3e32 100644 --- a/src/modules/cobol.rs +++ b/src/modules/cobol.rs @@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { } _ => None, }) - .parse(None) + .parse(None, Some(context)) }) |