diff --git a/README.md b/README.md index 46ce6070..68460dcf 100644 --- a/README.md +++ b/README.md @@ -671,9 +671,22 @@ foo: `just` passes the command to be executed as an argument. Many shells will need an additional flag, often `-c`, to make them evaluate the first argument. +##### Windows Shell + +`just` uses `sh` on Windows by default. To use a different shell on Windows, use `windows-shell`: + +```make +set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"] + +hello: + Write-Host "Hello, world!" +``` + ##### Windows PowerShell -`just` uses `sh` on Windows by default. To use PowerShell instead, set `windows-powershell` to true. +*`set windows-powershell` uses the legacy `powershell.exe` binary, and is no longer recommended. See the `windows-shell` setting above for a more flexible way to control which shell is used on Windows.* + +`just` uses `sh` on Windows by default. To use `powershell.exe` instead, set `windows-powershell` to true. ```make set windows-powershell := true diff --git a/src/analyzer.rs b/src/analyzer.rs index 4006dafb..1f6d6576 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -57,12 +57,14 @@ impl<'src> Analyzer<'src> { settings.positional_arguments = positional_arguments; } Setting::Shell(shell) => { - assert!(settings.shell.is_none()); settings.shell = Some(shell); } Setting::WindowsPowerShell(windows_powershell) => { settings.windows_powershell = windows_powershell; } + Setting::WindowsShell(windows_shell) => { + settings.windows_shell = Some(windows_shell); + } } } diff --git a/src/common.rs b/src/common.rs index 339e7e2f..678d759f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -38,7 +38,7 @@ pub(crate) use ::{ }; // modules -pub(crate) use crate::{completions, config, config_error, keyed, setting}; +pub(crate) use crate::{completions, config, config_error, keyed}; // functions pub(crate) use crate::{load_dotenv::load_dotenv, output::output, unindent::unindent}; @@ -64,11 +64,11 @@ pub(crate) use crate::{ parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, - settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, string_kind::StringKind, - string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, - thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, - unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, - verbosity::Verbosity, warning::Warning, + settings::Settings, shebang::Shebang, shell::Shell, show_whitespace::ShowWhitespace, + string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand, + suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind, + unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe, + use_color::UseColor, variables::Variables, verbosity::Verbosity, warning::Warning, }; // type aliases diff --git a/src/keyword.rs b/src/keyword.rs index 0f3f6fc7..11fdca1b 100644 --- a/src/keyword.rs +++ b/src/keyword.rs @@ -15,6 +15,7 @@ pub(crate) enum Keyword { Shell, True, WindowsPowershell, + WindowsShell, } impl Keyword { diff --git a/src/lib.rs b/src/lib.rs index c82975ea..c758c5de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,6 +106,7 @@ mod set; mod setting; mod settings; mod shebang; +mod shell; mod show_whitespace; mod string_kind; mod string_literal; diff --git a/src/node.rs b/src/node.rs index 58c5cf15..539c355c 100644 --- a/src/node.rs +++ b/src/node.rs @@ -215,20 +215,19 @@ impl<'src> Node<'src> for Fragment<'src> { impl<'src> Node<'src> for Set<'src> { fn tree(&self) -> Tree<'src> { - use Setting::*; - let mut set = Tree::atom(Keyword::Set.lexeme()); set.push_mut(self.name.lexeme().replace('-', "_")); match &self.value { - AllowDuplicateRecipes(value) - | DotenvLoad(value) - | Export(value) - | PositionalArguments(value) - | WindowsPowerShell(value) => { + Setting::AllowDuplicateRecipes(value) + | Setting::DotenvLoad(value) + | Setting::Export(value) + | Setting::PositionalArguments(value) + | Setting::WindowsPowerShell(value) => { set.push_mut(value.to_string()); } - Shell(setting::Shell { command, arguments }) => { + Setting::Shell(Shell { command, arguments }) + | Setting::WindowsShell(Shell { command, arguments }) => { set.push_mut(Tree::string(&command.cooked)); for argument in arguments { set.push_mut(Tree::string(&argument.cooked)); diff --git a/src/parser.rs b/src/parser.rs index a8c5e8da..9e94828c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -764,26 +764,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { self.expect(ColonEquals)?; if name.lexeme() == Keyword::Shell.lexeme() { - self.expect(BracketL)?; - - let command = self.parse_string_literal()?; - - let mut arguments = Vec::new(); - - if self.accepted(Comma)? { - while !self.next_is(BracketR) { - arguments.push(self.parse_string_literal()?); - - if !self.accepted(Comma)? { - break; - } - } - } - - self.expect(BracketR)?; - Ok(Set { - value: Setting::Shell(setting::Shell { arguments, command }), + value: Setting::Shell(self.parse_shell()?), + name, + }) + } else if name.lexeme() == Keyword::WindowsShell.lexeme() { + Ok(Set { + value: Setting::WindowsShell(self.parse_shell()?), name, }) } else { @@ -792,6 +779,29 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { })) } } + + /// Parse a shell setting value + fn parse_shell(&mut self) -> CompileResult<'src, Shell<'src>> { + self.expect(BracketL)?; + + let command = self.parse_string_literal()?; + + let mut arguments = Vec::new(); + + if self.accepted(Comma)? { + while !self.next_is(BracketR) { + arguments.push(self.parse_string_literal()?); + + if !self.accepted(Comma)? { + break; + } + } + } + + self.expect(BracketR)?; + + Ok(Shell { arguments, command }) + } } #[cfg(test)] diff --git a/src/setting.rs b/src/setting.rs index 220ce9dc..3646347f 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -8,12 +8,7 @@ pub(crate) enum Setting<'src> { PositionalArguments(bool), Shell(Shell<'src>), WindowsPowerShell(bool), -} - -#[derive(Debug, Clone, PartialEq, Serialize)] -pub(crate) struct Shell<'src> { - pub(crate) arguments: Vec>, - pub(crate) command: StringLiteral<'src>, + WindowsShell(Shell<'src>), } impl<'src> Display for Setting<'src> { @@ -24,19 +19,7 @@ impl<'src> Display for Setting<'src> { | Setting::Export(value) | Setting::PositionalArguments(value) | Setting::WindowsPowerShell(value) => write!(f, "{}", value), - Setting::Shell(shell) => write!(f, "{}", shell), + Setting::Shell(shell) | Setting::WindowsShell(shell) => write!(f, "{}", shell), } } } - -impl<'src> Display for Shell<'src> { - fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { - write!(f, "[{}", self.command)?; - - for argument in &self.arguments { - write!(f, ", {}", argument)?; - } - - write!(f, "]") - } -} diff --git a/src/settings.rs b/src/settings.rs index ed5432e4..b6b74079 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -11,8 +11,9 @@ pub(crate) struct Settings<'src> { pub(crate) dotenv_load: Option, pub(crate) export: bool, pub(crate) positional_arguments: bool, - pub(crate) shell: Option>, + pub(crate) shell: Option>, pub(crate) windows_powershell: bool, + pub(crate) windows_shell: Option>, } impl<'src> Settings<'src> { @@ -24,6 +25,7 @@ impl<'src> Settings<'src> { positional_arguments: false, shell: None, windows_powershell: false, + windows_shell: None, } } @@ -42,6 +44,8 @@ impl<'src> Settings<'src> { shell.command.cooked.as_ref() } else if let Some(shell) = &config.shell { shell + } else if let (true, Some(shell)) = (cfg!(windows), &self.windows_shell) { + shell.command.cooked.as_ref() } else if cfg!(windows) && self.windows_powershell { WINDOWS_POWERSHELL_SHELL } else { @@ -60,6 +64,12 @@ impl<'src> Settings<'src> { .collect() } else if let Some(shell_args) = &config.shell_args { shell_args.iter().map(String::as_ref).collect() + } else if let (true, Some(shell)) = (cfg!(windows), &self.windows_shell) { + shell + .arguments + .iter() + .map(|argument| argument.cooked.as_ref()) + .collect() } else if cfg!(windows) && self.windows_powershell { WINDOWS_POWERSHELL_ARGS.to_vec() } else { @@ -70,8 +80,6 @@ impl<'src> Settings<'src> { #[cfg(test)] mod tests { - use crate::setting::Shell; - use super::*; #[test] diff --git a/src/shell.rs b/src/shell.rs new file mode 100644 index 00000000..cc463c2f --- /dev/null +++ b/src/shell.rs @@ -0,0 +1,19 @@ +use crate::common::*; + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub(crate) struct Shell<'src> { + pub(crate) arguments: Vec>, + pub(crate) command: StringLiteral<'src>, +} + +impl<'src> Display for Shell<'src> { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + write!(f, "[{}", self.command)?; + + for argument in &self.arguments { + write!(f, ", {}", argument)?; + } + + write!(f, "]") + } +} diff --git a/tests/json.rs b/tests/json.rs index 345ca2ae..308b1ccd 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -45,6 +45,7 @@ fn alias() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -73,6 +74,7 @@ fn assignment() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -114,6 +116,7 @@ fn body() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -165,6 +168,7 @@ fn dependencies() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -253,6 +257,7 @@ fn dependency_argument() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -305,6 +310,7 @@ fn duplicate_recipes() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -339,6 +345,7 @@ fn doc_comment() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -361,6 +368,7 @@ fn empty_justfile() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -492,6 +500,7 @@ fn parameters() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -562,6 +571,7 @@ fn priors() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -596,6 +606,7 @@ fn private() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -630,6 +641,7 @@ fn quiet() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -685,6 +697,7 @@ fn settings() { "command": "a", }, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -722,6 +735,7 @@ fn shebang() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), @@ -756,6 +770,7 @@ fn simple() { "positional_arguments": false, "shell": null, "windows_powershell": false, + "windows_shell": null, }, "warnings": [], }), diff --git a/tests/lib.rs b/tests/lib.rs index 04ca4398..4f2f4248 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -45,4 +45,6 @@ mod tempdir; mod undefined_variables; #[cfg(target_family = "windows")] mod windows_powershell; +#[cfg(target_family = "windows")] +mod windows_shell; mod working_directory; diff --git a/tests/windows_shell.rs b/tests/windows_shell.rs new file mode 100644 index 00000000..db0a93a2 --- /dev/null +++ b/tests/windows_shell.rs @@ -0,0 +1,18 @@ +use crate::common::*; + +#[test] +fn windows_shell_setting() { + Test::new() + .justfile( + r#" + set windows-shell := ["pwsh.exe", "-NoLogo", "-Command"] + + foo: + Write-Output bar + "#, + ) + .shell(false) + .stdout("bar\r\n") + .stderr("Write-Output bar\n") + .run(); +}