1
0
mirror of https://github.com/Llewellynvdm/starship.git synced 2024-11-28 15:56:28 +00:00

fix(escaping): move escaping to individual variables (#3107)

This commit is contained in:
Fred Cox 2021-11-01 14:18:45 -07:00 committed by GitHub
parent 73277d37c6
commit c1f2d345aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 259 additions and 175 deletions

View File

@ -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 ### Options
| Option | Default | Description | | Option | Default | Description |

View File

@ -7,6 +7,7 @@ use std::error::Error;
use std::fmt; use std::fmt;
use crate::config::parse_style_string; use crate::config::parse_style_string;
use crate::context::{Context, Shell};
use crate::segment::Segment; use crate::segment::Segment;
use super::model::*; use super::model::*;
@ -15,6 +16,7 @@ use super::parser::{parse, Rule};
#[derive(Clone)] #[derive(Clone)]
enum VariableValue<'a> { enum VariableValue<'a> {
Plain(Cow<'a, str>), Plain(Cow<'a, str>),
NoEscapingPlain(Cow<'a, str>),
Styled(Vec<Segment>), Styled(Vec<Segment>),
Meta(Vec<FormatElement<'a>>), Meta(Vec<FormatElement<'a>>),
} }
@ -123,6 +125,27 @@ impl<'a> StringFormatter<'a> {
self 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. /// 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 /// 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 /// - Format string in meta variables fails to parse
/// - Variable mapper returns an error. /// - 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>( fn parse_textgroup<'a>(
textgroup: TextGroup<'a>, textgroup: TextGroup<'a>,
variables: &'a VariableMapType<'a>, variables: &'a VariableMapType<'a>,
style_variables: &'a StyleVariableMapType<'a>, style_variables: &'a StyleVariableMapType<'a>,
context: Option<&Context>,
) -> Result<Vec<Segment>, StringFormatterError> { ) -> Result<Vec<Segment>, StringFormatterError> {
let style = parse_style(textgroup.style, style_variables); let style = parse_style(textgroup.style, style_variables);
parse_format( parse_format(
@ -218,6 +246,7 @@ impl<'a> StringFormatter<'a> {
style.transpose()?, style.transpose()?,
variables, variables,
style_variables, style_variables,
context,
) )
} }
@ -252,6 +281,7 @@ impl<'a> StringFormatter<'a> {
style: Option<Style>, style: Option<Style>,
variables: &'a VariableMapType<'a>, variables: &'a VariableMapType<'a>,
style_variables: &'a StyleVariableMapType<'a>, style_variables: &'a StyleVariableMapType<'a>,
context: Option<&Context>,
) -> Result<Vec<Segment>, StringFormatterError> { ) -> Result<Vec<Segment>, StringFormatterError> {
let results: Result<Vec<Vec<Segment>>, StringFormatterError> = format let results: Result<Vec<Vec<Segment>>, StringFormatterError> = format
.into_iter() .into_iter()
@ -263,7 +293,7 @@ impl<'a> StringFormatter<'a> {
format: textgroup.format, format: textgroup.format,
style: textgroup.style, style: textgroup.style,
}; };
parse_textgroup(textgroup, variables, style_variables) parse_textgroup(textgroup, variables, style_variables, context)
} }
FormatElement::Variable(name) => variables FormatElement::Variable(name) => variables
.get(name.as_ref()) .get(name.as_ref())
@ -278,14 +308,26 @@ impl<'a> StringFormatter<'a> {
segment segment
}) })
.collect()), .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) => { VariableValue::Meta(format) => {
let formatter = StringFormatter { let formatter = StringFormatter {
format, format,
variables: clone_without_meta(variables), variables: clone_without_meta(variables),
style_variables: style_variables.clone(), style_variables: style_variables.clone(),
}; };
formatter.parse(style) formatter.parse(style, context)
} }
}) })
.unwrap_or_else(|| Ok(Vec::new())), .unwrap_or_else(|| Ok(Vec::new())),
@ -320,6 +362,9 @@ impl<'a> StringFormatter<'a> {
VariableValue::Plain(plain_value) => { VariableValue::Plain(plain_value) => {
!plain_value.is_empty() !plain_value.is_empty()
} }
VariableValue::NoEscapingPlain(
no_escaping_plain_value,
) => !no_escaping_plain_value.is_empty(),
VariableValue::Styled(segments) => segments VariableValue::Styled(segments) => segments
.iter() .iter()
.any(|x| !x.value().is_empty()), .any(|x| !x.value().is_empty()),
@ -331,7 +376,7 @@ impl<'a> StringFormatter<'a> {
let should_show: bool = should_show_elements(&format, variables); let should_show: bool = should_show_elements(&format, variables);
if should_show { if should_show {
parse_format(format, style, variables, style_variables) parse_format(format, style, variables, style_variables, context)
} else { } else {
Ok(Vec::new()) Ok(Vec::new())
} }
@ -347,6 +392,7 @@ impl<'a> StringFormatter<'a> {
default_style, default_style,
&self.variables, &self.variables,
&self.style_variables, &self.style_variables,
context,
) )
} }
} }
@ -380,6 +426,28 @@ fn clone_without_meta<'a>(variables: &VariableMapType<'a>) -> VariableMapType<'a
.collect() .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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -404,7 +472,7 @@ mod tests {
let style = Some(Color::Red.bold()); let style = Some(Color::Red.bold());
let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper); 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(); let mut result_iter = result.iter();
match_next!(result_iter, "text", style); match_next!(result_iter, "text", style);
} }
@ -413,7 +481,7 @@ mod tests {
fn test_textgroup_text_only() { fn test_textgroup_text_only() {
const FORMAT_STR: &str = "[text](red bold)"; const FORMAT_STR: &str = "[text](red bold)";
let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper); 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(); let mut result_iter = result.iter();
match_next!(result_iter, "text", Some(Color::Red.bold())); match_next!(result_iter, "text", Some(Color::Red.bold()));
} }
@ -428,7 +496,7 @@ mod tests {
"var1" => Some(Ok("text1".to_owned())), "var1" => Some(Ok("text1".to_owned())),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
let mut result_iter = result.iter(); let mut result_iter = result.iter();
match_next!(result_iter, "text1", None); match_next!(result_iter, "text1", None);
} }
@ -444,7 +512,7 @@ mod tests {
"style" => Some(Ok("red bold".to_owned())), "style" => Some(Ok("red bold".to_owned())),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
let mut result_iter = result.iter(); let mut result_iter = result.iter();
match_next!(result_iter, "root", root_style); match_next!(result_iter, "root", root_style);
} }
@ -456,7 +524,7 @@ mod tests {
let formatter = StringFormatter::new(FORMAT_STR) let formatter = StringFormatter::new(FORMAT_STR)
.unwrap() .unwrap()
.map(|variable| Some(Ok(format!("${{{}}}", variable)))); .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(); let mut result_iter = result.iter();
match_next!(result_iter, "${env:PWD}", None); match_next!(result_iter, "${env:PWD}", None);
} }
@ -466,7 +534,7 @@ mod tests {
const FORMAT_STR: &str = r#"\\\[\$text\]\(red bold\)"#; const FORMAT_STR: &str = r#"\\\[\$text\]\(red bold\)"#;
let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper); 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(); let mut result_iter = result.iter();
match_next!(result_iter, r#"\[$text](red bold)"#, None); match_next!(result_iter, r#"\[$text](red bold)"#, None);
} }
@ -479,7 +547,7 @@ mod tests {
let inner_style = Some(Color::Blue.normal()); let inner_style = Some(Color::Blue.normal());
let formatter = StringFormatter::new(FORMAT_STR).unwrap().map(empty_mapper); 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(); let mut result_iter = result.iter();
match_next!(result_iter, "outer ", outer_style); match_next!(result_iter, "outer ", outer_style);
match_next!(result_iter, "middle ", middle_style); match_next!(result_iter, "middle ", middle_style);
@ -497,7 +565,7 @@ mod tests {
"var" => Some(Ok("text".to_owned())), "var" => Some(Ok("text".to_owned())),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
let mut result_iter = result.iter(); let mut result_iter = result.iter();
match_next!(result_iter, "text", var_style); match_next!(result_iter, "text", var_style);
} }
@ -523,7 +591,7 @@ mod tests {
"var" => Some(Ok(segments.clone())), "var" => Some(Ok(segments.clone())),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
let mut result_iter = result.iter(); let mut result_iter = result.iter();
match_next!(result_iter, "styless", var_style); match_next!(result_iter, "styless", var_style);
match_next!(result_iter, "styled", styled_style); match_next!(result_iter, "styled", styled_style);
@ -546,7 +614,7 @@ mod tests {
"b" => Some(Ok("$b")), "b" => Some(Ok("$b")),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
let mut result_iter = result.iter(); let mut result_iter = result.iter();
match_next!(result_iter, "$a", None); match_next!(result_iter, "$a", None);
match_next!(result_iter, "$b", None); match_next!(result_iter, "$b", None);
@ -568,7 +636,7 @@ mod tests {
"c" => Some(Ok("$c")), "c" => Some(Ok("$c")),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
let mut result_iter = result.iter(); let mut result_iter = result.iter();
match_next!(result_iter, "$a", None); match_next!(result_iter, "$a", None);
match_next!(result_iter, "$b", None); match_next!(result_iter, "$b", None);
@ -585,7 +653,7 @@ mod tests {
"some" => Some(Ok("$some")), "some" => Some(Ok("$some")),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
let mut result_iter = result.iter(); let mut result_iter = result.iter();
match_next!(result_iter, "$some", None); match_next!(result_iter, "$some", None);
match_next!(result_iter, " should render but ", None); match_next!(result_iter, " should render but ", None);
@ -602,7 +670,7 @@ mod tests {
"empty" => Some(Ok("")), "empty" => Some(Ok("")),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
assert_eq!(result.len(), 0); assert_eq!(result.len(), 0);
} }
@ -616,7 +684,7 @@ mod tests {
"empty" => Some(Ok("")), "empty" => Some(Ok("")),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
assert_eq!(result.len(), 0); assert_eq!(result.len(), 0);
} }
@ -630,7 +698,7 @@ mod tests {
"some" => Some(Ok("$some")), "some" => Some(Ok("$some")),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
let mut result_iter = result.iter(); let mut result_iter = result.iter();
match_next!(result_iter, "$some", None); match_next!(result_iter, "$some", None);
match_next!(result_iter, " ", None); match_next!(result_iter, " ", None);
@ -649,7 +717,7 @@ mod tests {
"all" => Some("$some"), "all" => Some("$some"),
_ => None, _ => None,
}); });
let result = formatter.parse(None).unwrap(); let result = formatter.parse(None, None).unwrap();
let mut result_iter = result.iter(); let mut result_iter = result.iter();
match_next!(result_iter, " ", None); match_next!(result_iter, " ", None);
} }
@ -703,8 +771,50 @@ mod tests {
"never" => Some(Err(never_error.clone())), "never" => Some(Err(never_error.clone())),
_ => None, _ => None,
}) })
.parse(None) .parse(None, None)
}); });
assert!(segments.is_err()); 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
);
}
} }

