refactor(custom): various improvements (#3829)

This commit is contained in:
David Knaack 2022-04-09 17:32:45 +02:00 committed by GitHub
parent e61394a97a
commit 28da85061b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 574 additions and 182 deletions

View File

@ -4694,9 +4694,11 @@
"type": "string" "type": "string"
}, },
"when": { "when": {
"type": [ "default": false,
"string", "allOf": [
"null" {
"$ref": "#/definitions/Either_for_Boolean_and_String"
}
] ]
}, },
"shell": { "shell": {
@ -4719,21 +4721,21 @@
"default": false, "default": false,
"type": "boolean" "type": "boolean"
}, },
"files": { "detect_files": {
"default": [], "default": [],
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} }
}, },
"extensions": { "detect_extensions": {
"default": [], "default": [],
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} }
}, },
"directories": { "detect_folders": {
"default": [], "default": [],
"type": "array", "type": "array",
"items": { "items": {
@ -4745,8 +4747,28 @@
"string", "string",
"null" "null"
] ]
},
"use_stdin": {
"type": [
"boolean",
"null"
]
},
"ignore_timeout": {
"default": false,
"type": "boolean"
} }
} }
},
"Either_for_Boolean_and_String": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
} }
} }
} }

View File

@ -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: 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 file whose name is in `detect_files`
- The current directory contains a directory whose name is in `directories` - The current directory contains a directory whose name is in `detect_folders`
- The current directory contains a file whose extension is in `extensions` - The current directory contains a file whose extension is in `detect_extensions`
- The `when` command returns 0 - The `when` command returns 0
- The current Operating System (std::env::consts::OS) matchs with `os` field if defined. - 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 ### Options
| Option | Default | Description | | Option | Default | Description |
| ------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `command` | `""` | The command whose output should be printed. The command will be passed on stdin to the shell. | | `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. | | `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) | | `shell` | | [See below](#custom-command-shell) |
| `description` | `"<custom module>"` | The description of the module that is shown when running `starship explain`. | | `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. | | `detect_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. | | `detect_folders` | `[]` | 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. | | `detect_extensions` | `[]` | The extensions that will be searched in the working directory for a match. |
| `symbol` | `""` | The symbol used before displaying the command output. | | `symbol` | `""` | The symbol used before displaying the command output. |
| `style` | `"bold green"` | The style for the module. | | `style` | `"bold green"` | The style for the module. |
| `format` | `"[$symbol($output )]($style)"` | The format for the module. | | `format` | `"[$symbol($output )]($style)"` | The format for the module. |
| `disabled` | `false` | Disables this `custom` 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). | | `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 ### 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, 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 -`. 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. This behavior can be avoided by explicitly passing arguments to the shell, e.g.
```toml ```toml
@ -3782,12 +3787,18 @@ with shell details and starship configuration if you hit such scenario.
[custom.foo] [custom.foo]
command = "echo foo" # shows output of command 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" """ when = """ test "$HOME" == "$PWD" """
format = " transcending [$output]($style)" format = " transcending [$output]($style)"
[custom.time] [custom.time]
command = "time /T" command = "time /T"
extensions = ["pst"] # filters *.pst files detect_extensions = ["pst"] # filters *.pst files
shell = ["pwsh.exe", "-NoProfile", "-Command", "-"] 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
``` ```

View File

@ -1,4 +1,4 @@
use crate::config::VecOr; use crate::config::{Either, VecOr};
use serde::{self, Deserialize, Serialize}; use serde::{self, Deserialize, Serialize};
@ -9,17 +9,22 @@ pub struct CustomConfig<'a> {
pub format: &'a str, pub format: &'a str,
pub symbol: &'a str, pub symbol: &'a str,
pub command: &'a str, pub command: &'a str,
#[serde(skip_serializing_if = "Option::is_none")] pub when: Either<bool, &'a str>,
pub when: Option<&'a str>,
pub shell: VecOr<&'a str>, pub shell: VecOr<&'a str>,
pub description: &'a str, pub description: &'a str,
pub style: &'a str, pub style: &'a str,
pub disabled: bool, pub disabled: bool,
pub files: Vec<&'a str>, #[serde(alias = "files")]
pub extensions: Vec<&'a str>, pub detect_files: Vec<&'a str>,
pub directories: 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")] #[serde(skip_serializing_if = "Option::is_none")]
pub os: Option<&'a str>, 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> { impl<'a> Default for CustomConfig<'a> {
@ -28,15 +33,17 @@ impl<'a> Default for CustomConfig<'a> {
format: "[$symbol($output )]($style)", format: "[$symbol($output )]($style)",
symbol: "", symbol: "",
command: "", command: "",
when: None, when: Either::First(false),
shell: VecOr::default(), shell: VecOr::default(),
description: "<custom config>", description: "<custom config>",
style: "green bold", style: "green bold",
disabled: false, disabled: false,
files: Vec::default(), detect_files: Vec::default(),
extensions: Vec::default(), detect_extensions: Vec::default(),
directories: Vec::default(), detect_folders: Vec::default(),
os: None, os: None,
use_stdin: None,
ignore_timeout: false,
} }
} }
} }

