mirror of
https://github.com/starship/starship.git
synced 2025-03-04 02:01:45 +01:00
Merge branch 'master' into conditional-style
This commit is contained in:
commit
ba5241b612
11
.codecov.yml
Normal file
11
.codecov.yml
Normal file
@ -0,0 +1,11 @@
|
||||
comment: false
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 5%
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 5%
|
34
.github/config-schema.json
vendored
34
.github/config-schema.json
vendored
@ -4766,9 +4766,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"when": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
"default": false,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Either_for_Boolean_and_String"
|
||||
}
|
||||
]
|
||||
},
|
||||
"shell": {
|
||||
@ -4791,21 +4793,21 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"files": {
|
||||
"detect_files": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"detect_extensions": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"detect_folders": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -4817,8 +4819,28 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"use_stdin": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"ignore_timeout": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Either_for_Boolean_and_String": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
.github/workflows/workflow.yml
vendored
17
.github/workflows/workflow.yml
vendored
@ -177,9 +177,13 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1.0.7
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
components: llvm-tools-preview
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
|
||||
# Install Mercurial (pre-installed on Linux and windows)
|
||||
- name: Setup | Mercurial (macos)
|
||||
if: matrix.os == 'macOS-latest'
|
||||
@ -187,7 +191,18 @@ jobs:
|
||||
|
||||
# Run the ignored tests that expect the above setup
|
||||
- name: Build | Test
|
||||
run: cargo test --workspace --locked --all-features -- -Z unstable-options --include-ignored
|
||||
run: "cargo llvm-cov
|
||||
--all-features
|
||||
--locked
|
||||
--workspace
|
||||
--lcov --output-path lcov.info
|
||||
-- --include-ignored"
|
||||
env:
|
||||
# Avoid -D warnings on nightly builds
|
||||
RUSTFLAGS: ""
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: lcov.info
|
||||
fail_ci_if_error: true
|
||||
|
@ -3666,9 +3666,9 @@ The `custom` modules show the output of some arbitrary commands.
|
||||
|
||||
These modules will be shown if any of the following conditions are met:
|
||||
|
||||
- The current directory contains a file whose name is in `files`
|
||||
- The current directory contains a directory whose name is in `directories`
|
||||
- The current directory contains a file whose extension is in `extensions`
|
||||
- The current directory contains a file whose name is in `detect_files`
|
||||
- The current directory contains a directory whose name is in `detect_folders`
|
||||
- The current directory contains a file whose extension is in `detect_extensions`
|
||||
- The `when` command returns 0
|
||||
- The current Operating System (std::env::consts::OS) matchs with `os` field if defined.
|
||||
|
||||
@ -3708,20 +3708,21 @@ Format strings can also contain shell specific prompt sequences, e.g.
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `command` | `""` | The command whose output should be printed. The command will be passed on stdin to the shell. |
|
||||
| `when` | | A shell command used as a condition to show the module. The module will be shown if the command returns a `0` status code. |
|
||||
| `shell` | | [See below](#custom-command-shell) |
|
||||
| `description` | `"<custom module>"` | The description of the module that is shown when running `starship explain`. |
|
||||
| `files` | `[]` | The files that will be searched in the working directory for a match. |
|
||||
| `directories` | `[]` | The directories that will be searched in the working directory for a match. |
|
||||
| `extensions` | `[]` | The extensions that will be searched in the working directory for a match. |
|
||||
| `symbol` | `""` | The symbol used before displaying the command output. |
|
||||
| `style` | `"bold green"` | The style for the module. |
|
||||
| `format` | `"[$symbol($output )]($style)"` | The format for the module. |
|
||||
| `disabled` | `false` | Disables this `custom` module. |
|
||||
| `os` | | Operating System name on which the module will be shown (unix, linux, macos, windows, ... ) [See possible values](https://doc.rust-lang.org/std/env/consts/constant.OS.html). |
|
||||
| Option | Default | Description |
|
||||
| ------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `command` | `""` | The command whose output should be printed. The command will be passed on stdin to the shell. |
|
||||
| `when` | `false` | Either a boolean value (`true` or `false`, without quotes) or a string shell command used as a condition to show the module. In case of a string, the module will be shown if the command returns a `0` status code. |
|
||||
| `shell` | | [See below](#custom-command-shell) |
|
||||
| `description` | `"<custom module>"` | The description of the module that is shown when running `starship explain`. |
|
||||
| `detect_files` | `[]` | The files that will be searched in the working directory for a match. |
|
||||
| `detect_folders` | `[]` | The directories that will be searched in the working directory for a match. |
|
||||
| `detect_extensions` | `[]` | The extensions that will be searched in the working directory for a match. |
|
||||
| `symbol` | `""` | The symbol used before displaying the command output. |
|
||||
| `style` | `"bold green"` | The style for the module. |
|
||||
| `format` | `"[$symbol($output )]($style)"` | The format for the module. |
|
||||
| `disabled` | `false` | Disables this `custom` module. |
|
||||
| `os` | | Operating System name on which the module will be shown (unix, linux, macos, windows, ... ) [See possible values](https://doc.rust-lang.org/std/env/consts/constant.OS.html). |
|
||||
| `use_stdin` | | An optional boolean value that overrides whether commands should be forwarded to the shell via the standard input or as an argument. If unset standard input is used by default, unless the shell does not support it (cmd, nushell). Setting this disables shell-specific argument handling. |
|
||||
|
||||
### Variables
|
||||
|
||||
@ -3746,6 +3747,10 @@ The `command` will be passed in on stdin.
|
||||
|
||||
If `shell` is not given or only contains one element and Starship detects PowerShell will be used,
|
||||
the following arguments will automatically be added: `-NoProfile -Command -`.
|
||||
If `shell` is not given or only contains one element and Starship detects Cmd will be used,
|
||||
the following argument will automatically be added: `/C` and `stdin` will be set to `false`.
|
||||
If `shell` is not given or only contains one element and Starship detects Nushell will be used,
|
||||
the following arguments will automatically be added: `-c` and `stdin` will be set to `false`.
|
||||
This behavior can be avoided by explicitly passing arguments to the shell, e.g.
|
||||
|
||||
```toml
|
||||
@ -3782,12 +3787,18 @@ with shell details and starship configuration if you hit such scenario.
|
||||
|
||||
[custom.foo]
|
||||
command = "echo foo" # shows output of command
|
||||
files = ["foo"] # can specify filters but wildcards are not supported
|
||||
detect_files = ["foo"] # can specify filters but wildcards are not supported
|
||||
when = """ test "$HOME" == "$PWD" """
|
||||
format = " transcending [$output]($style)"
|
||||
|
||||
[custom.time]
|
||||
command = "time /T"
|
||||
extensions = ["pst"] # filters *.pst files
|
||||
detect_extensions = ["pst"] # filters *.pst files
|
||||
shell = ["pwsh.exe", "-NoProfile", "-Command", "-"]
|
||||
|
||||
[custom.time-as-arg]
|
||||
command = "time /T"
|
||||
detect_extensions = ["pst"] # filters *.pst files
|
||||
shell = ["pwsh.exe", "-NoProfile", "-Command"]
|
||||
use_stdin = false
|
||||
```
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::config::VecOr;
|
||||
use crate::config::{Either, VecOr};
|
||||
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
|
||||
@ -9,17 +9,22 @@ pub struct CustomConfig<'a> {
|
||||
pub format: &'a str,
|
||||
pub symbol: &'a str,
|
||||
pub command: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub when: Option<&'a str>,
|
||||
pub when: Either<bool, &'a str>,
|
||||
pub shell: VecOr<&'a str>,
|
||||
pub description: &'a str,
|
||||
pub style: &'a str,
|
||||
pub disabled: bool,
|
||||
pub files: Vec<&'a str>,
|
||||
pub extensions: Vec<&'a str>,
|
||||
pub directories: Vec<&'a str>,
|
||||
#[serde(alias = "files")]
|
||||
pub detect_files: Vec<&'a str>,
|
||||
#[serde(alias = "extensions")]
|
||||
pub detect_extensions: Vec<&'a str>,
|
||||
#[serde(alias = "directories")]
|
||||
pub detect_folders: Vec<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub os: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub use_stdin: Option<bool>,
|
||||
pub ignore_timeout: bool,
|
||||
}
|
||||
|
||||
impl<'a> Default for CustomConfig<'a> {
|
||||
@ -28,15 +33,17 @@ impl<'a> Default for CustomConfig<'a> {
|
||||
format: "[$symbol($output )]($style)",
|
||||
symbol: "",
|
||||
command: "",
|
||||
when: None,
|
||||
when: Either::First(false),
|
||||
shell: VecOr::default(),
|
||||
description: "<custom config>",
|
||||
style: "green bold",
|
||||
disabled: false,
|
||||
files: Vec::default(),
|
||||
extensions: Vec::default(),
|
||||
directories: Vec::default(),
|
||||
detect_files: Vec::default(),
|
||||
detect_extensions: Vec::default(),
|
||||
detect_folders: Vec::default(),
|
||||
os: None,
|
||||
use_stdin: None,
|
||||
ignore_timeout: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,19 @@ use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::DateTime;
|
||||
use ini::Ini;
|
||||
use once_cell::unsync::OnceCell;
|
||||
|
||||
use super::{Context, Module, ModuleConfig};
|
||||
|
||||
use crate::configs::aws::AwsConfig;
|
||||
use crate::formatter::StringFormatter;
|
||||
use crate::utils::{read_file, render_time};
|
||||
use crate::utils::render_time;
|
||||
|
||||
type Profile = String;
|
||||
type Region = String;
|
||||
type AwsConfigFile = OnceCell<Option<Ini>>;
|
||||
type AwsCredsFile = OnceCell<Option<Ini>>;
|
||||
|
||||
fn get_credentials_file_path(context: &Context) -> Option<PathBuf> {
|
||||
context
|
||||
@ -35,36 +39,65 @@ fn get_config_file_path(context: &Context) -> Option<PathBuf> {
|
||||
})
|
||||
}
|
||||
|
||||
fn get_aws_region_from_config(context: &Context, aws_profile: Option<&str>) -> Option<Region> {
|
||||
let config_location = get_config_file_path(context)?;
|
||||
|
||||
let contents = read_file(&config_location).ok()?;
|
||||
|
||||
let region_line = if let Some(aws_profile) = aws_profile {
|
||||
contents
|
||||
.lines()
|
||||
.skip_while(|line| line != &format!("[profile {}]", &aws_profile))
|
||||
.skip(1)
|
||||
.take_while(|line| !line.starts_with('['))
|
||||
.find(|line| line.starts_with("region"))
|
||||
} else {
|
||||
contents
|
||||
.lines()
|
||||
.skip_while(|&line| line != "[default]")
|
||||
.skip(1)
|
||||
.take_while(|line| !line.starts_with('['))
|
||||
.find(|line| line.starts_with("region"))
|
||||
}?;
|
||||
|
||||
let region = region_line.split('=').nth(1)?;
|
||||
let region = region.trim();
|
||||
|
||||
Some(region.to_string())
|
||||
// Initialize the AWS config file once
|
||||
fn get_config<'a>(context: &Context, config: &'a OnceCell<Option<Ini>>) -> Option<&'a Ini> {
|
||||
config
|
||||
.get_or_init(|| {
|
||||
let path = get_config_file_path(context)?;
|
||||
Ini::load_from_file(path).ok()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
fn get_aws_profile_and_region(context: &Context) -> (Option<Profile>, Option<Region>) {
|
||||
let profile_env_vars = vec!["AWSU_PROFILE", "AWS_VAULT", "AWSUME_PROFILE", "AWS_PROFILE"];
|
||||
let region_env_vars = vec!["AWS_REGION", "AWS_DEFAULT_REGION"];
|
||||
// Initialize the AWS credentials file once
|
||||
fn get_creds<'a>(context: &Context, config: &'a OnceCell<Option<Ini>>) -> Option<&'a Ini> {
|
||||
config
|
||||
.get_or_init(|| {
|
||||
let path = get_credentials_file_path(context)?;
|
||||
Ini::load_from_file(path).ok()
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
// Get the section for a given profile name in the config file.
|
||||
fn get_profile_config<'a>(
|
||||
config: &'a Ini,
|
||||
profile: &Option<Profile>,
|
||||
) -> Option<&'a ini::Properties> {
|
||||
match profile {
|
||||
Some(profile) => config.section(Some(format!("profile {}", profile))),
|
||||
None => config.section(Some("default")),
|
||||
}
|
||||
}
|
||||
|
||||
// Get the section for a given profile name in the credentials file.
|
||||
fn get_profile_creds<'a>(
|
||||
config: &'a Ini,
|
||||
profile: &Option<Profile>,
|
||||
) -> Option<&'a ini::Properties> {
|
||||
match profile {
|
||||
None => config.section(Some("default")),
|
||||
_ => config.section(profile.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_aws_region_from_config(
|
||||
context: &Context,
|
||||
aws_profile: &Option<Profile>,
|
||||
aws_config: &AwsConfigFile,
|
||||
) -> Option<Region> {
|
||||
let config = get_config(context, aws_config)?;
|
||||
let section = get_profile_config(config, aws_profile)?;
|
||||
|
||||
section.get("region").map(|region| region.to_owned())
|
||||
}
|
||||
|
||||
fn get_aws_profile_and_region(
|
||||
context: &Context,
|
||||
aws_config: &AwsConfigFile,
|
||||
) -> (Option<Profile>, Option<Region>) {
|
||||
let profile_env_vars = ["AWSU_PROFILE", "AWS_VAULT", "AWSUME_PROFILE", "AWS_PROFILE"];
|
||||
let region_env_vars = ["AWS_REGION", "AWS_DEFAULT_REGION"];
|
||||
let profile = profile_env_vars
|
||||
.iter()
|
||||
.find_map(|env_var| context.get_env(env_var));
|
||||
@ -74,39 +107,32 @@ fn get_aws_profile_and_region(context: &Context) -> (Option<Profile>, Option<Reg
|
||||
match (profile, region) {
|
||||
(Some(p), Some(r)) => (Some(p), Some(r)),
|
||||
(None, Some(r)) => (None, Some(r)),
|
||||
(Some(ref p), None) => (
|
||||
(Some(p), None) => (
|
||||
Some(p.clone()),
|
||||
get_aws_region_from_config(context, Some(p)),
|
||||
get_aws_region_from_config(context, &Some(p), aws_config),
|
||||
),
|
||||
(None, None) => (None, get_aws_region_from_config(context, None)),
|
||||
(None, None) => (None, get_aws_region_from_config(context, &None, aws_config)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_credentials_duration(context: &Context, aws_profile: Option<&Profile>) -> Option<i64> {
|
||||
let expiration_env_vars = vec!["AWS_SESSION_EXPIRATION", "AWSUME_EXPIRATION"];
|
||||
fn get_credentials_duration(
|
||||
context: &Context,
|
||||
aws_profile: &Option<String>,
|
||||
aws_creds: &AwsCredsFile,
|
||||
) -> Option<i64> {
|
||||
let expiration_env_vars = ["AWS_SESSION_EXPIRATION", "AWSUME_EXPIRATION"];
|
||||
let expiration_date = if let Some(expiration_date) = expiration_env_vars
|
||||
.iter()
|
||||
.find_map(|env_var| context.get_env(env_var))
|
||||
{
|
||||
chrono::DateTime::parse_from_rfc3339(&expiration_date).ok()
|
||||
} else {
|
||||
let contents = read_file(get_credentials_file_path(context)?).ok()?;
|
||||
let creds = get_creds(context, aws_creds)?;
|
||||
let section = get_profile_creds(creds, aws_profile)?;
|
||||
|
||||
let profile_line = if let Some(aws_profile) = aws_profile {
|
||||
format!("[{}]", aws_profile)
|
||||
} else {
|
||||
"[default]".to_string()
|
||||
};
|
||||
|
||||
let expiration_date_line = contents
|
||||
.lines()
|
||||
.skip_while(|line| line != &profile_line)
|
||||
.skip(1)
|
||||
.take_while(|line| !line.starts_with('['))
|
||||
.find(|line| line.starts_with("expiration"))?;
|
||||
|
||||
let expiration_date = expiration_date_line.split('=').nth(1)?.trim();
|
||||
DateTime::parse_from_rfc3339(expiration_date).ok()
|
||||
section
|
||||
.get("expiration")
|
||||
.and_then(|expiration| DateTime::parse_from_rfc3339(expiration).ok())
|
||||
}?;
|
||||
|
||||
Some(expiration_date.timestamp() - chrono::Local::now().timestamp())
|
||||
@ -119,82 +145,63 @@ fn alias_name(name: Option<String>, aliases: &HashMap<String, &str>) -> Option<S
|
||||
.or(name)
|
||||
}
|
||||
|
||||
fn has_credential_process_or_sso(context: &Context, aws_profile: Option<&Profile>) -> bool {
|
||||
let fp = match get_config_file_path(context) {
|
||||
Some(fp) => fp,
|
||||
None => return false,
|
||||
};
|
||||
let contents = match read_file(fp) {
|
||||
Ok(contents) => contents,
|
||||
Err(_) => return false,
|
||||
};
|
||||
fn has_credential_process_or_sso(
|
||||
context: &Context,
|
||||
aws_profile: &Option<Profile>,
|
||||
aws_config: &AwsConfigFile,
|
||||
) -> Option<bool> {
|
||||
let config = get_config(context, aws_config)?;
|
||||
|
||||
let profile_line = if let Some(aws_profile) = aws_profile {
|
||||
format!("[profile {}]", aws_profile)
|
||||
} else {
|
||||
"[default]".to_string()
|
||||
};
|
||||
|
||||
contents
|
||||
.lines()
|
||||
.skip_while(|line| line != &profile_line)
|
||||
.skip(1)
|
||||
.take_while(|line| !line.starts_with('['))
|
||||
.any(|line| line.starts_with("credential_process") || line.starts_with("sso_start_url"))
|
||||
let section = get_profile_config(config, aws_profile)?;
|
||||
Some(section.contains_key("credential_process") || section.contains_key("sso_start_url"))
|
||||
}
|
||||
|
||||
fn get_defined_credentials(context: &Context, aws_profile: Option<&Profile>) -> Option<String> {
|
||||
let valid_env_vars = vec![
|
||||
fn has_defined_credentials(
|
||||
context: &Context,
|
||||
aws_profile: &Option<Profile>,
|
||||
aws_creds: &AwsCredsFile,
|
||||
) -> Option<bool> {
|
||||
let valid_env_vars = [
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"AWS_SESSION_TOKEN",
|
||||
];
|
||||
|
||||
// accept if set through environment variable
|
||||
if let Some(aws_identity_cred) = valid_env_vars
|
||||
if valid_env_vars
|
||||
.iter()
|
||||
.find_map(|env_var| context.get_env(env_var))
|
||||
.any(|env_var| context.get_env(env_var).is_some())
|
||||
{
|
||||
return Some(aws_identity_cred);
|
||||
return Some(true);
|
||||
}
|
||||
|
||||
let contents = read_file(get_credentials_file_path(context)?).ok()?;
|
||||
|
||||
let profile_line = if let Some(aws_profile) = aws_profile {
|
||||
format!("[{}]", aws_profile)
|
||||
} else {
|
||||
"[default]".to_string()
|
||||
};
|
||||
|
||||
let aws_key_id_line = contents
|
||||
.lines()
|
||||
.skip_while(|line| line != &profile_line)
|
||||
.skip(1)
|
||||
.take_while(|line| !line.starts_with('['))
|
||||
.find(|line| line.starts_with("aws_access_key_id"))?;
|
||||
let aws_key_id = aws_key_id_line.split('=').nth(1)?.trim();
|
||||
Some(aws_key_id.to_string())
|
||||
let creds = get_creds(context, aws_creds)?;
|
||||
let section = get_profile_creds(creds, aws_profile)?;
|
||||
Some(section.contains_key("aws_access_key_id"))
|
||||
}
|
||||
|
||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
let mut module = context.new_module("aws");
|
||||
let config: AwsConfig = AwsConfig::try_load(module.config);
|
||||
|
||||
let (aws_profile, aws_region) = get_aws_profile_and_region(context);
|
||||
let aws_config = OnceCell::new();
|
||||
let aws_creds = OnceCell::new();
|
||||
|
||||
let (aws_profile, aws_region) = get_aws_profile_and_region(context, &aws_config);
|
||||
if aws_profile.is_none() && aws_region.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// only display if credential_process is defined or has valid credentials
|
||||
if !config.force_display
|
||||
&& !has_credential_process_or_sso(context, aws_profile.as_ref())
|
||||
&& get_defined_credentials(context, aws_profile.as_ref()).is_none()
|
||||
&& !has_credential_process_or_sso(context, &aws_profile, &aws_config).unwrap_or(false)
|
||||
&& !has_defined_credentials(context, &aws_profile, &aws_creds).unwrap_or(false)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let duration = {
|
||||
get_credentials_duration(context, aws_profile.as_ref()).map(|duration| {
|
||||
get_credentials_duration(context, &aws_profile, &aws_creds).map(|duration| {
|
||||
if duration > 0 {
|
||||
render_time((duration * 1000) as u128, false)
|
||||
} else {
|
||||
|
@ -1,11 +1,18 @@
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::{Context, Module, ModuleConfig, Shell};
|
||||
use process_control::{ChildExt, Control, Output};
|
||||
|
||||
use crate::{configs::custom::CustomConfig, formatter::StringFormatter, utils::create_command};
|
||||
use super::{Context, Module, ModuleConfig};
|
||||
|
||||
use crate::{
|
||||
config::Either, configs::custom::CustomConfig, formatter::StringFormatter,
|
||||
utils::create_command,
|
||||
};
|
||||
|
||||
/// Creates a custom module with some configuration
|
||||
///
|
||||
@ -29,15 +36,16 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
|
||||
|
||||
let mut is_match = context
|
||||
.try_begin_scan()?
|
||||
.set_files(&config.files)
|
||||
.set_extensions(&config.extensions)
|
||||
.set_folders(&config.directories)
|
||||
.set_extensions(&config.detect_extensions)
|
||||
.set_files(&config.detect_files)
|
||||
.set_folders(&config.detect_folders)
|
||||
.is_match();
|
||||
|
||||
if !is_match {
|
||||
if let Some(when) = config.when {
|
||||
is_match = exec_when(when, &config.shell.0);
|
||||
}
|
||||
is_match = match config.when {
|
||||
Either::First(b) => b,
|
||||
Either::Second(s) => exec_when(s, &config, context),
|
||||
};
|
||||
|
||||
if !is_match {
|
||||
return None;
|
||||
@ -58,12 +66,7 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
|
||||
})
|
||||
.map_no_escaping(|variable| match variable {
|
||||
"output" => {
|
||||
if context.shell == Shell::Cmd && config.shell.0.is_empty() {
|
||||
log::error!("Executing custom commands with cmd shell is not currently supported. Please set a different shell with the \"shell\" option.");
|
||||
return None;
|
||||
}
|
||||
|
||||
let output = exec_command(config.command, &config.shell.0)?;
|
||||
let output = exec_command(config.command, context, &config)?;
|
||||
let trimmed = output.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
@ -89,112 +92,105 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
|
||||
Some(module)
|
||||
}
|
||||
|
||||
/// Return the invoking shell, using `shell` and fallbacking in order to STARSHIP_SHELL and "sh"
|
||||
#[cfg(not(windows))]
|
||||
fn get_shell<'a, 'b>(shell_args: &'b [&'a str]) -> (std::borrow::Cow<'a, str>, &'b [&'a str]) {
|
||||
/// Return the invoking shell, using `shell` and fallbacking in order to STARSHIP_SHELL and "sh"/"cmd"
|
||||
fn get_shell<'a, 'b>(
|
||||
shell_args: &'b [&'a str],
|
||||
context: &Context,
|
||||
) -> (std::borrow::Cow<'a, str>, &'b [&'a str]) {
|
||||
if !shell_args.is_empty() {
|
||||
(shell_args[0].into(), &shell_args[1..])
|
||||
} else if let Ok(env_shell) = std::env::var("STARSHIP_SHELL") {
|
||||
} else if let Some(env_shell) = context.get_env("STARSHIP_SHELL") {
|
||||
(env_shell.into(), &[] as &[&str])
|
||||
} else if cfg!(windows) {
|
||||
// `/C` is added by `handle_shell`
|
||||
("cmd".into(), &[] as &[&str])
|
||||
} else {
|
||||
("sh".into(), &[] as &[&str])
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to run the given command in a shell by passing it as `stdin` to `get_shell()`
|
||||
#[cfg(not(windows))]
|
||||
fn shell_command(cmd: &str, shell_args: &[&str]) -> Option<Output> {
|
||||
let (shell, shell_args) = get_shell(shell_args);
|
||||
let mut command = create_command(shell.as_ref()).ok()?;
|
||||
/// Attempt to run the given command in a shell by passing it as either `stdin` or an argument to `get_shell()`,
|
||||
/// depending on the configuration or by invoking a platform-specific falback shell if `shell` is empty.
|
||||
fn shell_command(cmd: &str, config: &CustomConfig, context: &Context) -> Option<Output> {
|
||||
let (shell, shell_args) = get_shell(config.shell.0.as_ref(), context);
|
||||
let mut use_stdin = config.use_stdin;
|
||||
|
||||
let mut command = match create_command(shell.as_ref()) {
|
||||
Ok(command) => command,
|
||||
// Don't attempt to use fallback shell if the user specified a shell
|
||||
Err(error) if !shell_args.is_empty() => {
|
||||
log::debug!(
|
||||
"Error creating command with STARSHIP_SHELL, falling back to fallback shell: {}",
|
||||
error
|
||||
);
|
||||
|
||||
// Skip `handle_shell` and just set the shell and command
|
||||
use_stdin = Some(!cfg!(windows));
|
||||
|
||||
if cfg!(windows) {
|
||||
let mut c = create_command("cmd").ok()?;
|
||||
c.arg("/C");
|
||||
c
|
||||
} else {
|
||||
let mut c = create_command("/usr/bin/env").ok()?;
|
||||
c.arg("sh");
|
||||
c
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
command
|
||||
.current_dir(&context.current_dir)
|
||||
.args(shell_args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
handle_powershell(&mut command, &shell, shell_args);
|
||||
let use_stdin = use_stdin.unwrap_or_else(|| handle_shell(&mut command, &shell, shell_args));
|
||||
|
||||
let mut child = match command.spawn() {
|
||||
Ok(command) => command,
|
||||
Err(err) => {
|
||||
log::trace!("Error executing command: {:?}", err);
|
||||
log::debug!(
|
||||
"Could not launch command with given shell or STARSHIP_SHELL env variable, retrying with /usr/bin/env sh"
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
Command::new("/usr/bin/env")
|
||||
.arg("sh")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.ok()?
|
||||
}
|
||||
};
|
||||
|
||||
child.stdin.as_mut()?.write_all(cmd.as_bytes()).ok()?;
|
||||
child.wait_with_output().ok()
|
||||
}
|
||||
|
||||
/// Attempt to run the given command in a shell by passing it as `stdin` to `get_shell()`,
|
||||
/// or by invoking cmd.exe /C.
|
||||
#[cfg(windows)]
|
||||
fn shell_command(cmd: &str, shell_args: &[&str]) -> Option<Output> {
|
||||
let (shell, shell_args) = if !shell_args.is_empty() {
|
||||
(
|
||||
Some(std::borrow::Cow::Borrowed(shell_args[0])),
|
||||
&shell_args[1..],
|
||||
)
|
||||
} else if let Some(env_shell) = std::env::var("STARSHIP_SHELL")
|
||||
.ok()
|
||||
.filter(|s| !cfg!(test) && !s.is_empty())
|
||||
{
|
||||
(Some(std::borrow::Cow::Owned(env_shell)), &[] as &[&str])
|
||||
} else {
|
||||
(None, &[] as &[&str])
|
||||
};
|
||||
|
||||
if let Some(forced_shell) = shell {
|
||||
let mut command = create_command(forced_shell.as_ref()).ok()?;
|
||||
|
||||
command
|
||||
.args(shell_args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
handle_powershell(&mut command, &forced_shell, shell_args);
|
||||
|
||||
if let Ok(mut child) = command.spawn() {
|
||||
child.stdin.as_mut()?.write_all(cmd.as_bytes()).ok()?;
|
||||
|
||||
return child.wait_with_output().ok();
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Could not launch command with given shell or STARSHIP_SHELL env variable, retrying with cmd.exe /C"
|
||||
);
|
||||
if !use_stdin {
|
||||
command.arg(cmd);
|
||||
}
|
||||
|
||||
let command = create_command("cmd")
|
||||
.ok()?
|
||||
.arg("/C")
|
||||
.arg(cmd)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn();
|
||||
let mut child = match command.spawn() {
|
||||
Ok(child) => child,
|
||||
Err(error) => {
|
||||
log::debug!(
|
||||
"Failed to run command with given shell or STARSHIP_SHELL env variable:: {}",
|
||||
error
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
command.ok()?.wait_with_output().ok()
|
||||
if use_stdin {
|
||||
child.stdin.as_mut()?.write_all(cmd.as_bytes()).ok()?;
|
||||
}
|
||||
|
||||
let mut output = child.controlled_with_output();
|
||||
|
||||
if !config.ignore_timeout {
|
||||
output = output
|
||||
.time_limit(Duration::from_millis(context.root_config.command_timeout))
|
||||
.terminate_for_timeout()
|
||||
}
|
||||
|
||||
match output.wait().ok()? {
|
||||
None => {
|
||||
log::warn!("Executing custom command {cmd:?} timed out.");
|
||||
log::warn!("You can set command_timeout in your config to a higher value or set ignore_timeout to true for this module to allow longer-running commands to keep executing.");
|
||||
None
|
||||
}
|
||||
Some(status) => Some(status),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the given command capturing all output, and return whether it return 0
|
||||
fn exec_when(cmd: &str, shell_args: &[&str]) -> bool {
|
||||
fn exec_when(cmd: &str, config: &CustomConfig, context: &Context) -> bool {
|
||||
log::trace!("Running '{}'", cmd);
|
||||
|
||||
if let Some(output) = shell_command(cmd, shell_args) {
|
||||
if let Some(output) = shell_command(cmd, config, context) {
|
||||
if !output.status.success() {
|
||||
log::trace!("non-zero exit code '{:?}'", output.status.code());
|
||||
log::trace!(
|
||||
@ -216,10 +212,10 @@ fn exec_when(cmd: &str, shell_args: &[&str]) -> bool {
|
||||
}
|
||||
|
||||
/// Execute the given command, returning its output on success
|
||||
fn exec_command(cmd: &str, shell_args: &[&str]) -> Option<String> {
|
||||
log::trace!("Running '{}'", cmd);
|
||||
fn exec_command(cmd: &str, context: &Context, config: &CustomConfig) -> Option<String> {
|
||||
log::trace!("Running '{cmd}'");
|
||||
|
||||
if let Some(output) = shell_command(cmd, shell_args) {
|
||||
if let Some(output) = shell_command(cmd, config, context) {
|
||||
if !output.status.success() {
|
||||
log::trace!("Non-zero exit code '{:?}'", output.status.code());
|
||||
log::trace!(
|
||||
@ -241,14 +237,31 @@ fn exec_command(cmd: &str, shell_args: &[&str]) -> Option<String> {
|
||||
|
||||
/// If the specified shell refers to PowerShell, adds the arguments "-Command -" to the
|
||||
/// given command.
|
||||
fn handle_powershell(command: &mut Command, shell: &str, shell_args: &[&str]) {
|
||||
let is_powershell = shell.ends_with("pwsh.exe")
|
||||
|| shell.ends_with("powershell.exe")
|
||||
|| shell.ends_with("pwsh")
|
||||
|| shell.ends_with("powershell");
|
||||
/// Retruns `false` if the shell shell expects scripts as arguments, `true` if as `stdin`.
|
||||
fn handle_shell(command: &mut Command, shell: &str, shell_args: &[&str]) -> bool {
|
||||
let shell_exe = Path::new(shell).file_stem();
|
||||
let no_args = shell_args.is_empty();
|
||||
|
||||
if is_powershell && shell_args.is_empty() {
|
||||
command.arg("-NoProfile").arg("-Command").arg("-");
|
||||
match shell_exe.and_then(std::ffi::OsStr::to_str) {
|
||||
Some("pwsh" | "powershell") => {
|
||||
if no_args {
|
||||
command.arg("-NoProfile").arg("-Command").arg("-");
|
||||
}
|
||||
true
|
||||
}
|
||||
Some("cmd") => {
|
||||
if no_args {
|
||||
command.arg("/C");
|
||||
}
|
||||
false
|
||||
}
|
||||
Some("nu") => {
|
||||
if no_args {
|
||||
command.arg("-c");
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,10 +269,15 @@ fn handle_powershell(command: &mut Command, shell: &str, shell_args: &[&str]) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test::ModuleRenderer;
|
||||
use ansi_term::Color;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
const SHELL: &[&str] = &["/bin/sh"];
|
||||
#[cfg(windows)]
|
||||
const SHELL: &[&str] = &[];
|
||||
const SHELL: &[&str] = &["cmd"];
|
||||
|
||||
#[cfg(not(windows))]
|
||||
const FAILING_COMMAND: &str = "false";
|
||||
@ -268,66 +286,394 @@ mod tests {
|
||||
|
||||
const UNKNOWN_COMMAND: &str = "ydelsyiedsieudleylse dyesdesl";
|
||||
|
||||
#[test]
|
||||
fn when_returns_right_value() {
|
||||
assert!(exec_when("echo hello", SHELL));
|
||||
assert!(!exec_when(FAILING_COMMAND, SHELL));
|
||||
fn render_cmd(cmd: &str) -> io::Result<Option<String>> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let cmd = cmd.to_owned();
|
||||
let shell = SHELL.iter().map(|s| s.to_owned()).collect::<Vec<_>>();
|
||||
let out = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "$output"
|
||||
command = cmd
|
||||
shell = shell
|
||||
when = true
|
||||
ignore_timeout = true
|
||||
})
|
||||
.collect();
|
||||
dir.close()?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn render_when(cmd: &str) -> io::Result<bool> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let cmd = cmd.to_owned();
|
||||
let shell = SHELL.iter().map(|s| s.to_owned()).collect::<Vec<_>>();
|
||||
let out = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "test"
|
||||
when = cmd
|
||||
shell = shell
|
||||
ignore_timeout = true
|
||||
})
|
||||
.collect()
|
||||
.is_some();
|
||||
dir.close()?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_returns_false_if_invalid_command() {
|
||||
assert!(!exec_when(UNKNOWN_COMMAND, SHELL));
|
||||
fn when_returns_right_value() -> io::Result<()> {
|
||||
assert!(render_cmd("echo hello")?.is_some());
|
||||
assert!(render_cmd(FAILING_COMMAND)?.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_returns_false_if_invalid_command() -> io::Result<()> {
|
||||
assert!(!render_when(UNKNOWN_COMMAND)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn command_returns_right_string() {
|
||||
assert_eq!(exec_command("echo hello", SHELL), Some("hello\n".into()));
|
||||
assert_eq!(
|
||||
exec_command("echo 강남스타일", SHELL),
|
||||
Some("강남스타일\n".into())
|
||||
);
|
||||
fn command_returns_right_string() -> io::Result<()> {
|
||||
assert_eq!(render_cmd("echo hello")?, Some("hello".into()));
|
||||
assert_eq!(render_cmd("echo 강남스타일")?, Some("강남스타일".into()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn command_returns_right_string() {
|
||||
assert_eq!(exec_command("echo hello", SHELL), Some("hello\r\n".into()));
|
||||
assert_eq!(
|
||||
exec_command("echo 강남스타일", SHELL),
|
||||
Some("강남스타일\r\n".into())
|
||||
);
|
||||
fn command_returns_right_string() -> io::Result<()> {
|
||||
assert_eq!(render_cmd("echo hello")?, Some("hello".into()));
|
||||
assert_eq!(render_cmd("echo 강남스타일")?, Some("강남스타일".into()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn command_ignores_stderr() {
|
||||
assert_eq!(
|
||||
exec_command("echo foo 1>&2; echo bar", SHELL),
|
||||
Some("bar\n".into())
|
||||
);
|
||||
assert_eq!(
|
||||
exec_command("echo foo; echo bar 1>&2", SHELL),
|
||||
Some("foo\n".into())
|
||||
);
|
||||
fn command_ignores_stderr() -> io::Result<()> {
|
||||
assert_eq!(render_cmd("echo foo 1>&2; echo bar")?, Some("bar".into()));
|
||||
assert_eq!(render_cmd("echo foo; echo bar 1>&2")?, Some("foo".into()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn command_ignores_stderr() {
|
||||
assert_eq!(
|
||||
exec_command("echo foo 1>&2 & echo bar", SHELL),
|
||||
Some("bar\r\n".into())
|
||||
);
|
||||
assert_eq!(
|
||||
exec_command("echo foo& echo bar 1>&2", SHELL),
|
||||
Some("foo\r\n".into())
|
||||
);
|
||||
fn command_ignores_stderr() -> io::Result<()> {
|
||||
assert_eq!(render_cmd("echo foo 1>&2 & echo bar")?, Some("bar".into()));
|
||||
assert_eq!(render_cmd("echo foo& echo bar 1>&2")?, Some("foo".into()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_can_fail() {
|
||||
assert_eq!(exec_command(FAILING_COMMAND, SHELL), None);
|
||||
assert_eq!(exec_command(UNKNOWN_COMMAND, SHELL), None);
|
||||
fn command_can_fail() -> io::Result<()> {
|
||||
assert_eq!(render_cmd(FAILING_COMMAND)?, None);
|
||||
assert_eq!(render_cmd(UNKNOWN_COMMAND)?, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cwd_command() -> io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
let mut f = File::create(dir.path().join("a.txt"))?;
|
||||
write!(f, "hello")?;
|
||||
f.sync_all()?;
|
||||
|
||||
let cat = if cfg!(windows) { "type" } else { "cat" };
|
||||
let cmd = format!("{cat} a.txt");
|
||||
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
command = cmd
|
||||
when = true
|
||||
ignore_timeout = true
|
||||
})
|
||||
.collect();
|
||||
let expected = Some(format!("{}", Color::Green.bold().paint("hello ")));
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cwd_when() -> io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
File::create(dir.path().join("a.txt"))?.sync_all()?;
|
||||
|
||||
let cat = if cfg!(windows) { "type" } else { "cat" };
|
||||
let cmd = format!("{cat} a.txt");
|
||||
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "test"
|
||||
when = cmd
|
||||
ignore_timeout = true
|
||||
})
|
||||
.collect();
|
||||
let expected = Some("test".to_owned());
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_stdin_false() -> io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
let shell = if cfg!(windows) {
|
||||
vec![
|
||||
"powershell".to_owned(),
|
||||
"-NoProfile".to_owned(),
|
||||
"-Command".to_owned(),
|
||||
]
|
||||
} else {
|
||||
vec!["sh".to_owned(), "-c".to_owned()]
|
||||
};
|
||||
|
||||
// `use_stdin = false` doesn't like Korean on Windows
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
command = "echo test"
|
||||
when = true
|
||||
use_stdin = false
|
||||
shell = shell
|
||||
ignore_timeout = true
|
||||
})
|
||||
.collect();
|
||||
let expected = Some(format!("{}", Color::Green.bold().paint("test ")));
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_stdin_true() -> io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
let shell = if cfg!(windows) {
|
||||
vec![
|
||||
"powershell".to_owned(),
|
||||
"-NoProfile".to_owned(),
|
||||
"-Command".to_owned(),
|
||||
"-".to_owned(),
|
||||
]
|
||||
} else {
|
||||
vec!["sh".to_owned()]
|
||||
};
|
||||
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
command = "echo 강남스타일"
|
||||
when = true
|
||||
use_stdin = true
|
||||
ignore_timeout = true
|
||||
shell = shell
|
||||
})
|
||||
.collect();
|
||||
let expected = Some(format!("{}", Color::Green.bold().paint("강남스타일 ")));
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn when_true_with_string() -> std::io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "test"
|
||||
shell = ["sh"]
|
||||
when = "true"
|
||||
ignore_timeout = true
|
||||
})
|
||||
.collect();
|
||||
let expected = Some("test".to_string());
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn when_false_with_string() -> std::io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "test"
|
||||
shell = ["sh"]
|
||||
when = "false"
|
||||
ignore_timeout = true
|
||||
})
|
||||
.collect();
|
||||
let expected = None;
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_true_with_bool() -> std::io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "test"
|
||||
when = true
|
||||
})
|
||||
.collect();
|
||||
let expected = Some("test".to_string());
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn when_false_with_bool() -> std::io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "test"
|
||||
when = false
|
||||
})
|
||||
.collect();
|
||||
let expected = None;
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeout_short_cmd() -> std::io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
let shell = if cfg!(windows) {
|
||||
"powershell".to_owned()
|
||||
} else {
|
||||
"sh".to_owned()
|
||||
};
|
||||
|
||||
let when = if cfg!(windows) {
|
||||
"$true".to_owned()
|
||||
} else {
|
||||
"true".to_owned()
|
||||
};
|
||||
|
||||
// Use a long timeout to ensure that the test doesn't fail
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
command_timeout = 10000
|
||||
[custom.test]
|
||||
format = "test"
|
||||
when = when
|
||||
shell = shell
|
||||
ignore_timeout = false
|
||||
})
|
||||
.collect();
|
||||
let expected = Some("test".to_owned());
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeout_cmd() -> std::io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
let shell = if cfg!(windows) {
|
||||
"powershell".to_owned()
|
||||
} else {
|
||||
"sh".to_owned()
|
||||
};
|
||||
|
||||
// Use a long timeout to ensure that the test doesn't fail
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "test"
|
||||
when = "sleep 3"
|
||||
shell = shell
|
||||
ignore_timeout = false
|
||||
})
|
||||
.collect();
|
||||
let expected = None;
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_aliases_work() -> std::io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
File::create(dir.path().join("a.txt"))?;
|
||||
std::fs::create_dir(dir.path().join("dir"))?;
|
||||
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "test"
|
||||
files = ["a.txt"]
|
||||
})
|
||||
.collect();
|
||||
let expected = Some("test".to_string());
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "test"
|
||||
extensions = ["txt"]
|
||||
})
|
||||
.collect();
|
||||
let expected = Some("test".to_string());
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
let actual = ModuleRenderer::new("custom.test")
|
||||
.path(dir.path())
|
||||
.config(toml::toml! {
|
||||
[custom.test]
|
||||
format = "test"
|
||||
directories = ["dir"]
|
||||
})
|
||||
.collect();
|
||||
let expected = Some("test".to_string());
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
dir.close()
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,10 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
}
|
||||
};
|
||||
|
||||
if ctx == "default" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
||||
formatter
|
||||
.map_meta(|variable, _| match variable {
|
||||
@ -307,6 +311,24 @@ mod tests {
|
||||
cfg_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_docker_context_default() -> io::Result<()> {
|
||||
let cfg_dir = tempfile::tempdir()?;
|
||||
|
||||
let actual = ModuleRenderer::new("docker_context")
|
||||
.env("DOCKER_CONTEXT", "default")
|
||||
.config(toml::toml! {
|
||||
[docker_context]
|
||||
only_with_files = false
|
||||
})
|
||||
.collect();
|
||||
let expected = None;
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
|
||||
cfg_dir.close()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_docker_context_overrides_config() -> io::Result<()> {
|
||||
let cfg_dir = tempfile::tempdir()?;
|
||||
|
@ -1,5 +1,6 @@
|
||||
use ini::Ini;
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use std::ops::Deref;
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@ -9,15 +10,15 @@ use crate::configs::gcloud::GcloudConfig;
|
||||
use crate::formatter::StringFormatter;
|
||||
use crate::utils;
|
||||
|
||||
type Account = (String, Option<String>);
|
||||
type Account<'a> = (&'a str, Option<&'a str>);
|
||||
|
||||
struct GcloudContext {
|
||||
config_name: String,
|
||||
config_path: PathBuf,
|
||||
config: OnceCell<String>,
|
||||
config: OnceCell<Option<Ini>>,
|
||||
}
|
||||
|
||||
impl GcloudContext {
|
||||
impl<'a> GcloudContext {
|
||||
pub fn new(config_name: &str, config_path: &Path) -> Self {
|
||||
Self {
|
||||
config_name: config_name.to_string(),
|
||||
@ -26,54 +27,27 @@ impl GcloudContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_config(&self) -> Option<&str> {
|
||||
let config = self
|
||||
.config
|
||||
.get_or_try_init(|| utils::read_file(&self.config_path));
|
||||
match config {
|
||||
Ok(data) => Some(data),
|
||||
Err(_) => None,
|
||||
}
|
||||
fn get_config(&self) -> Option<&Ini> {
|
||||
self.config
|
||||
.get_or_init(|| Ini::load_from_file(&self.config_path).ok())
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_account(&self) -> Option<Account> {
|
||||
pub fn get_account(&'a self) -> Option<Account<'a>> {
|
||||
let config = self.get_config()?;
|
||||
let account_line = config
|
||||
.lines()
|
||||
.skip_while(|line| *line != "[core]")
|
||||
.skip(1)
|
||||
.take_while(|line| !line.starts_with('['))
|
||||
.find(|line| line.starts_with("account"))?;
|
||||
let account = account_line.split_once('=')?.1.trim();
|
||||
let account = config.section(Some("core"))?.get("account")?;
|
||||
let mut segments = account.splitn(2, '@');
|
||||
Some((
|
||||
segments.next().map(String::from)?,
|
||||
segments.next().map(String::from),
|
||||
))
|
||||
Some((segments.next()?, segments.next()))
|
||||
}
|
||||
|
||||
pub fn get_project(&self) -> Option<String> {
|
||||
pub fn get_project(&'a self) -> Option<&'a str> {
|
||||
let config = self.get_config()?;
|
||||
let project_line = config
|
||||
.lines()
|
||||
.skip_while(|line| *line != "[core]")
|
||||
.skip(1)
|
||||
.take_while(|line| !line.starts_with('['))
|
||||
.find(|line| line.starts_with("project"))?;
|
||||
let project = project_line.split_once('=')?.1.trim();
|
||||
Some(project.to_string())
|
||||
config.section(Some("core"))?.get("project")
|
||||
}
|
||||
|
||||
pub fn get_region(&self) -> Option<String> {
|
||||
pub fn get_region(&'a self) -> Option<&'a str> {
|
||||
let config = self.get_config()?;
|
||||
let region_line = config
|
||||
.lines()
|
||||
.skip_while(|line| *line != "[compute]")
|
||||
.skip(1)
|
||||
.take_while(|line| !line.starts_with('['))
|
||||
.find(|line| line.starts_with("region"))?;
|
||||
let region = region_line.split_once('=')?.1.trim();
|
||||
Some(region.to_string())
|
||||
config.section(Some("compute"))?.get("region")
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +86,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
|
||||
let (config_name, config_path) = get_current_config(context)?;
|
||||
let gcloud_context = GcloudContext::new(&config_name, &config_path);
|
||||
let account: Lazy<Option<Account>, _> = Lazy::new(|| gcloud_context.get_account());
|
||||
let account: Lazy<Option<Account<'_>>, _> = Lazy::new(|| gcloud_context.get_account());
|
||||
|
||||
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
||||
formatter
|
||||
@ -126,35 +100,32 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||
})
|
||||
.map(|variable| match variable {
|
||||
"account" => account
|
||||
.deref()
|
||||
.as_ref()
|
||||
.map(|(account, _)| (*account).clone())
|
||||
.map(|(account, _)| account)
|
||||
.map(Cow::Borrowed)
|
||||
.map(Ok),
|
||||
"domain" => account
|
||||
.deref()
|
||||
.as_ref()
|
||||
.and_then(|(_, domain)| (*domain).clone())
|
||||
.and_then(|(_, domain)| domain)
|
||||
.map(Cow::Borrowed)
|
||||
.map(Ok),
|
||||
"region" => gcloud_context
|
||||
.get_region()
|
||||
.map(|region| {
|
||||
config
|
||||
.region_aliases
|
||||
.get(®ion)
|
||||
.map_or(region, |alias| (*alias).to_owned())
|
||||
})
|
||||
.map(|region| config.region_aliases.get(region).copied().unwrap_or(region))
|
||||
.map(Cow::Borrowed)
|
||||
.map(Ok),
|
||||
"project" => context
|
||||
.get_env("CLOUDSDK_CORE_PROJECT")
|
||||
.or_else(|| gcloud_context.get_project())
|
||||
.map(Cow::Owned)
|
||||
.or_else(|| gcloud_context.get_project().map(Cow::Borrowed))
|
||||
.map(|project| {
|
||||
config
|
||||
.project_aliases
|
||||
.get(&project)
|
||||
.map_or(project, |alias| (*alias).to_owned())
|
||||
.get(project.as_ref())
|
||||
.copied()
|
||||
.map(Cow::Borrowed)
|
||||
.unwrap_or(project)
|
||||
})
|
||||
.map(Ok),
|
||||
"active" => Some(Ok(gcloud_context.config_name.clone())),
|
||||
"active" => Some(Ok(Cow::Borrowed(&gcloud_context.config_name))),
|
||||
_ => None,
|
||||
})
|
||||
.parse(None, Some(context))
|
||||
|
@ -163,6 +163,12 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
|
||||
"vagrant" => vagrant::module(context),
|
||||
"vcsh" => vcsh::module(context),
|
||||
"zig" => zig::module(context),
|
||||
// Added for tests, avoid potential side effects in production code.
|
||||
#[cfg(test)]
|
||||
custom if custom.starts_with("custom.") => {
|
||||
// SAFETY: We just checked that the module starts with "custom."
|
||||
custom::module(custom.strip_prefix("custom.").unwrap(), context)
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Error: Unknown module {}. Use starship module --list to list out all supported modules.", module);
|
||||
None
|
||||
|
Loading…
Reference in New Issue
Block a user