2020-04-11 16:37:24 +00:00
|
|
|
use std::io::Write;
|
|
|
|
use std::process::{Command, Output, Stdio};
|
|
|
|
|
|
|
|
use super::{Context, Module, RootModuleConfig};
|
|
|
|
|
2020-07-07 22:45:32 +00:00
|
|
|
use crate::{configs::custom::CustomConfig, formatter::StringFormatter};
|
2020-04-11 16:37:24 +00:00
|
|
|
|
|
|
|
/// Creates a custom module with some configuration
|
|
|
|
///
|
|
|
|
/// The relevant TOML config will set the files, extensions, and directories needed
|
|
|
|
/// for the module to be displayed. If none of them match, and optional "when"
|
|
|
|
/// command can be run -- if its result is 0, the module will be shown.
|
|
|
|
///
|
|
|
|
/// Finally, the content of the module itself is also set by a command.
|
2020-07-07 22:45:32 +00:00
|
|
|
pub fn module<'a>(name: &str, context: &'a Context) -> Option<Module<'a>> {
|
2020-04-11 16:37:24 +00:00
|
|
|
let toml_config = context.config.get_custom_module_config(name).expect(
|
|
|
|
"modules::custom::module should only be called after ensuring that the module exists",
|
|
|
|
);
|
|
|
|
let config = CustomConfig::load(toml_config);
|
|
|
|
|
|
|
|
let mut scan_dir = context.try_begin_scan()?;
|
|
|
|
|
|
|
|
if !config.files.0.is_empty() {
|
|
|
|
scan_dir = scan_dir.set_files(&config.files.0);
|
|
|
|
}
|
|
|
|
if !config.extensions.0.is_empty() {
|
|
|
|
scan_dir = scan_dir.set_extensions(&config.extensions.0);
|
|
|
|
}
|
|
|
|
if !config.directories.0.is_empty() {
|
|
|
|
scan_dir = scan_dir.set_folders(&config.directories.0);
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut is_match = scan_dir.is_match();
|
|
|
|
|
|
|
|
if !is_match {
|
|
|
|
if let Some(when) = config.when {
|
2020-05-27 07:38:05 +00:00
|
|
|
is_match = exec_when(when, &config.shell.0);
|
2020-04-11 16:37:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if !is_match {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut module = Module::new(name, config.description, Some(toml_config));
|
|
|
|
|
2020-07-07 22:45:32 +00:00
|
|
|
let output = exec_command(config.command, &config.shell.0)?;
|
2020-04-11 16:37:24 +00:00
|
|
|
|
2020-07-07 22:45:32 +00:00
|
|
|
let trimmed = output.trim();
|
|
|
|
if trimmed.is_empty() {
|
|
|
|
return None;
|
2020-04-11 16:37:24 +00:00
|
|
|
}
|
|
|
|
|
2020-07-07 22:45:32 +00:00
|
|
|
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
|
|
|
formatter
|
|
|
|
.map_meta(|var, _| match var {
|
|
|
|
"symbol" => Some(config.symbol),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.map_style(|variable| match variable {
|
|
|
|
"style" => Some(Ok(config.style)),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.map(|variable| match variable {
|
|
|
|
// This may result in multiple calls to `get_module_version` when a user have
|
|
|
|
// multiple `$version` variables defined in `format`.
|
|
|
|
"output" => Some(Ok(trimmed)),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.parse(None)
|
|
|
|
});
|
|
|
|
|
|
|
|
module.set_segments(match parsed {
|
|
|
|
Ok(segments) => segments,
|
|
|
|
Err(error) => {
|
|
|
|
log::warn!("Error in module `custom.{}`:\n{}", name, error);
|
2020-04-11 16:37:24 +00:00
|
|
|
return None;
|
|
|
|
}
|
2020-07-07 22:45:32 +00:00
|
|
|
});
|
2020-04-11 16:37:24 +00:00
|
|
|
|
2020-07-07 22:45:32 +00:00
|
|
|
Some(module)
|
2020-04-11 16:37:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Return the invoking shell, using `shell` and fallbacking in order to STARSHIP_SHELL and "sh"
|
|
|
|
#[cfg(not(windows))]
|
2020-05-27 07:38:05 +00:00
|
|
|
fn get_shell<'a, 'b>(shell_args: &'b [&'a str]) -> (std::borrow::Cow<'a, str>, &'b [&'a str]) {
|
|
|
|
if !shell_args.is_empty() {
|
|
|
|
(shell_args[0].into(), &shell_args[1..])
|
2020-04-11 16:37:24 +00:00
|
|
|
} else if let Ok(env_shell) = std::env::var("STARSHIP_SHELL") {
|
2020-05-27 07:38:05 +00:00
|
|
|
(env_shell.into(), &[] as &[&str])
|
2020-04-11 16:37:24 +00:00
|
|
|
} else {
|
2020-05-27 07:38:05 +00:00
|
|
|
("sh".into(), &[] as &[&str])
|
2020-04-11 16:37:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Attempt to run the given command in a shell by passing it as `stdin` to `get_shell()`
|
|
|
|
#[cfg(not(windows))]
|
2020-05-27 07:38:05 +00:00
|
|
|
fn shell_command(cmd: &str, shell_args: &[&str]) -> Option<Output> {
|
|
|
|
let (shell, shell_args) = get_shell(shell_args);
|
|
|
|
let mut command = Command::new(shell.as_ref());
|
|
|
|
|
|
|
|
command
|
|
|
|
.args(shell_args)
|
2020-04-11 16:37:24 +00:00
|
|
|
.stdin(Stdio::piped())
|
|
|
|
.stdout(Stdio::piped())
|
2020-05-27 07:38:05 +00:00
|
|
|
.stderr(Stdio::piped());
|
|
|
|
|
|
|
|
handle_powershell(&mut command, &shell, shell_args);
|
2020-04-11 16:37:24 +00:00
|
|
|
|
2020-05-27 07:38:05 +00:00
|
|
|
let mut child = match command.spawn() {
|
2020-04-11 16:37:24 +00:00
|
|
|
Ok(command) => command,
|
2020-06-26 15:30:11 +00:00
|
|
|
Err(err) => {
|
|
|
|
log::trace!("Error executing command: {:?}", err);
|
2020-04-11 16:37:24 +00:00
|
|
|
log::debug!(
|
2020-06-28 19:41:05 +00:00
|
|
|
"Could not launch command with given shell or STARSHIP_SHELL env variable, retrying with /usr/bin/env sh"
|
2020-04-11 16:37:24 +00:00
|
|
|
);
|
|
|
|
|
2020-06-28 19:41:05 +00:00
|
|
|
Command::new("/usr/bin/env")
|
2020-04-11 16:37:24 +00:00
|
|
|
.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)]
|
2020-05-27 07:38:05 +00:00
|
|
|
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..],
|
|
|
|
)
|
2020-04-11 16:37:24 +00:00
|
|
|
} else if let Ok(env_shell) = std::env::var("STARSHIP_SHELL") {
|
2020-05-27 07:38:05 +00:00
|
|
|
(Some(std::borrow::Cow::Owned(env_shell)), &[] as &[&str])
|
2020-04-11 16:37:24 +00:00
|
|
|
} else {
|
2020-05-27 07:38:05 +00:00
|
|
|
(None, &[] as &[&str])
|
2020-04-11 16:37:24 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if let Some(forced_shell) = shell {
|
2020-05-27 07:38:05 +00:00
|
|
|
let mut command = Command::new(forced_shell.as_ref());
|
|
|
|
|
|
|
|
command
|
|
|
|
.args(shell_args)
|
2020-04-11 16:37:24 +00:00
|
|
|
.stdin(Stdio::piped())
|
|
|
|
.stdout(Stdio::piped())
|
2020-05-27 07:38:05 +00:00
|
|
|
.stderr(Stdio::piped());
|
|
|
|
|
|
|
|
handle_powershell(&mut command, &forced_shell, shell_args);
|
2020-04-11 16:37:24 +00:00
|
|
|
|
2020-05-27 07:38:05 +00:00
|
|
|
if let Ok(mut child) = command.spawn() {
|
2020-04-11 16:37:24 +00:00
|
|
|
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 = Command::new("cmd.exe")
|
|
|
|
.arg("/C")
|
|
|
|
.arg(cmd)
|
|
|
|
.stdin(Stdio::piped())
|
|
|
|
.stdout(Stdio::piped())
|
|
|
|
.stderr(Stdio::piped())
|
|
|
|
.spawn();
|
|
|
|
|
|
|
|
command.ok()?.wait_with_output().ok()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Execute the given command capturing all output, and return whether it return 0
|
2020-05-27 07:38:05 +00:00
|
|
|
fn exec_when(cmd: &str, shell_args: &[&str]) -> bool {
|
2020-04-11 16:37:24 +00:00
|
|
|
log::trace!("Running '{}'", cmd);
|
|
|
|
|
2020-05-27 07:38:05 +00:00
|
|
|
if let Some(output) = shell_command(cmd, shell_args) {
|
2020-04-11 16:37:24 +00:00
|
|
|
if !output.status.success() {
|
|
|
|
log::trace!("non-zero exit code '{:?}'", output.status.code());
|
|
|
|
log::trace!(
|
|
|
|
"stdout: {}",
|
|
|
|
std::str::from_utf8(&output.stdout).unwrap_or("<invalid utf8>")
|
|
|
|
);
|
|
|
|
log::trace!(
|
|
|
|
"stderr: {}",
|
|
|
|
std::str::from_utf8(&output.stderr).unwrap_or("<invalid utf8>")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
output.status.success()
|
|
|
|
} else {
|
|
|
|
log::debug!("Cannot start command");
|
|
|
|
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Execute the given command, returning its output on success
|
2020-05-27 07:38:05 +00:00
|
|
|
fn exec_command(cmd: &str, shell_args: &[&str]) -> Option<String> {
|
2020-04-11 16:37:24 +00:00
|
|
|
log::trace!("Running '{}'", cmd);
|
|
|
|
|
2020-05-27 07:38:05 +00:00
|
|
|
if let Some(output) = shell_command(cmd, shell_args) {
|
2020-04-11 16:37:24 +00:00
|
|
|
if !output.status.success() {
|
|
|
|
log::trace!("Non-zero exit code '{:?}'", output.status.code());
|
|
|
|
log::trace!(
|
|
|
|
"stdout: {}",
|
|
|
|
std::str::from_utf8(&output.stdout).unwrap_or("<invalid utf8>")
|
|
|
|
);
|
|
|
|
log::trace!(
|
|
|
|
"stderr: {}",
|
|
|
|
std::str::from_utf8(&output.stderr).unwrap_or("<invalid utf8>")
|
|
|
|
);
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(String::from_utf8_lossy(&output.stdout).into())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-27 07:38:05 +00:00
|
|
|
/// 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");
|
|
|
|
|
|
|
|
if is_powershell && shell_args.is_empty() {
|
|
|
|
command.arg("-NoProfile").arg("-Command").arg("-");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-11 16:37:24 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[cfg(not(windows))]
|
2020-05-27 07:38:05 +00:00
|
|
|
const SHELL: &[&str] = &["/bin/sh"];
|
2020-04-11 16:37:24 +00:00
|
|
|
#[cfg(windows)]
|
2020-05-27 07:38:05 +00:00
|
|
|
const SHELL: &[&str] = &[];
|
2020-04-11 16:37:24 +00:00
|
|
|
|
|
|
|
#[cfg(not(windows))]
|
|
|
|
const FAILING_COMMAND: &str = "false";
|
|
|
|
#[cfg(windows)]
|
|
|
|
const FAILING_COMMAND: &str = "color 00";
|
|
|
|
|
|
|
|
const UNKNOWN_COMMAND: &str = "ydelsyiedsieudleylse dyesdesl";
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn when_returns_right_value() {
|
|
|
|
assert!(exec_when("echo hello", SHELL));
|
|
|
|
assert!(!exec_when(FAILING_COMMAND, SHELL));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn when_returns_false_if_invalid_command() {
|
|
|
|
assert!(!exec_when(UNKNOWN_COMMAND, SHELL));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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())
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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())
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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())
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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())
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn command_can_fail() {
|
|
|
|
assert_eq!(exec_command(FAILING_COMMAND, SHELL), None);
|
|
|
|
assert_eq!(exec_command(UNKNOWN_COMMAND, SHELL), None);
|
|
|
|
}
|
|
|
|
}
|