summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFred Cox <mcfedr@gmail.com>2021-11-01 14:18:45 -0700
committerGitHub <noreply@github.com>2021-11-01 22:18:45 +0100
commitc1f2d345aac0b0241ea1b6d99977fea20fa3f5bb (patch)
treeb6828442ee1aaef179b30d65007d79b9f47bfe9e
parent73277d37c62e6d3bff15e0125796a9cef6346318 (diff)
fix(escaping): move escaping to individual variables (#3107)
-rw-r--r--docs/config/README.md13
-rw-r--r--src/formatter/string_formatter.rs154
-rw-r--r--src/formatter/version.rs2
-rw-r--r--src/modules/aws.rs2
-rw-r--r--src/modules/battery.rs2
-rw-r--r--src/modules/character.rs2
-rw-r--r--src/modules/cmake.rs2
-rw-r--r--src/modules/cmd_duration.rs2
-rw-r--r--src/modules/cobol.rs2
-rw-r--r--src/modules/conda.rs2
-rw-r--r--src/modules/crystal.rs2
-rw-r--r--src/modules/custom.rs4
-rw-r--r--src/modules/dart.rs2
-rw-r--r--src/modules/deno.rs2
-rw-r--r--src/modules/directory.rs2
-rw-r--r--src/modules/docker_context.rs2
-rw-r--r--src/modules/dotnet.rs2
-rw-r--r--src/modules/elixir.rs2
-rw-r--r--src/modules/elm.rs2
-rw-r--r--src/modules/env_var.rs2
-rw-r--r--src/modules/erlang.rs2
-rw-r--r--src/modules/gcloud.rs2
-rw-r--r--src/modules/git_branch.rs2
-rw-r--r--src/modules/git_commit.rs2
-rw-r--r--src/modules/git_metrics.rs2
-rw-r--r--src/modules/git_state.rs2
-rw-r--r--src/modules/git_status.rs70
-rw-r--r--src/modules/golang.rs2
-rw-r--r--src/modules/helm.rs2
-rw-r--r--src/modules/hg_branch.rs2
-rw-r--r--src/modules/hostname.rs2
-rw-r--r--src/modules/java.rs2
-rw-r--r--src/modules/jobs.rs2
-rw-r--r--src/modules/julia.rs2
-rw-r--r--src/modules/kotlin.rs2
-rw-r--r--src/modules/kubernetes.rs2
-rw-r--r--src/modules/lua.rs2
-rw-r--r--src/modules/memory_usage.rs2
-rw-r--r--src/modules/nim.rs2
-rw-r--r--src/modules/nix_shell.rs2
-rw-r--r--src/modules/nodejs.rs2
-rw-r--r--src/modules/ocaml.rs2
-rw-r--r--src/modules/openstack.rs2
-rw-r--r--src/modules/package.rs2
-rw-r--r--src/modules/perl.rs2
-rw-r--r--src/modules/php.rs2
-rw-r--r--src/modules/pulumi.rs2
-rw-r--r--src/modules/purescript.rs2
-rw-r--r--src/modules/python.rs2
-rw-r--r--src/modules/red.rs2
-rw-r--r--src/modules/rlang.rs2
-rw-r--r--src/modules/ruby.rs2
-rw-r--r--src/modules/rust.rs2
-rw-r--r--src/modules/scala.rs2
-rw-r--r--src/modules/shell.rs2
-rw-r--r--src/modules/shlvl.rs2
-rw-r--r--src/modules/singularity.rs2
-rw-r--r--src/modules/status.rs7
-rw-r--r--src/modules/swift.rs2
-rw-r--r--src/modules/terraform.rs2
-rw-r--r--src/modules/time.rs2
-rw-r--r--src/modules/username.rs2
-rw-r--r--src/modules/vagrant.rs2
-rw-r--r--src/modules/vcsh.rs2
-rw-r--r--src/modules/vlang.rs2
-rw-r--r--src/modules/zig.rs2
-rw-r--r--src/print.rs2
-rw-r--r--src/utils.rs62
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))
})