fix(custom): improve handling of Powershell prompts (#1237)

To improve overall support of PowerShell in custom modules,
the ability to pass arguments to the shell was also added. Additionally,
the arguments `-NoProfile -Command -` will be automatically passed
to PowerShell, unless other arguments are given by the user.
This commit is contained in:
Grégoire Geis 2020-05-27 09:38:05 +02:00 committed by GitHub
parent 5731d6674e
commit 09996159f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 31 deletions

View File

@ -1420,7 +1420,7 @@ will simply show all custom modules in the order they were defined.
| ------------- | ------------------- | ---------------------------------------------------------------------------- | | ------------- | ------------------- | ---------------------------------------------------------------------------- |
| `command` | | The command whose output should be printed. | | `command` | | The command whose output should be printed. |
| `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` | | 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` | | The path to the shell to use to execute the command. If unset, it will fallback to STARSHIP_SHELL and then to "sh". | | `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. | | `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. | | `directories` | `[]` | The directories that will be searched in the working directory for a match. |
@ -1431,6 +1431,22 @@ will simply show all custom modules in the order they were defined.
| `suffix` | `""` | Suffix to display immediately after the command output. | | `suffix` | `""` | Suffix to display immediately after the command output. |
| `disabled` | `false` | Disables this `custom` module. | | `disabled` | `false` | Disables this `custom` module. |
#### Custom command shell
`shell` accepts a non-empty list of strings, where:
- The first string is the path to the shell to use to execute the command.
- Other following arguments are passed to the shell.
If unset, it will fallback to STARSHIP_SHELL and then to "sh" on Linux, and "cmd /C" on Windows.
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 -`.
This behavior can be avoided by explicitly passing arguments to the shell, e.g.
```toml
shell = ["pwsh", "-Command", "-"]
```
### Example ### Example
```toml ```toml

View File

@ -153,6 +153,30 @@ where
} }
} }
/// A wrapper around `Vec<T>` that implements `ModuleConfig`, and either
/// accepts a value of type `T` or a list of values of type `T`.
#[derive(Clone, Default)]
pub struct VecOr<T>(pub Vec<T>);
impl<'a, T> ModuleConfig<'a> for VecOr<T>
where
T: ModuleConfig<'a> + Sized,
{
fn from_config(config: &'a Value) -> Option<Self> {
if let Some(item) = T::from_config(config) {
return Some(VecOr(vec![item]));
}
let vec = config
.as_array()?
.iter()
.map(|value| T::from_config(value))
.collect::<Option<Vec<T>>>()?;
Some(VecOr(vec))
}
}
/// Root config of starship. /// Root config of starship.
pub struct StarshipConfig { pub struct StarshipConfig {
pub config: Option<Value>, pub config: Option<Value>,

View File

@ -1,4 +1,4 @@
use crate::config::{ModuleConfig, RootModuleConfig, SegmentConfig}; use crate::config::{ModuleConfig, RootModuleConfig, SegmentConfig, VecOr};
use ansi_term::Style; use ansi_term::Style;
use starship_module_config_derive::ModuleConfig; use starship_module_config_derive::ModuleConfig;
@ -17,7 +17,7 @@ pub struct CustomConfig<'a> {
pub symbol: Option<SegmentConfig<'a>>, pub symbol: Option<SegmentConfig<'a>>,
pub command: &'a str, pub command: &'a str,
pub when: Option<&'a str>, pub when: Option<&'a str>,
pub shell: Option<&'a str>, pub shell: VecOr<&'a str>,
pub description: &'a str, pub description: &'a str,
pub style: Option<Style>, pub style: Option<Style>,
pub disabled: bool, pub disabled: bool,
@ -34,7 +34,7 @@ impl<'a> RootModuleConfig<'a> for CustomConfig<'a> {
symbol: None, symbol: None,
command: "", command: "",
when: None, when: None,
shell: None, shell: VecOr::default(),
description: "<custom config>", description: "<custom config>",
style: None, style: None,
disabled: false, disabled: false,

View File

@ -35,7 +35,7 @@ pub fn module<'a>(name: &'a str, context: &'a Context) -> Option<Module<'a>> {
if !is_match { if !is_match {
if let Some(when) = config.when { if let Some(when) = config.when {
is_match = exec_when(when, config.shell); is_match = exec_when(when, &config.shell.0);
} }
if !is_match { if !is_match {
@ -57,7 +57,7 @@ pub fn module<'a>(name: &'a str, context: &'a Context) -> Option<Module<'a>> {
module.create_segment("symbol", &symbol); module.create_segment("symbol", &symbol);
} }
if let Some(output) = exec_command(config.command, config.shell) { if let Some(output) = exec_command(config.command, &config.shell.0) {
let trimmed = output.trim(); let trimmed = output.trim();
if trimmed.is_empty() { if trimmed.is_empty() {
@ -77,26 +77,31 @@ pub fn module<'a>(name: &'a str, context: &'a Context) -> Option<Module<'a>> {
/// 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"
#[cfg(not(windows))] #[cfg(not(windows))]
fn get_shell(shell: Option<&str>) -> std::borrow::Cow<str> { fn get_shell<'a, 'b>(shell_args: &'b [&'a str]) -> (std::borrow::Cow<'a, str>, &'b [&'a str]) {
if let Some(forced_shell) = shell { if !shell_args.is_empty() {
forced_shell.into() (shell_args[0].into(), &shell_args[1..])
} else if let Ok(env_shell) = std::env::var("STARSHIP_SHELL") { } else if let Ok(env_shell) = std::env::var("STARSHIP_SHELL") {
env_shell.into() (env_shell.into(), &[] as &[&str])
} else { } else {
"sh".into() ("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 `stdin` to `get_shell()`
#[cfg(not(windows))] #[cfg(not(windows))]
fn shell_command(cmd: &str, shell: Option<&str>) -> Option<Output> { fn shell_command(cmd: &str, shell_args: &[&str]) -> Option<Output> {
let command = Command::new(get_shell(shell).as_ref()) let (shell, shell_args) = get_shell(shell_args);
let mut command = Command::new(shell.as_ref());
command
.args(shell_args)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped());
.spawn();
let mut child = match command { handle_powershell(&mut command, &shell, shell_args);
let mut child = match command.spawn() {
Ok(command) => command, Ok(command) => command,
Err(_) => { Err(_) => {
log::debug!( log::debug!(
@ -120,23 +125,30 @@ fn shell_command(cmd: &str, shell: Option<&str>) -> Option<Output> {
/// 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 `stdin` to `get_shell()`,
/// or by invoking cmd.exe /C. /// or by invoking cmd.exe /C.
#[cfg(windows)] #[cfg(windows)]
fn shell_command(cmd: &str, shell: Option<&str>) -> Option<Output> { fn shell_command(cmd: &str, shell_args: &[&str]) -> Option<Output> {
let shell = if let Some(shell) = shell { let (shell, shell_args) = if !shell_args.is_empty() {
Some(std::borrow::Cow::Borrowed(shell)) (
Some(std::borrow::Cow::Borrowed(shell_args[0])),
&shell_args[1..],
)
} else if let Ok(env_shell) = std::env::var("STARSHIP_SHELL") { } else if let Ok(env_shell) = std::env::var("STARSHIP_SHELL") {
Some(std::borrow::Cow::Owned(env_shell)) (Some(std::borrow::Cow::Owned(env_shell)), &[] as &[&str])
} else { } else {
None (None, &[] as &[&str])
}; };
if let Some(forced_shell) = shell { if let Some(forced_shell) = shell {
let command = Command::new(forced_shell.as_ref()) let mut command = Command::new(forced_shell.as_ref());
command
.args(shell_args)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped());
.spawn();
if let Ok(mut child) = command { 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()?; child.stdin.as_mut()?.write_all(cmd.as_bytes()).ok()?;
return child.wait_with_output().ok(); return child.wait_with_output().ok();
@ -159,10 +171,10 @@ fn shell_command(cmd: &str, shell: Option<&str>) -> Option<Output> {
} }
/// 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: Option<&str>) -> bool { fn exec_when(cmd: &str, shell_args: &[&str]) -> bool {
log::trace!("Running '{}'", cmd); log::trace!("Running '{}'", cmd);
if let Some(output) = shell_command(cmd, shell) { if let Some(output) = shell_command(cmd, shell_args) {
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!(
@ -184,10 +196,10 @@ fn exec_when(cmd: &str, shell: Option<&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: Option<&str>) -> Option<String> { fn exec_command(cmd: &str, shell_args: &[&str]) -> Option<String> {
log::trace!("Running '{}'", cmd); log::trace!("Running '{}'", cmd);
if let Some(output) = shell_command(cmd, shell) { if let Some(output) = shell_command(cmd, shell_args) {
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!(
@ -207,14 +219,27 @@ fn exec_command(cmd: &str, shell: Option<&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");
if is_powershell && shell_args.is_empty() {
command.arg("-NoProfile").arg("-Command").arg("-");
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[cfg(not(windows))] #[cfg(not(windows))]
const SHELL: Option<&'static str> = Some("/bin/sh"); const SHELL: &[&str] = &["/bin/sh"];
#[cfg(windows)] #[cfg(windows)]
const SHELL: Option<&'static str> = None; const SHELL: &[&str] = &[];
#[cfg(not(windows))] #[cfg(not(windows))]
const FAILING_COMMAND: &str = "false"; const FAILING_COMMAND: &str = "false";