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
| Option | Default | Description |

View File

@ -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
);
}
}

View File

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

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

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))),
_ => None,
})
.parse(None)
.parse(None, Some(context))
});
module.set_segments(match parsed {

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -95,7 +95,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
}
_ => None,
})
.parse(None)
.parse(None, Some(context))
});
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),
_ => None,
})
.parse(None)
.parse(None, Some(context))
});
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),
_ => None,
})
.parse(None)
.parse(None, Some(context))
});
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),
_ => None,
})
.parse(None)
.parse(None, Some(context))
});
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 segments = match variable {
"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)| {
let (ahead, behind) = (ahead?, behind?);
if ahead > 0 && behind > 0 {
format_text(config.diverged, "git_status.diverged", |variable| {
match variable {
format_text(
config.diverged,
"git_status.diverged",
context,
|variable| match variable {
"ahead_count" => Some(ahead.to_string()),
"behind_count" => Some(behind.to_string()),
_ => None,
}
})
},
)
} 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 {
format_count(config.behind, "git_status.behind", behind)
format_count(config.behind, "git_status.behind", context, behind)
} 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| {
format_count(config.conflicted, "git_status.conflicted", count)
format_count(config.conflicted, "git_status.conflicted", context, 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| {
format_count(config.renamed, "git_status.renamed", count)
format_count(config.renamed, "git_status.renamed", context, 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| {
format_count(config.untracked, "git_status.untracked", count)
format_count(config.untracked, "git_status.untracked", context, count)
}),
_ => None,
};
segments.map(Ok)
})
.parse(None)
.parse(None, Some(context))
});
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
F: Fn(&str) -> Option<String> + Send + Sync,
{
if let Ok(formatter) = StringFormatter::new(format_str) {
formatter
.map(|variable| mapper(variable).map(Ok))
.parse(None)
.parse(None, Some(context))
.ok()
} else {
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 {
return None;
}
format_text(format_str, config_path, |variable| match variable {
"count" => Some(count.to_string()),
_ => None,
})
format_text(
format_str,
config_path,
context,
|variable| match variable {
"count" => Some(count.to_string()),
_ => None,
},
)
}
fn format_symbol(format_str: &str, config_path: &str) -> Option<Vec<Segment>> {
format_text(format_str, config_path, |_variable| None)
fn format_symbol(format_str: &str, config_path: &str, context: &Context) -> Option<Vec<Segment>> {
format_text(format_str, config_path, context, |_variable| None)
}
#[cfg(test)]

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -42,7 +42,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
}
_ => None,
})
.parse(None)
.parse(None, Some(context))
});
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);
root_module.set_segments(
formatter
.parse(None)
.parse(None, Some(&context))
.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
/// that need to be escaped in the shell-appropriate wrappers.
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('%', "%%");
}
_ => {}
};
/// Wraps ANSI color escape sequences in the shell-appropriate wrappers.
pub fn wrap_colorseq_for_shell(ansi: String, shell: Shell) -> String {
const ESCAPE_BEGIN: char = '\u{1b}';
const ESCAPE_END: char = 'm';
wrap_seq_for_shell(ansi, shell, ESCAPE_BEGIN, ESCAPE_END)
@ -674,47 +657,6 @@ mod tests {
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]
fn test_get_command_string_output() {
let case1 = CommandOutput {