diff --git a/README.adoc b/README.adoc index ff290e4..dbeb3b5 100644 --- a/README.adoc +++ b/README.adoc @@ -559,6 +559,7 @@ foo: | `export` | boolean | Export all variables as environment variables. | `positional-arguments` | boolean | Pass positional arguments. | `shell` | `[COMMAND, ARGS...]` | Set the command used to invoke recipes and evaluate backticks. +| `windows-powershell` | boolean | Use PowerShell on Windows as default shell. |================= Boolean settings can be written as: @@ -637,7 +638,7 @@ $ just test foo "bar baz" - foo - bar baz ``` - + ==== Shell The `shell` setting controls the command used to invoke recipe lines and backticks. Shebang recipes are unaffected. @@ -656,6 +657,17 @@ 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 PowerShell + +`just` uses `sh` on Windows by default. To use PowerShell instead, set `windows-powershell` to true. + +```make +set windows-powershell := true + +hello: + Write-Host "Hello, world!" +``` + ===== Python 3 ```make diff --git a/src/analyzer.rs b/src/analyzer.rs index 1e78a62..c8bfe3d 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -76,6 +76,9 @@ impl<'src> Analyzer<'src> { assert!(settings.shell.is_none()); settings.shell = Some(shell); } + Setting::WindowsPowerShell(windows_powershell) => { + settings.windows_powershell = windows_powershell; + } } } diff --git a/src/config.rs b/src/config.rs index 0587916..50104b1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,9 +9,6 @@ pub(crate) const CHOOSE_HELP: &str = "Select one or more recipes to run using a `--chooser` is not passed the chooser defaults to the value \ of $JUST_CHOOSER, falling back to `fzf`"; -pub(crate) const DEFAULT_SHELL: &str = "sh"; -pub(crate) const DEFAULT_SHELL_ARG: &str = "-cu"; - #[derive(Debug, PartialEq)] pub(crate) struct Config { pub(crate) check: bool, @@ -26,10 +23,9 @@ pub(crate) struct Config { pub(crate) list_prefix: String, pub(crate) load_dotenv: bool, pub(crate) search_config: SearchConfig, - pub(crate) shell: String, - pub(crate) shell_args: Vec, + pub(crate) shell: Option, + pub(crate) shell_args: Option>, pub(crate) shell_command: bool, - pub(crate) shell_present: bool, pub(crate) subcommand: Subcommand, pub(crate) unsorted: bool, pub(crate) unstable: bool, @@ -217,7 +213,6 @@ impl Config { Arg::with_name(arg::SHELL) .long("shell") .takes_value(true) - .default_value(DEFAULT_SHELL) .help("Invoke to run recipes"), ) .arg( @@ -226,7 +221,6 @@ impl Config { .takes_value(true) .multiple(true) .number_of_values(1) - .default_value(DEFAULT_SHELL_ARG) .allow_hyphen_values(true) .overrides_with(arg::CLEAR_SHELL_ARGS) .help("Invoke shell with as an argument"), @@ -558,26 +552,26 @@ impl Config { } }; - let shell_args = if matches.is_present(arg::CLEAR_SHELL_ARGS) { - Vec::new() + let shell_args = if matches.occurrences_of(arg::SHELL_ARG) > 0 + || matches.occurrences_of(arg::CLEAR_SHELL_ARGS) > 0 + { + Some( + matches + .values_of(arg::SHELL_ARG) + .map_or(Vec::new(), |shell_args| { + shell_args.map(str::to_owned).collect() + }), + ) } else { - matches - .values_of(arg::SHELL_ARG) - .unwrap() - .map(str::to_owned) - .collect() + None }; - let shell_present = matches.occurrences_of(arg::CLEAR_SHELL_ARGS) > 0 - || matches.occurrences_of(arg::SHELL) > 0 - || matches.occurrences_of(arg::SHELL_ARG) > 0; - Ok(Self { check: matches.is_present(arg::CHECK), dry_run: matches.is_present(arg::DRY_RUN), dump_format: Self::dump_format_from_matches(matches)?, highlight: !matches.is_present(arg::NO_HIGHLIGHT), - shell: matches.value_of(arg::SHELL).unwrap().to_owned(), + shell: matches.value_of(arg::SHELL).map(str::to_owned), load_dotenv: !matches.is_present(arg::NO_DOTENV), shell_command: matches.is_present(arg::SHELL_COMMAND), unsorted: matches.is_present(arg::UNSORTED), @@ -594,7 +588,6 @@ impl Config { invocation_directory, search_config, shell_args, - shell_present, subcommand, dotenv_filename: matches.value_of(arg::DOTENV_FILENAME).map(str::to_owned), dotenv_path: matches.value_of(arg::DOTENV_PATH).map(PathBuf::from), @@ -697,11 +690,9 @@ OPTIONS: --set Override with - --shell - Invoke to run recipes [default: sh] - + --shell Invoke to run recipes --shell-arg ... - Invoke shell with as an argument [default: -cu] + Invoke shell with as an argument -s, --show Show information about @@ -735,7 +726,6 @@ ARGS: $(search_config: $search_config:expr,)? $(shell: $shell:expr,)? $(shell_args: $shell_args:expr,)? - $(shell_present: $shell_present:expr,)? $(subcommand: $subcommand:expr,)? $(unsorted: $unsorted:expr,)? $(verbosity: $verbosity:expr,)? @@ -753,9 +743,8 @@ ARGS: $(dump_format: $dump_format,)? $(highlight: $highlight,)? $(search_config: $search_config,)? - $(shell: $shell.to_owned(),)? + $(shell: $shell,)? $(shell_args: $shell_args,)? - $(shell_present: $shell_present,)? $(subcommand: $subcommand,)? $(unsorted: $unsorted,)? $(verbosity: $verbosity,)? @@ -1016,16 +1005,21 @@ ARGS: test! { name: shell_default, args: [], - shell: "sh", - shell_args: vec!["-cu".to_owned()], - shell_present: false, + shell: None, + shell_args: None, } test! { name: shell_set, args: ["--shell", "tclsh"], - shell: "tclsh", - shell_present: true, + shell: Some("tclsh".to_owned()), + } + + test! { + name: shell_args_set, + args: ["--shell-arg", "hello"], + shell: None, + shell_args: Some(vec!["hello".into()]), } test! { @@ -1262,56 +1256,53 @@ ARGS: test! { name: shell_args_default, args: [], - shell_args: vec!["-cu".to_owned()], } test! { name: shell_args_set_hyphen, args: ["--shell-arg", "--foo"], - shell_args: vec!["--foo".to_owned()], - shell_present: true, + shell_args: Some(vec!["--foo".to_owned()]), } test! { name: shell_args_set_word, args: ["--shell-arg", "foo"], - shell_args: vec!["foo".to_owned()], - shell_present: true, + shell_args: Some(vec!["foo".to_owned()]), } test! { name: shell_args_set_multiple, args: ["--shell-arg", "foo", "--shell-arg", "bar"], - shell_args: vec!["foo".to_owned(), "bar".to_owned()], - shell_present: true, + shell_args: Some(vec!["foo".to_owned(), "bar".to_owned()]), + } test! { name: shell_args_clear, args: ["--clear-shell-args"], - shell_args: vec![], - shell_present: true, + shell_args: Some(vec![]), + } test! { name: shell_args_clear_and_set, args: ["--clear-shell-args", "--shell-arg", "bar"], - shell_args: vec!["bar".to_owned()], - shell_present: true, + shell_args: Some(vec!["bar".to_owned()]), + } test! { name: shell_args_set_and_clear, args: ["--shell-arg", "bar", "--clear-shell-args"], - shell_args: vec![], - shell_present: true, + shell_args: Some(vec![]), + } test! { name: shell_args_set_multiple_and_clear, args: ["--shell-arg", "bar", "--shell-arg", "baz", "--clear-shell-args"], - shell_args: vec![], - shell_present: true, + shell_args: Some(vec![]), + } test! { diff --git a/src/keyword.rs b/src/keyword.rs index c7c97bf..2ed06d8 100644 --- a/src/keyword.rs +++ b/src/keyword.rs @@ -13,6 +13,7 @@ pub(crate) enum Keyword { Set, Shell, True, + WindowsPowershell, } impl Keyword { diff --git a/src/node.rs b/src/node.rs index 15df6c7..c3b0f9b 100644 --- a/src/node.rs +++ b/src/node.rs @@ -221,7 +221,7 @@ impl<'src> Node<'src> for Set<'src> { set.push_mut(self.name.lexeme().replace('-', "_")); match &self.value { - DotenvLoad(value) | Export(value) | PositionalArguments(value) => { + DotenvLoad(value) | Export(value) | PositionalArguments(value) | WindowsPowerShell(value) => { set.push_mut(value.to_string()); } Shell(setting::Shell { command, arguments }) => { diff --git a/src/parser.rs b/src/parser.rs index 1dc064a..7c50cda 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -747,6 +747,12 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { value: Setting::PositionalArguments(value), name, }); + } else if Keyword::WindowsPowershell == lexeme { + let value = self.parse_set_bool()?; + return Ok(Set { + value: Setting::WindowsPowerShell(value), + name, + }); } self.expect(ColonEquals)?; @@ -1774,6 +1780,24 @@ mod tests { tree: (justfile (set shell "bash" "-cu" "-l")), } + test! { + name: set_windows_powershell_implicit, + text: "set windows-powershell", + tree: (justfile (set windows_powershell true)), + } + + test! { + name: set_windows_powershell_true, + text: "set windows-powershell := true", + tree: (justfile (set windows_powershell true)), + } + + test! { + name: set_windows_powershell_false, + text: "set windows-powershell := false", + tree: (justfile (set windows_powershell false)), + } + test! { name: conditional, text: "a := if b == c { d } else { e }", diff --git a/src/setting.rs b/src/setting.rs index 71dfb36..39c9eca 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -6,6 +6,7 @@ pub(crate) enum Setting<'src> { Export(bool), PositionalArguments(bool), Shell(Shell<'src>), + WindowsPowerShell(bool), } #[derive(Debug, Clone, PartialEq, Serialize)] @@ -17,9 +18,10 @@ pub(crate) struct Shell<'src> { impl<'src> Display for Setting<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { match self { - Setting::DotenvLoad(value) | Setting::Export(value) | Setting::PositionalArguments(value) => { - write!(f, "{}", value) - } + Setting::DotenvLoad(value) + | Setting::Export(value) + | Setting::PositionalArguments(value) + | Setting::WindowsPowerShell(value) => write!(f, "{}", value), Setting::Shell(shell) => write!(f, "{}", shell), } } diff --git a/src/settings.rs b/src/settings.rs index 81d09c7..8078ebc 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,11 +1,17 @@ use crate::common::*; +pub(crate) const DEFAULT_SHELL: &str = "sh"; +pub(crate) const DEFAULT_SHELL_ARGS: &[&str] = &["-cu"]; +pub(crate) const WINDOWS_POWERSHELL_SHELL: &str = "powershell.exe"; +pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"]; + #[derive(Debug, PartialEq, Serialize)] 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) windows_powershell: bool, } impl<'src> Settings<'src> { @@ -15,6 +21,7 @@ impl<'src> Settings<'src> { export: false, positional_arguments: false, shell: None, + windows_powershell: false, } } @@ -27,22 +34,166 @@ impl<'src> Settings<'src> { } pub(crate) fn shell_binary<'a>(&'a self, config: &'a Config) -> &'a str { - if let (Some(shell), false) = (&self.shell, config.shell_present) { + let shell_or_args_present = config.shell.is_some() || config.shell_args.is_some(); + + if let (Some(shell), false) = (&self.shell, shell_or_args_present) { shell.command.cooked.as_ref() + } else if let Some(shell) = &config.shell { + shell + } else if cfg!(windows) && self.windows_powershell { + WINDOWS_POWERSHELL_SHELL } else { - &config.shell + DEFAULT_SHELL } } pub(crate) fn shell_arguments<'a>(&'a self, config: &'a Config) -> Vec<&'a str> { - if let (Some(shell), false) = (&self.shell, config.shell_present) { + let shell_or_args_present = config.shell.is_some() || config.shell_args.is_some(); + + if let (Some(shell), false) = (&self.shell, shell_or_args_present) { shell .arguments .iter() .map(|argument| argument.cooked.as_ref()) .collect() + } else if let Some(shell_args) = &config.shell_args { + shell_args.iter().map(String::as_ref).collect() + } else if cfg!(windows) && self.windows_powershell { + WINDOWS_POWERSHELL_ARGS.to_vec() } else { - config.shell_args.iter().map(String::as_ref).collect() + DEFAULT_SHELL_ARGS.to_vec() + } + } +} + +#[cfg(test)] +mod tests { + use crate::setting::Shell; + + use super::*; + + #[test] + fn default_shell() { + let settings = Settings::new(); + + let config = Config { + shell_command: false, + ..testing::config(&[]) + }; + + assert_eq!(settings.shell_binary(&config), "sh"); + assert_eq!(settings.shell_arguments(&config), vec!["-cu"]); + } + + #[test] + fn default_shell_powershell() { + let mut settings = Settings::new(); + settings.windows_powershell = true; + + let config = Config { + shell_command: false, + ..testing::config(&[]) + }; + + if cfg!(windows) { + assert_eq!(settings.shell_binary(&config), "powershell.exe"); + assert_eq!( + settings.shell_arguments(&config), + vec!["-NoLogo", "-Command"] + ); + } else { + assert_eq!(settings.shell_binary(&config), "sh"); + assert_eq!(settings.shell_arguments(&config), vec!["-cu"]); + } + } + + #[test] + fn overwrite_shell() { + let settings = Settings::new(); + + let config = Config { + shell_command: true, + shell: Some("lol".to_string()), + shell_args: Some(vec!["-nice".to_string()]), + ..testing::config(&[]) + }; + + assert_eq!(settings.shell_binary(&config), "lol"); + assert_eq!(settings.shell_arguments(&config), vec!["-nice"]); + } + + #[test] + fn overwrite_shell_powershell() { + let mut settings = Settings::new(); + settings.windows_powershell = true; + + let config = Config { + shell_command: true, + shell: Some("lol".to_string()), + shell_args: Some(vec!["-nice".to_string()]), + ..testing::config(&[]) + }; + + assert_eq!(settings.shell_binary(&config), "lol"); + assert_eq!(settings.shell_arguments(&config), vec!["-nice"]); + } + + #[test] + fn shell_cooked() { + let mut settings = Settings::new(); + + settings.shell = Some(Shell { + command: StringLiteral { + kind: StringKind::from_token_start("\"").unwrap(), + raw: "asdf.exe", + cooked: "asdf.exe".to_string(), + }, + arguments: vec![StringLiteral { + kind: StringKind::from_token_start("\"").unwrap(), + raw: "-nope", + cooked: "-nope".to_string(), + }], + }); + + let config = Config { + shell_command: false, + ..testing::config(&[]) + }; + + assert_eq!(settings.shell_binary(&config), "asdf.exe"); + assert_eq!(settings.shell_arguments(&config), vec!["-nope"]); + } + + #[test] + fn shell_present_but_not_shell_args() { + let mut settings = Settings::new(); + settings.windows_powershell = true; + + let config = Config { + shell: Some("lol".to_string()), + ..testing::config(&[]) + }; + + assert_eq!(settings.shell_binary(&config), "lol"); + } + + #[test] + fn shell_args_present_but_not_shell() { + let mut settings = Settings::new(); + settings.windows_powershell = true; + + let config = Config { + shell_command: false, + shell_args: Some(vec!["-nice".to_string()]), + ..testing::config(&[]) + }; + + if cfg!(windows) { + assert_eq!(settings.shell_binary(&config), "powershell.exe"); + assert_eq!(settings.shell_arguments(&config), vec!["-nice"]); + } else { + assert_eq!(settings.shell_binary(&config), "sh"); + assert_eq!(settings.shell_arguments(&config), vec!["-nice"]); } } } diff --git a/tests/command.rs b/tests/command.rs index d139b5f..8fcb92f 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -31,7 +31,7 @@ test! { error: The argument '--command ' requires a value but none was supplied USAGE: - just{} --color --dump-format --shell --shell-arg ... \ + just{} --color --dump-format --shell \ <--changelog|--choose|--command |--completions |--dump|--edit|\ --evaluate|--fmt|--init|--list|--show |--summary|--variables> diff --git a/tests/json.rs b/tests/json.rs index 512623c..2604f6f 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -43,6 +43,7 @@ fn alias() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -69,6 +70,7 @@ fn assignment() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -108,6 +110,7 @@ fn body() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -157,6 +160,7 @@ fn dependencies() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -243,6 +247,7 @@ fn dependency_argument() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -275,6 +280,7 @@ fn doc_comment() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -295,6 +301,7 @@ fn empty_justfile() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -424,6 +431,7 @@ fn parameters() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -492,6 +500,7 @@ fn priors() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -524,6 +533,7 @@ fn private() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -556,6 +566,7 @@ fn quiet() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -609,6 +620,7 @@ fn settings() { "arguments": ["b", "c"], "command": "a", }, + "windows_powershell": false, }, "warnings": [], }), @@ -644,6 +656,7 @@ fn shebang() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), @@ -676,6 +689,7 @@ fn simple() { "export": false, "positional_arguments": false, "shell": null, + "windows_powershell": false, }, "warnings": [], }), diff --git a/tests/lib.rs b/tests/lib.rs index 72e1e49..7492e41 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -40,4 +40,6 @@ mod sublime_syntax; mod subsequents; mod tempdir; mod undefined_variables; +#[cfg(target_family = "windows")] +mod windows_powershell; mod working_directory; diff --git a/tests/windows_powershell.rs b/tests/windows_powershell.rs new file mode 100644 index 0000000..79ea0b3 --- /dev/null +++ b/tests/windows_powershell.rs @@ -0,0 +1,18 @@ +use crate::common::*; + +#[test] +fn windows_poweshell_setting_uses_powershell() { + Test::new() + .justfile( + r#" + set windows-powershell + + foo: + Write-Output bar + "#, + ) + .shell(false) + .stdout("bar\r\n") + .stderr("Write-Output bar\n") + .run(); +}