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:
parent
5731d6674e
commit
09996159f4
|
@ -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
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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";
|
||||||
|
|
Loading…
Reference in New Issue