View File

@ -1,11 +1,18 @@
use std::env; use std::env;
use std::io::Write; 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 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 /// 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 let mut is_match = context
.try_begin_scan()? .try_begin_scan()?
.set_files(&config.files) .set_extensions(&config.detect_extensions)
.set_extensions(&config.extensions) .set_files(&config.detect_files)
.set_folders(&config.directories) .set_folders(&config.detect_folders)
.is_match(); .is_match();
if !is_match { if !is_match {
if let Some(when) = config.when { is_match = match config.when {
is_match = exec_when(when, &config.shell.0); Either::First(b) => b,
} Either::Second(s) => exec_when(s, &config, context),
};
if !is_match { if !is_match {
return None; return None;
@ -58,12 +66,7 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
}) })
.map_no_escaping(|variable| match variable { .map_no_escaping(|variable| match variable {
"output" => { "output" => {
if context.shell == Shell::Cmd && config.shell.0.is_empty() { let output = exec_command(config.command, context, &config)?;
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 trimmed = output.trim(); let trimmed = output.trim();
if trimmed.is_empty() { if trimmed.is_empty() {
@ -89,112 +92,105 @@ pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
Some(module) Some(module)
} }
/// Return the invoking shell, using `shell` and fallbacking in order to STARSHIP_SHELL and "sh" /// Return the invoking shell, using `shell` and fallbacking in order to STARSHIP_SHELL and "sh"/"cmd"
#[cfg(not(windows))] fn get_shell<'a, 'b>(
fn get_shell<'a, 'b>(shell_args: &'b [&'a str]) -> (std::borrow::Cow<'a, str>, &'b [&'a str]) { shell_args: &'b [&'a str],
context: &Context,
) -> (std::borrow::Cow<'a, str>, &'b [&'a str]) {
if !shell_args.is_empty() { if !shell_args.is_empty() {
(shell_args[0].into(), &shell_args[1..]) (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]) (env_shell.into(), &[] as &[&str])
} else if cfg!(windows) {
// `/C` is added by `handle_shell`
("cmd".into(), &[] as &[&str])
} else { } else {
("sh".into(), &[] as &[&str]) ("sh".into(), &[] as &[&str])
} }
} }
/// Attempt to run the given command in a shell by passing it as `stdin` to `get_shell()` /// Attempt to run the given command in a shell by passing it as either `stdin` or an argument to `get_shell()`,
#[cfg(not(windows))] /// depending on the configuration or by invoking a platform-specific falback shell if `shell` is empty.
fn shell_command(cmd: &str, shell_args: &[&str]) -> Option<Output> { fn shell_command(cmd: &str, config: &CustomConfig, context: &Context) -> Option<Output> {
let (shell, shell_args) = get_shell(shell_args); let (shell, shell_args) = get_shell(config.shell.0.as_ref(), context);
let mut command = create_command(shell.as_ref()).ok()?; 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 command
.current_dir(&context.current_dir)
.args(shell_args) .args(shell_args)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(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() { if !use_stdin {
Ok(command) => command, command.arg(cmd);
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"
);
} }
let command = create_command("cmd") let mut child = match command.spawn() {
.ok()? Ok(child) => child,
.arg("/C") Err(error) => {
.arg(cmd) log::debug!(
.stdin(Stdio::piped()) "Failed to run command with given shell or STARSHIP_SHELL env variable:: {}",
.stdout(Stdio::piped()) error
.stderr(Stdio::piped()) );
.spawn(); 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 /// 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); 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() { if !output.status.success() {
log::trace!("non-zero exit code '{:?}'", output.status.code()); log::trace!("non-zero exit code '{:?}'", output.status.code());
log::trace!( log::trace!(
@ -216,10 +212,10 @@ fn exec_when(cmd: &str, shell_args: &[&str]) -> bool {
} }
/// Execute the given command, returning its output on success /// Execute the given command, returning its output on success
fn exec_command(cmd: &str, shell_args: &[&str]) -> Option<String> { fn exec_command(cmd: &str, context: &Context, config: &CustomConfig) -> Option<String> {
log::trace!("Running '{}'", cmd); 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() { if !output.status.success() {
log::trace!("Non-zero exit code '{:?}'", output.status.code()); log::trace!("Non-zero exit code '{:?}'", output.status.code());
log::trace!( 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 /// If the specified shell refers to PowerShell, adds the arguments "-Command -" to the
/// given command. /// given command.
fn handle_powershell(command: &mut Command, shell: &str, shell_args: &[&str]) { /// Retruns `false` if the shell shell expects scripts as arguments, `true` if as `stdin`.
let is_powershell = shell.ends_with("pwsh.exe") fn handle_shell(command: &mut Command, shell: &str, shell_args: &[&str]) -> bool {
|| shell.ends_with("powershell.exe") let shell_exe = Path::new(shell).file_stem();
|| shell.ends_with("pwsh") let no_args = shell_args.is_empty();
|| shell.ends_with("powershell");
if is_powershell && shell_args.is_empty() { match shell_exe.and_then(std::ffi::OsStr::to_str) {
command.arg("-NoProfile").arg("-Command").arg("-"); 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 { mod tests {
use super::*; use super::*;
use crate::test::ModuleRenderer;
use ansi_term::Color;
use std::fs::File;
use std::io;
#[cfg(not(windows))] #[cfg(not(windows))]
const SHELL: &[&str] = &["/bin/sh"]; const SHELL: &[&str] = &["/bin/sh"];
#[cfg(windows)] #[cfg(windows)]
const SHELL: &[&str] = &[]; const SHELL: &[&str] = &["cmd"];
#[cfg(not(windows))] #[cfg(not(windows))]
const FAILING_COMMAND: &str = "false"; const FAILING_COMMAND: &str = "false";
@ -268,66 +286,394 @@ mod tests {
const UNKNOWN_COMMAND: &str = "ydelsyiedsieudleylse dyesdesl"; const UNKNOWN_COMMAND: &str = "ydelsyiedsieudleylse dyesdesl";
#[test] fn render_cmd(cmd: &str) -> io::Result<Option<String>> {
fn when_returns_right_value() { let dir = tempfile::tempdir()?;
assert!(exec_when("echo hello", SHELL)); let cmd = cmd.to_owned();
assert!(!exec_when(FAILING_COMMAND, SHELL)); 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] #[test]
fn when_returns_false_if_invalid_command() { fn when_returns_right_value() -> io::Result<()> {
assert!(!exec_when(UNKNOWN_COMMAND, SHELL)); 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] #[test]
#[cfg(not(windows))] #[cfg(not(windows))]
fn command_returns_right_string() { fn command_returns_right_string() -> io::Result<()> {
assert_eq!(exec_command("echo hello", SHELL), Some("hello\n".into())); assert_eq!(render_cmd("echo hello")?, Some("hello".into()));
assert_eq!( assert_eq!(render_cmd("echo 강남스타일")?, Some("강남스타일".into()));
exec_command("echo 강남스타일", SHELL), Ok(())
Some("강남스타일\n".into())
);
} }
#[test] #[test]
#[cfg(windows)] #[cfg(windows)]
fn command_returns_right_string() { fn command_returns_right_string() -> io::Result<()> {
assert_eq!(exec_command("echo hello", SHELL), Some("hello\r\n".into())); assert_eq!(render_cmd("echo hello")?, Some("hello".into()));
assert_eq!( assert_eq!(render_cmd("echo 강남스타일")?, Some("강남스타일".into()));
exec_command("echo 강남스타일", SHELL), Ok(())
Some("강남스타일\r\n".into())
);
} }
#[test] #[test]
#[cfg(not(windows))] #[cfg(not(windows))]
fn command_ignores_stderr() { fn command_ignores_stderr() -> io::Result<()> {
assert_eq!( assert_eq!(render_cmd("echo foo 1>&2; echo bar")?, Some("bar".into()));
exec_command("echo foo 1>&2; echo bar", SHELL), assert_eq!(render_cmd("echo foo; echo bar 1>&2")?, Some("foo".into()));
Some("bar\n".into()) Ok(())
);
assert_eq!(
exec_command("echo foo; echo bar 1>&2", SHELL),
Some("foo\n".into())
);
} }
#[test] #[test]
#[cfg(windows)] #[cfg(windows)]
fn command_ignores_stderr() { fn command_ignores_stderr() -> io::Result<()> {
assert_eq!( assert_eq!(render_cmd("echo foo 1>&2 & echo bar")?, Some("bar".into()));
exec_command("echo foo 1>&2 & echo bar", SHELL), assert_eq!(render_cmd("echo foo& echo bar 1>&2")?, Some("foo".into()));
Some("bar\r\n".into()) Ok(())
);
assert_eq!(
exec_command("echo foo& echo bar 1>&2", SHELL),
Some("foo\r\n".into())
);
} }
#[test] #[test]
fn command_can_fail() { fn command_can_fail() -> io::Result<()> {
assert_eq!(exec_command(FAILING_COMMAND, SHELL), None); assert_eq!(render_cmd(FAILING_COMMAND)?, None);
assert_eq!(exec_command(UNKNOWN_COMMAND, SHELL), 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()
} }
} }

View File

@ -163,6 +163,12 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
"vagrant" => vagrant::module(context), "vagrant" => vagrant::module(context),
"vcsh" => vcsh::module(context), "vcsh" => vcsh::module(context),
"zig" => zig::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); eprintln!("Error: Unknown module {}. Use starship module --list to list out all supported modules.", module);
None None