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": {
|
"default": {
|
||||||
"truncation_length": 3,
|
"truncation_length": 3,
|
||||||
"truncate_to_repo": true,
|
"truncate_to_repo": true,
|
||||||
"substitutions": {},
|
"substitutions": [],
|
||||||
"fish_style_pwd_dir_length": 0,
|
"fish_style_pwd_dir_length": 0,
|
||||||
"use_logical_path": true,
|
"use_logical_path": true,
|
||||||
"format": "[$path]($style)[$read_only]($read_only_style) ",
|
"format": "[$path]($style)[$read_only]($read_only_style) ",
|
||||||
@@ -2668,11 +2668,8 @@
|
|||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
"substitutions": {
|
"substitutions": {
|
||||||
"type": "object",
|
"$ref": "#/$defs/Either",
|
||||||
"additionalProperties": {
|
"default": []
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"default": {}
|
|
||||||
},
|
},
|
||||||
"fish_style_pwd_dir_length": {
|
"fish_style_pwd_dir_length": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -2736,6 +2733,42 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"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": {
|
"DirenvConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -5413,7 +5446,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"pixi_binary": {
|
"pixi_binary": {
|
||||||
"$ref": "#/$defs/Either",
|
"$ref": "#/$defs/Either2",
|
||||||
"default": [
|
"default": [
|
||||||
"pixi"
|
"pixi"
|
||||||
]
|
]
|
||||||
@@ -5469,7 +5502,7 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"Either": {
|
"Either2": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -5577,7 +5610,7 @@
|
|||||||
"default": "pyenv "
|
"default": "pyenv "
|
||||||
},
|
},
|
||||||
"python_binary": {
|
"python_binary": {
|
||||||
"$ref": "#/$defs/Either",
|
"$ref": "#/$defs/Either2",
|
||||||
"default": [
|
"default": [
|
||||||
[
|
[
|
||||||
"python"
|
"python"
|
||||||
@@ -6187,7 +6220,7 @@
|
|||||||
"default": "S "
|
"default": "S "
|
||||||
},
|
},
|
||||||
"compiler": {
|
"compiler": {
|
||||||
"$ref": "#/$defs/Either",
|
"$ref": "#/$defs/Either2",
|
||||||
"default": [
|
"default": [
|
||||||
"solc"
|
"solc"
|
||||||
]
|
]
|
||||||
@@ -6893,7 +6926,7 @@
|
|||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
"when": {
|
"when": {
|
||||||
"$ref": "#/$defs/Either2",
|
"$ref": "#/$defs/Either3",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"require_repo": {
|
"require_repo": {
|
||||||
@@ -6901,7 +6934,7 @@
|
|||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"$ref": "#/$defs/Either",
|
"$ref": "#/$defs/Either2",
|
||||||
"default": []
|
"default": []
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
@@ -6960,7 +6993,7 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"Either2": {
|
"Either3": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
+24
-2
@@ -1206,12 +1206,34 @@ it would have been `nixpkgs/pkgs`.
|
|||||||
|
|
||||||
| Advanced Option | Default | Description |
|
| 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. |
|
| `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. |
|
| `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
|
`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
|
```toml
|
||||||
[directory.substitutions]
|
[directory.substitutions]
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
|
use crate::config::Either;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "config-schema",
|
feature = "config-schema",
|
||||||
@@ -12,7 +25,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub struct DirectoryConfig<'a> {
|
pub struct DirectoryConfig<'a> {
|
||||||
pub truncation_length: i64,
|
pub truncation_length: i64,
|
||||||
pub truncate_to_repo: bool,
|
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 fish_style_pwd_dir_length: i64,
|
||||||
pub use_logical_path: bool,
|
pub use_logical_path: bool,
|
||||||
pub format: &'a str,
|
pub format: &'a str,
|
||||||
@@ -28,6 +41,15 @@ pub struct DirectoryConfig<'a> {
|
|||||||
pub use_os_path_sep: bool,
|
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<'_> {
|
impl Default for DirectoryConfig<'_> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -35,7 +57,7 @@ impl Default for DirectoryConfig<'_> {
|
|||||||
truncate_to_repo: true,
|
truncate_to_repo: true,
|
||||||
fish_style_pwd_dir_length: 0,
|
fish_style_pwd_dir_length: 0,
|
||||||
use_logical_path: true,
|
use_logical_path: true,
|
||||||
substitutions: IndexMap::new(),
|
substitutions: Either::First(vec![]),
|
||||||
format: "[$path]($style)[$read_only]($read_only_style) ",
|
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) ",
|
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",
|
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 super::utils::path::PathExt as SPathExt;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use path_slash::{PathBufExt, PathExt};
|
use path_slash::{PathBufExt, PathExt};
|
||||||
|
use regex::{Error, Regex};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -13,8 +14,8 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||||||
use super::{Context, Module};
|
use super::{Context, Module};
|
||||||
|
|
||||||
use super::utils::directory::truncate;
|
use super::utils::directory::truncate;
|
||||||
use crate::config::ModuleConfig;
|
use crate::config::{Either, ModuleConfig};
|
||||||
use crate::configs::directory::DirectoryConfig;
|
use crate::configs::directory::{DirectoryConfig, SubstitutionConfig};
|
||||||
use crate::formatter::StringFormatter;
|
use crate::formatter::StringFormatter;
|
||||||
|
|
||||||
/// Creates a module with the current logical or physical directory
|
/// 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);
|
let dir_string = remove_extended_path_prefix(dir_string);
|
||||||
|
|
||||||
// Apply path substitutions
|
// 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
|
// Truncate the dir string to the maximum number of path components
|
||||||
let dir_string =
|
let dir_string =
|
||||||
@@ -88,7 +95,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
|||||||
let prefix = if is_truncated {
|
let prefix = if is_truncated {
|
||||||
// Substitutions could have changed the prefix, so don't allow them and
|
// Substitutions could have changed the prefix, so don't allow them and
|
||||||
// fish-style path contraction together
|
// 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
|
// 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);
|
let contracted_home_dir = contract_path(display_dir, &home_dir, config.home_symbol);
|
||||||
to_fish_style(
|
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
|
/// 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.
|
/// 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;
|
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
|
/// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::config::Either::First;
|
||||||
use crate::test::ModuleRenderer;
|
use crate::test::ModuleRenderer;
|
||||||
use crate::utils::create_command;
|
use crate::utils::create_command;
|
||||||
use crate::utils::home_dir;
|
use crate::utils::home_dir;
|
||||||
@@ -443,11 +474,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn substitute_prefix_and_middle() {
|
fn substitute_prefix_and_middle() {
|
||||||
let full_path = "/absolute/path/foo/bar/baz";
|
let full_path = "/absolute/path/foo/bar/baz";
|
||||||
let mut substitutions = IndexMap::new();
|
let substitutions = First(vec![
|
||||||
substitutions.insert("/absolute/path".to_string(), "");
|
SubstitutionConfig {
|
||||||
substitutions.insert("/bar/".to_string(), "/");
|
from: "/absolute/path".to_string(),
|
||||||
|
to: "",
|
||||||
let output = substitute_path(full_path.to_string(), &substitutions);
|
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");
|
assert_eq!(output, "/foo/baz");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,6 +730,73 @@ mod tests {
|
|||||||
assert_eq!(expected, actual);
|
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]
|
#[test]
|
||||||
fn directory_in_home() -> io::Result<()> {
|
fn directory_in_home() -> io::Result<()> {
|
||||||
let (tmp_dir, name) = make_known_tempdir(home_dir().unwrap().as_path())?;
|
let (tmp_dir, name) = make_known_tempdir(home_dir().unwrap().as_path())?;
|
||||||
|
|||||||
Reference in New Issue
Block a user