View File

@ -51,7 +51,7 @@ impl<'a> VersionFormatter<'a> {
}, },
_ => None, _ => None,
}) })
.parse(None); .parse(None, None);
formatted.map(|segments| { formatted.map(|segments| {
segments segments

View File

@ -164,7 +164,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"duration" => duration.as_ref().map(Ok), "duration" => duration.as_ref().map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -52,7 +52,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
_ => None, _ => None,
}); });
match formatter.parse(None) { match formatter.parse(None, Some(context)) {
Ok(format_string) => { Ok(format_string) => {
module.set_segments(format_string); module.set_segments(format_string);
Some(module) Some(module)

View File

@ -53,7 +53,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"symbol" => Some(symbol), "symbol" => Some(symbol),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -37,7 +37,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"duration" => Some(Ok(render_time(elapsed, config.show_milliseconds))), "duration" => Some(Ok(render_time(elapsed, config.show_milliseconds))),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -37,7 +37,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"environment" => Some(Ok(conda_env.as_str())), "environment" => Some(Ok(conda_env.as_str())),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -44,7 +44,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
.map(Ok), .map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -56,7 +56,7 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
"style" => Some(Ok(config.style)), "style" => Some(Ok(config.style)),
_ => None, _ => None,
}) })
.map(|variable| match variable { .map_no_escaping(|variable| match variable {
"output" => { "output" => {
let output = exec_command(config.command, &config.shell.0)?; let output = exec_command(config.command, &config.shell.0)?;
let trimmed = output.trim(); let trimmed = output.trim();
@ -69,7 +69,7 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
match parsed { match parsed {

View File

@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -42,7 +42,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -113,7 +113,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -71,7 +71,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"context" => Some(Ok(ctx.as_str())), "context" => Some(Ok(ctx.as_str())),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -74,7 +74,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"tfm" => find_current_tfm(&dotnet_files).map(Ok), "tfm" => find_current_tfm(&dotnet_files).map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -55,7 +55,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
.map(Ok), .map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -42,7 +42,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -68,7 +68,7 @@ fn env_var_module<'a>(module_config_path: Vec<&str>, context: &'a Context) -> Op
"env_value" => Some(Ok(&env_value)), "env_value" => Some(Ok(&env_value)),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -42,7 +42,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -151,7 +151,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"active" => Some(Ok(gcloud_context.config_name.clone())), "active" => Some(Ok(gcloud_context.config_name.clone())),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -95,7 +95,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -68,7 +68,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"tag" => format_tag(config.tag_symbol, &tag_name), "tag" => format_tag(config.tag_symbol, &tag_name),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -50,7 +50,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"deleted" => GitDiff::get_variable(config.only_nonzero_diffs, stats.deleted), "deleted" => GitDiff::get_variable(config.only_nonzero_diffs, stats.deleted),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -34,7 +34,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"progress_total" => state_description.total.as_ref().map(Ok), "progress_total" => state_description.total.as_ref().map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -49,49 +49,52 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let info = Arc::clone(&info); let info = Arc::clone(&info);
let segments = match variable { let segments = match variable {
"stashed" => info.get_stashed().and_then(|count| { "stashed" => info.get_stashed().and_then(|count| {
format_count(config.stashed, "git_status.stashed", count) format_count(config.stashed, "git_status.stashed", context, count)
}), }),
"ahead_behind" => info.get_ahead_behind().and_then(|(ahead, behind)| { "ahead_behind" => info.get_ahead_behind().and_then(|(ahead, behind)| {
let (ahead, behind) = (ahead?, behind?); let (ahead, behind) = (ahead?, behind?);
if ahead > 0 && behind > 0 { if ahead > 0 && behind > 0 {
format_text(config.diverged, "git_status.diverged", |variable| { format_text(
match variable { config.diverged,
"git_status.diverged",
context,
|variable| match variable {
"ahead_count" => Some(ahead.to_string()), "ahead_count" => Some(ahead.to_string()),
"behind_count" => Some(behind.to_string()), "behind_count" => Some(behind.to_string()),
_ => None, _ => None,
} },
}) )
} else if ahead > 0 && behind == 0 { } else if ahead > 0 && behind == 0 {
format_count(config.ahead, "git_status.ahead", ahead) format_count(config.ahead, "git_status.ahead", context, ahead)
} else if behind > 0 && ahead == 0 { } else if behind > 0 && ahead == 0 {
format_count(config.behind, "git_status.behind", behind) format_count(config.behind, "git_status.behind", context, behind)
} else { } else {
format_symbol(config.up_to_date, "git_status.up_to_date") format_symbol(config.up_to_date, "git_status.up_to_date", context)
} }
}), }),
"conflicted" => info.get_conflicted().and_then(|count| { "conflicted" => info.get_conflicted().and_then(|count| {
format_count(config.conflicted, "git_status.conflicted", count) format_count(config.conflicted, "git_status.conflicted", context, count)
}), }),
"deleted" => info.get_deleted().and_then(|count| { "deleted" => info.get_deleted().and_then(|count| {
format_count(config.deleted, "git_status.deleted", count) format_count(config.deleted, "git_status.deleted", context, count)
}), }),
"renamed" => info.get_renamed().and_then(|count| { "renamed" => info.get_renamed().and_then(|count| {
format_count(config.renamed, "git_status.renamed", count) format_count(config.renamed, "git_status.renamed", context, count)
}), }),
"modified" => info.get_modified().and_then(|count| { "modified" => info.get_modified().and_then(|count| {
format_count(config.modified, "git_status.modified", count) format_count(config.modified, "git_status.modified", context, count)
}),
"staged" => info.get_staged().and_then(|count| {
format_count(config.staged, "git_status.staged", context, count)
}), }),
"staged" => info
.get_staged()
.and_then(|count| format_count(config.staged, "git_status.staged", count)),
"untracked" => info.get_untracked().and_then(|count| { "untracked" => info.get_untracked().and_then(|count| {
format_count(config.untracked, "git_status.untracked", count) format_count(config.untracked, "git_status.untracked", context, count)
}), }),
_ => None, _ => None,
}; };
segments.map(Ok) segments.map(Ok)
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {
@ -288,14 +291,19 @@ impl RepoStatus {
} }
} }
fn format_text<F>(format_str: &str, config_path: &str, mapper: F) -> Option<Vec<Segment>> fn format_text<F>(
format_str: &str,
config_path: &str,
context: &Context,
mapper: F,
) -> Option<Vec<Segment>>
where where
F: Fn(&str) -> Option<String> + Send + Sync, F: Fn(&str) -> Option<String> + Send + Sync,
{ {
if let Ok(formatter) = StringFormatter::new(format_str) { if let Ok(formatter) = StringFormatter::new(format_str) {
formatter formatter
.map(|variable| mapper(variable).map(Ok)) .map(|variable| mapper(variable).map(Ok))
.parse(None) .parse(None, Some(context))
.ok() .ok()
} else { } else {
log::warn!("Error parsing format string `{}`", &config_path); log::warn!("Error parsing format string `{}`", &config_path);
@ -303,19 +311,29 @@ where
} }
} }
fn format_count(format_str: &str, config_path: &str, count: usize) -> Option<Vec<Segment>> { fn format_count(
format_str: &str,
config_path: &str,
context: &Context,
count: usize,
) -> Option<Vec<Segment>> {
if count == 0 { if count == 0 {
return None; return None;
} }
format_text(format_str, config_path, |variable| match variable { format_text(
format_str,
config_path,
context,
|variable| match variable {
"count" => Some(count.to_string()), "count" => Some(count.to_string()),
_ => None, _ => None,
}) },
)
} }
fn format_symbol(format_str: &str, config_path: &str) -> Option<Vec<Segment>> { fn format_symbol(format_str: &str, config_path: &str, context: &Context) -> Option<Vec<Segment>> {
format_text(format_str, config_path, |_variable| None) format_text(format_str, config_path, context, |_variable| None)
} }
#[cfg(test)] #[cfg(test)]

View File

@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -46,7 +46,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -60,7 +60,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"branch" => Some(Ok(truncated_and_symbol.as_str())), "branch" => Some(Ok(truncated_and_symbol.as_str())),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -51,7 +51,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"hostname" => Some(Ok(host)), "hostname" => Some(Ok(host)),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -45,7 +45,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -88,7 +88,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"number" => Some(Ok(module_number.clone())), "number" => Some(Ok(module_number.clone())),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -46,7 +46,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -104,7 +104,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"namespace" => kube_ns.as_ref().map(|s| Ok(Cow::Borrowed(s.as_str()))), "namespace" => kube_ns.as_ref().map(|s| Ok(Cow::Borrowed(s.as_str()))),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -45,7 +45,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -86,7 +86,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"swap_pct" if total_swap_kib > 0 => Some(Ok(&swap_pct)), "swap_pct" if total_swap_kib > 0 => Some(Ok(&swap_pct)),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -44,7 +44,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
.map(Ok), .map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -46,7 +46,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"name" => shell_name.as_ref().map(Ok), "name" => shell_name.as_ref().map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -73,7 +73,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -73,7 +73,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -69,7 +69,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"project" => osp_project.as_ref().map(Ok), "project" => osp_project.as_ref().map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -29,7 +29,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"version" => Some(Ok(&module_version)), "version" => Some(Ok(&module_version)),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -42,7 +42,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"stack" => stack_name(&project_file, context).map(Ok), "stack" => stack_name(&project_file, context).map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
match parsed { match parsed {

View File

@ -41,7 +41,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -61,7 +61,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"pyenv_prefix" => Some(Ok(pyenv_prefix.to_string())), "pyenv_prefix" => Some(Ok(pyenv_prefix.to_string())),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -41,7 +41,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
.map(Ok), .map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -41,7 +41,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
.map(Ok), .map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -42,7 +42,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"version" => get_module_version(context, &config).map(Ok), "version" => get_module_version(context, &config).map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -42,7 +42,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -46,7 +46,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"unknown_indicator" => Some(Ok(config.unknown_indicator)), "unknown_indicator" => Some(Ok(config.unknown_indicator)),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -47,7 +47,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"shlvl" => Some(Ok(shlvl_str)), "shlvl" => Some(Ok(shlvl_str)),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -26,7 +26,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"env" => Some(Ok(&singularity_env)), "env" => Some(Ok(&singularity_env)),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -61,7 +61,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
PipeStatusStatus::Pipe(pipestatus) => pipestatus PipeStatusStatus::Pipe(pipestatus) => pipestatus
.iter() .iter()
.map( .map(
|ec| match format_exit_code(ec.as_str(), config.format, None, &config) { |ec| match format_exit_code(ec.as_str(), config.format, None, &config, context) {
Ok(segments) => segments Ok(segments) => segments
.into_iter() .into_iter()
.map(|s| s.to_string()) .map(|s| s.to_string())
@ -79,7 +79,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
PipeStatusStatus::Pipe(_) => config.pipestatus_format, PipeStatusStatus::Pipe(_) => config.pipestatus_format,
_ => config.format, _ => config.format,
}; };
let parsed = format_exit_code(exit_code, main_format, Some(&pipestatus), &config); let parsed = format_exit_code(exit_code, main_format, Some(&pipestatus), &config, context);
module.set_segments(match parsed { module.set_segments(match parsed {
Ok(segments) => segments, Ok(segments) => segments,
@ -96,6 +96,7 @@ fn format_exit_code<'a>(
format: &'a str, format: &'a str,
pipestatus: Option<&str>, pipestatus: Option<&str>,
config: &'a StatusConfig, config: &'a StatusConfig,
context: &'a Context,
) -> Result<Vec<Segment>, StringFormatterError> { ) -> Result<Vec<Segment>, StringFormatterError> {
let exit_code_int: ExitCode = match exit_code.parse() { let exit_code_int: ExitCode = match exit_code.parse() {
Ok(i) => i, Ok(i) => i,
@ -164,7 +165,7 @@ fn format_exit_code<'a>(
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}) })
} }

View File

@ -43,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -49,7 +49,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"workspace" => get_terraform_workspace(context).map(Ok), "workspace" => get_terraform_workspace(context).map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -54,7 +54,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"time" => Some(Ok(&formatted_time_string)), "time" => Some(Ok(&formatted_time_string)),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -48,7 +48,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"user" => Some(Ok(&username)), "user" => Some(Ok(&username)),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {
Ok(segments) => segments, Ok(segments) => segments,

View File

@ -44,7 +44,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
.map(Ok), .map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -30,7 +30,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"repo" => Some(Ok(&repo)), "repo" => Some(Ok(&repo)),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -42,7 +42,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
.map(Ok), .map(Ok),
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -42,7 +42,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
_ => None, _ => None,
}) })
.parse(None) .parse(None, Some(context))
}); });
module.set_segments(match parsed { module.set_segments(match parsed {

View File

@ -109,7 +109,7 @@ pub fn get_prompt(context: Context) -> String {
let mut root_module = Module::new("Starship Root", "The root module", None); let mut root_module = Module::new("Starship Root", "The root module", None);
root_module.set_segments( root_module.set_segments(
formatter formatter
.parse(None) .parse(None, Some(&context))
.expect("Unexpected error returned in root format variables"), .expect("Unexpected error returned in root format variables"),
); );

View File

@ -318,25 +318,8 @@ CMake suite maintained and supported by Kitware (kitware.com/cmake).\n",
} }
} }
/// Wraps ANSI color escape sequences and other interpretable characters /// Wraps ANSI color escape sequences in the shell-appropriate wrappers.
/// that need to be escaped in the shell-appropriate wrappers. pub fn wrap_colorseq_for_shell(ansi: String, shell: Shell) -> String {
pub fn wrap_colorseq_for_shell(mut ansi: String, shell: Shell) -> String {
// Handle other interpretable characters
match shell {
// Bash might interepret baskslashes, backticks and $
// see #658 for more details
Shell::Bash => {
ansi = ansi.replace('\\', r"\\");
ansi = ansi.replace('$', r"\$");
ansi = ansi.replace('`', r"\`");
}
Shell::Zsh => {
// % is an escape in zsh, see PROMPT in `man zshmisc`
ansi = ansi.replace('%', "%%");
}
_ => {}
};
const ESCAPE_BEGIN: char = '\u{1b}'; const ESCAPE_BEGIN: char = '\u{1b}';
const ESCAPE_END: char = 'm'; const ESCAPE_END: char = 'm';
wrap_seq_for_shell(ansi, shell, ESCAPE_BEGIN, ESCAPE_END) wrap_seq_for_shell(ansi, shell, ESCAPE_BEGIN, ESCAPE_END)
@ -674,47 +657,6 @@ mod tests {
assert_eq!(&bresult5, ""); assert_eq!(&bresult5, "");
} }
#[test]
fn test_bash_escape() {
let test = "$(echo a)";
assert_eq!(
wrap_colorseq_for_shell(test.to_owned(), Shell::Bash),
r"\$(echo a)"
);
assert_eq!(
wrap_colorseq_for_shell(test.to_owned(), Shell::PowerShell),
test
);
let test = r"\$(echo a)";
assert_eq!(
wrap_colorseq_for_shell(test.to_owned(), Shell::Bash),
r"\\\$(echo a)"
);
assert_eq!(
wrap_colorseq_for_shell(test.to_owned(), Shell::PowerShell),
test
);
let test = r"`echo a`";
assert_eq!(
wrap_colorseq_for_shell(test.to_owned(), Shell::Bash),
r"\`echo a\`"
);
assert_eq!(
wrap_colorseq_for_shell(test.to_owned(), Shell::PowerShell),
test
);
}
#[test]
fn test_zsh_escape() {
let test = "10%";
assert_eq!(wrap_colorseq_for_shell(test.to_owned(), Shell::Zsh), "10%%");
assert_eq!(
wrap_colorseq_for_shell(test.to_owned(), Shell::PowerShell),
test
);
}
#[test] #[test]
fn test_get_command_string_output() { fn test_get_command_string_output() {
let case1 = CommandOutput { let case1 = CommandOutput {