Repository: casey/just
Pull request #1198 (2022-05-28 04:51:49)

Windows shell setting
To do:

- [x] Add documentation


Patch:
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<StringLiteral<'src>>,
-  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<bool>,
   pub(crate) export: bool,
   pub(crate) positional_arguments: bool,
-  pub(crate) shell: Option<setting::Shell<'src>>,
+  pub(crate) shell: Option<Shell<'src>>,
   pub(crate) windows_powershell: bool,
+  pub(crate) windows_shell: Option<Shell<'src>>,
 }
 
 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<StringLiteral<'src>>,
+  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, "]")
+  }
+}


Test patch:
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();
+}