mirror of
https://github.com/starship/starship.git
synced 2026-06-22 02:02:12 +07:00
feat(directory): add support for regexes in substitutions (#7145)
--------- Co-authored-by: David Knaack <davidkna@users.noreply.github.com>
This commit is contained in:
+46
-13
@@ -367,7 +367,7 @@
|
||||
"default": {
|
||||
"truncation_length": 3,
|
||||
"truncate_to_repo": true,
|
||||
"substitutions": {},
|
||||
"substitutions": [],
|
||||
"fish_style_pwd_dir_length": 0,
|
||||
"use_logical_path": true,
|
||||
"format": "[$path]($style)[$read_only]($read_only_style) ",
|
||||
@@ -2668,11 +2668,8 @@
|
||||
"default": true
|
||||
},
|
||||
"substitutions": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {}
|
||||
"$ref": "#/$defs/Either",
|
||||
"default": []
|
||||
},
|
||||
"fish_style_pwd_dir_length": {
|
||||
"type": "integer",
|
||||
@@ -2736,6 +2733,42 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Either": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/SubstitutionConfig"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"SubstitutionConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"from": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string"
|
||||
},
|
||||
"regex": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"from",
|
||||
"to"
|
||||
]
|
||||
},
|
||||
"DirenvConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -5413,7 +5446,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pixi_binary": {
|
||||
"$ref": "#/$defs/Either",
|
||||
"$ref": "#/$defs/Either2",
|
||||
"default": [
|
||||
"pixi"
|
||||
]
|
||||
@@ -5469,7 +5502,7 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Either": {
|
||||
"Either2": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
@@ -5577,7 +5610,7 @@
|
||||
"default": "pyenv "
|
||||
},
|
||||
"python_binary": {
|
||||
"$ref": "#/$defs/Either",
|
||||
"$ref": "#/$defs/Either2",
|
||||
"default": [
|
||||
[
|
||||
"python"
|
||||
@@ -6187,7 +6220,7 @@
|
||||
"default": "S "
|
||||
},
|
||||
"compiler": {
|
||||
"$ref": "#/$defs/Either",
|
||||
"$ref": "#/$defs/Either2",
|
||||
"default": [
|
||||
"solc"
|
||||
]
|
||||
@@ -6893,7 +6926,7 @@
|
||||
"default": ""
|
||||
},
|
||||
"when": {
|
||||
"$ref": "#/$defs/Either2",
|
||||
"$ref": "#/$defs/Either3",
|
||||
"default": false
|
||||
},
|
||||
"require_repo": {
|
||||
@@ -6901,7 +6934,7 @@
|
||||
"default": false
|
||||
},
|
||||
"shell": {
|
||||
"$ref": "#/$defs/Either",
|
||||
"$ref": "#/$defs/Either2",
|
||||
"default": []
|
||||
},
|
||||
"description": {
|
||||
@@ -6960,7 +6993,7 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Either2": {
|
||||
"Either3": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
|
||||
+24
-2
@@ -1206,12 +1206,34 @@ it would have been `nixpkgs/pkgs`.
|
||||
|
||||
| Advanced Option | Default | Description |
|
||||
| --------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `substitutions` | | A table of substitutions to be made to the path. |
|
||||
| `substitutions` | | An Array or table of substitutions to be made to the path. |
|
||||
| `fish_style_pwd_dir_length` | `0` | The number of characters to use when applying fish shell pwd path logic. |
|
||||
| `use_logical_path` | `true` | If `true` render the logical path sourced from the shell via `PWD` or `--logical-path`. If `false` instead render the physical filesystem path with symlinks resolved. |
|
||||
|
||||
`substitutions` allows you to define arbitrary replacements for literal strings that occur in the path, for example long network
|
||||
prefixes or development directories of Java. Note that this will disable the fish style PWD.
|
||||
prefixes or development directories of Java. Note that this will disable the fish style PWD. It takes an array of the following
|
||||
key/value pairs:
|
||||
|
||||
| Value | Type | Description |
|
||||
| ------- | ------- | ---------------------------------------- |
|
||||
| `from` | String | The value to substitute |
|
||||
| `to` | String | The replacement for that value, if found |
|
||||
| `regex` | Boolean | (Optional) Whether `from` is a regex |
|
||||
|
||||
By using `regex = true`, you can use [Rust's regular expressions](https://docs.rs/regex/latest/regex/#syntax) in `from`.
|
||||
For instance you can replace every slash except the first with the following:
|
||||
|
||||
```toml
|
||||
substitutions = [
|
||||
{ from = "^/", to = "<root>/", regex = true },
|
||||
{ from = "/", to = " | " },
|
||||
{ from = "^<root>", to = "/", regex = true },
|
||||
]
|
||||
```
|
||||
|
||||
This will replace `/var/log` to `/ | var | log`.
|
||||
|
||||
The old syntax still works, although it doesn't support regular expressions:
|
||||
|
||||
```toml
|
||||
[directory.substitutions]
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
use crate::config::Either;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(
|
||||
feature = "config-schema",
|
||||
derive(schemars::JsonSchema),
|
||||
schemars(deny_unknown_fields)
|
||||
)]
|
||||
pub struct SubstitutionConfig<'a> {
|
||||
pub from: String,
|
||||
pub to: &'a str,
|
||||
#[serde(default)]
|
||||
pub regex: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(
|
||||
feature = "config-schema",
|
||||
@@ -12,7 +25,7 @@ use serde::{Deserialize, Serialize};
|
||||
pub struct DirectoryConfig<'a> {
|
||||
pub truncation_length: i64,
|
||||
pub truncate_to_repo: bool,
|
||||
pub substitutions: IndexMap<String, &'a str>,
|
||||
pub substitutions: Either<Vec<SubstitutionConfig<'a>>, IndexMap<String, &'a str>>,
|
||||
pub fish_style_pwd_dir_length: i64,
|
||||
pub use_logical_path: bool,
|
||||
pub format: &'a str,
|
||||
@@ -28,6 +41,15 @@ pub struct DirectoryConfig<'a> {
|
||||
pub use_os_path_sep: bool,
|
||||
}
|
||||
|
||||
impl<'a> DirectoryConfig<'a> {
|
||||
pub fn substitutions_empty(&self) -> bool {
|
||||
match &self.substitutions {
|
||||
Either::First(vec) => vec.is_empty(),
|
||||
Either::Second(table) => table.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DirectoryConfig<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -35,7 +57,7 @@ impl Default for DirectoryConfig<'_> {
|
||||
truncate_to_repo: true,
|
||||
fish_style_pwd_dir_length: 0,
|
||||
use_logical_path: true,
|
||||
substitutions: IndexMap::new(),
|
||||
substitutions: Either::First(vec![]),
|
||||
format: "[$path]($style)[$read_only]($read_only_style) ",
|
||||
repo_root_format: "[$before_root_path]($before_repo_root_style)[$repo_root]($repo_root_style)[$path]($style)[$read_only]($read_only_style) ",
|
||||
style: "cyan bold",
|
||||
|
||||
+119
-13
@@ -5,6 +5,7 @@ use super::utils::directory_win as directory_utils;
|
||||
use super::utils::path::PathExt as SPathExt;
|
||||
use indexmap::IndexMap;
|
||||
use path_slash::{PathBufExt, PathExt};
|
||||
use regex::{Error, Regex};
|
||||
use std::borrow::Cow;
|
||||
use std::iter::FromIterator;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -13,8 +14,8 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use super::{Context, Module};
|
||||
|
||||
use super::utils::directory::truncate;
|
||||
use crate::config::ModuleConfig;
|
||||
use crate::configs::directory::DirectoryConfig;
|
||||
use crate::config::{Either, ModuleConfig};
|
||||
use crate::configs::directory::{DirectoryConfig, SubstitutionConfig};
|
||||
use crate::formatter::StringFormatter;
|
||||
|
||||
/// Creates a module with the current logical or physical directory
|
||||
@@ -74,7 +75,13 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let dir_string = remove_extended_path_prefix(dir_string);
|
||||
|
||||
// Apply path substitutions
|
||||
let dir_string = substitute_path(dir_string, &config.substitutions);
|
||||
let dir_string = match substitute_path(dir_string.clone(), &config.substitutions) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
log::warn!("Invalid regex in directory substitutions: {err}");
|
||||
dir_string
|
||||
}
|
||||
};
|
||||
|
||||
// Truncate the dir string to the maximum number of path components
|
||||
let dir_string =
|
||||
@@ -88,7 +95,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let prefix = if is_truncated {
|
||||
// Substitutions could have changed the prefix, so don't allow them and
|
||||
// fish-style path contraction together
|
||||
if config.fish_style_pwd_dir_length > 0 && config.substitutions.is_empty() {
|
||||
if config.fish_style_pwd_dir_length > 0 && config.substitutions_empty() {
|
||||
// If user is using fish style path, we need to add the segment first
|
||||
let contracted_home_dir = contract_path(display_dir, &home_dir, config.home_symbol);
|
||||
to_fish_style(
|
||||
@@ -291,12 +298,35 @@ fn real_path<P: AsRef<Path>>(path: P) -> PathBuf {
|
||||
///
|
||||
/// Given a list of (from, to) pairs, this will perform the string
|
||||
/// substitutions, in order, on the path. Any non-pair of strings is ignored.
|
||||
fn substitute_path(dir_string: String, substitutions: &IndexMap<String, &str>) -> String {
|
||||
fn substitute_path(
|
||||
dir_string: String,
|
||||
substitutions: &Either<Vec<SubstitutionConfig>, IndexMap<String, &str>>,
|
||||
) -> Result<String, Error> {
|
||||
let substitutions: &Vec<SubstitutionConfig> = match substitutions {
|
||||
Either::First(vec) => vec,
|
||||
Either::Second(table) => &table
|
||||
.iter()
|
||||
.map(|(from, to)| SubstitutionConfig {
|
||||
from: String::from(from),
|
||||
to,
|
||||
regex: false,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let mut substituted_dir = dir_string;
|
||||
for substitution_pair in substitutions {
|
||||
substituted_dir = substituted_dir.replace(substitution_pair.0, substitution_pair.1);
|
||||
|
||||
for substitution in substitutions {
|
||||
if substitution.regex {
|
||||
let re = Regex::new(substitution.from.as_str())?;
|
||||
substituted_dir = re
|
||||
.replace(substituted_dir.as_str(), substitution.to)
|
||||
.to_string()
|
||||
} else {
|
||||
substituted_dir = substituted_dir.replace(substitution.from.as_str(), substitution.to)
|
||||
}
|
||||
}
|
||||
substituted_dir
|
||||
Ok(substituted_dir)
|
||||
}
|
||||
|
||||
/// Takes part before contracted path and replaces it with fish style path
|
||||
@@ -350,6 +380,7 @@ fn before_root_dir<'a>(path: &'a str, repo: &'a str) -> &'a str {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Either::First;
|
||||
use crate::test::ModuleRenderer;
|
||||
use crate::utils::create_command;
|
||||
use crate::utils::home_dir;
|
||||
@@ -443,11 +474,19 @@ mod tests {
|
||||
#[test]
|
||||
fn substitute_prefix_and_middle() {
|
||||
let full_path = "/absolute/path/foo/bar/baz";
|
||||
let mut substitutions = IndexMap::new();
|
||||
substitutions.insert("/absolute/path".to_string(), "");
|
||||
substitutions.insert("/bar/".to_string(), "/");
|
||||
|
||||
let output = substitute_path(full_path.to_string(), &substitutions);
|
||||
let substitutions = First(vec![
|
||||
SubstitutionConfig {
|
||||
from: "/absolute/path".to_string(),
|
||||
to: "",
|
||||
regex: false,
|
||||
},
|
||||
SubstitutionConfig {
|
||||
from: "/bar/".to_string(),
|
||||
to: "/",
|
||||
regex: false,
|
||||
},
|
||||
]);
|
||||
let output = substitute_path(full_path.to_string(), &substitutions).unwrap();
|
||||
assert_eq!(output, "/foo/baz");
|
||||
}
|
||||
|
||||
@@ -691,6 +730,73 @@ mod tests {
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regex_substitution() {
|
||||
let actual = ModuleRenderer::new("directory")
|
||||
.path("/var/log")
|
||||
.config(toml::toml! {
|
||||
[directory]
|
||||
format = "[$path]($style)"
|
||||
substitutions = [
|
||||
{ from = "~/Documents", to = "docs"},
|
||||
{ from = "^/", to = "<root>/", regex = true},
|
||||
{ from = "/", to = " | "},
|
||||
{ from = "^<root>", to = "/", regex = true},
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
let expected = Some(format!(
|
||||
"{}",
|
||||
Color::Cyan.bold().paint(convert_path_sep("/ | var | log"))
|
||||
));
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
let actual = ModuleRenderer::new("directory")
|
||||
.path("~/Documents/var/log")
|
||||
.config(toml::toml! {
|
||||
[directory]
|
||||
format = "[$path]($style)"
|
||||
substitutions = [
|
||||
{ from = "~/Documents", to = "docs"},
|
||||
{ from = "^/", to = "<root>/", regex = true},
|
||||
{ from = "/", to = " | "},
|
||||
{ from = "^<root>", to = "/", regex = true},
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
let expected = Some(format!(
|
||||
"{}",
|
||||
Color::Cyan
|
||||
.bold()
|
||||
.paint(convert_path_sep("docs | var | log"))
|
||||
));
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_regex_substitution_leaves_path_untouched() {
|
||||
let path = "/var/log";
|
||||
let actual = ModuleRenderer::new("directory")
|
||||
.path(path)
|
||||
.config(toml::toml! {
|
||||
[directory]
|
||||
format = "[$path]($style)"
|
||||
substitutions = [
|
||||
{ from = "~/Documents", to = "docs"},
|
||||
{ from = "(^/", to = "<home>/", regex = true},
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
let expected = Some(format!(
|
||||
"{}",
|
||||
Color::Cyan.bold().paint(convert_path_sep(path))
|
||||
));
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_in_home() -> io::Result<()> {
|
||||
let (tmp_dir, name) = make_known_tempdir(home_dir().unwrap().as_path())?;
|
||||
|
||||
Reference in New Issue
Block a user