From e80bf34d9ada793650e1c8feeade02fa1027e811 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 10 Nov 2019 23:17:47 -0800 Subject: [PATCH] Add `shell` setting (#525) Add a `set SETTING := VALUE` construct. This construct is intended to be extended as needed with new settings, but for now we're starting with `set shell := [COMMAND, ARG1, ...]`, which allows setting the shell to use for recipe and backtick execution in a justfile. One of the primary reasons for adding this feature is to have a better story on windows, where users are forced to scrounge up an `sh` binary if they want to use `just`. This should allow them to use cmd.exe or powershell in their justfiles, making just optionally dependency-free. --- src/analyzer.rs | 58 ++++++++--- src/assignment_evaluator.rs | 9 +- src/common.rs | 11 +- src/compilation_error.rs | 12 +++ src/compilation_error_kind.rs | 7 ++ src/item.rs | 1 + src/justfile.rs | 41 ++++---- src/keyword.rs | 3 + src/lexer.rs | 10 ++ src/lib.rs | 3 + src/node.rs | 21 ++++ src/parser.rs | 189 +++++++++++++++++++++++++++++++++- src/recipe.rs | 7 +- src/recipe_context.rs | 1 + src/set.rs | 13 +++ src/setting.rs | 12 +++ src/settings.rs | 30 ++++++ src/token_kind.rs | 4 + tests/integration.rs | 16 +++ tests/shell.rs | 68 +++++++++++- 20 files changed, 468 insertions(+), 48 deletions(-) create mode 100644 src/set.rs create mode 100644 src/setting.rs create mode 100644 src/settings.rs diff --git a/src/analyzer.rs b/src/analyzer.rs index 8812751..32ab2cc 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -2,28 +2,33 @@ use crate::common::*; use CompilationErrorKind::*; -pub(crate) struct Analyzer<'a> { - recipes: Table<'a, Recipe<'a>>, - assignments: Table<'a, Assignment<'a>>, - aliases: Table<'a, Alias<'a>>, +pub(crate) struct Analyzer<'src> { + recipes: Table<'src, Recipe<'src>>, + assignments: Table<'src, Assignment<'src>>, + aliases: Table<'src, Alias<'src>>, + sets: Table<'src, Set<'src>>, } -impl<'a> Analyzer<'a> { - pub(crate) fn analyze(module: Module<'a>) -> CompilationResult<'a, Justfile> { +impl<'src> Analyzer<'src> { + pub(crate) fn analyze(module: Module<'src>) -> CompilationResult<'src, Justfile> { let analyzer = Analyzer::new(); analyzer.justfile(module) } - pub(crate) fn new() -> Analyzer<'a> { + pub(crate) fn new() -> Analyzer<'src> { Analyzer { recipes: empty(), assignments: empty(), aliases: empty(), + sets: empty(), } } - pub(crate) fn justfile(mut self, module: Module<'a>) -> CompilationResult<'a, Justfile<'a>> { + pub(crate) fn justfile( + mut self, + module: Module<'src>, + ) -> CompilationResult<'src, Justfile<'src>> { for item in module.items { match item { Item::Alias(alias) => { @@ -38,6 +43,10 @@ impl<'a> Analyzer<'a> { self.analyze_recipe(&recipe)?; self.recipes.insert(recipe); } + Item::Set(set) => { + self.analyze_set(&set)?; + self.sets.insert(set); + } } } @@ -70,15 +79,27 @@ impl<'a> Analyzer<'a> { AliasResolver::resolve_aliases(&aliases, &recipes)?; + let mut settings = Settings::new(); + + for (_, set) in self.sets.into_iter() { + match set.value { + Setting::Shell(shell) => { + assert!(settings.shell.is_none()); + settings.shell = Some(shell); + } + } + } + Ok(Justfile { warnings: module.warnings, - recipes, - assignments, aliases, + assignments, + recipes, + settings, }) } - fn analyze_recipe(&self, recipe: &Recipe<'a>) -> CompilationResult<'a, ()> { + fn analyze_recipe(&self, recipe: &Recipe<'src>) -> CompilationResult<'src, ()> { if let Some(original) = self.recipes.get(recipe.name.lexeme()) { return Err(recipe.name.token().error(DuplicateRecipe { recipe: original.name(), @@ -141,7 +162,7 @@ impl<'a> Analyzer<'a> { Ok(()) } - fn analyze_assignment(&self, assignment: &Assignment<'a>) -> CompilationResult<'a, ()> { + fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompilationResult<'src, ()> { if self.assignments.contains_key(assignment.name.lexeme()) { return Err(assignment.name.token().error(DuplicateVariable { variable: assignment.name.lexeme(), @@ -150,7 +171,7 @@ impl<'a> Analyzer<'a> { Ok(()) } - fn analyze_alias(&self, alias: &Alias<'a>) -> CompilationResult<'a, ()> { + fn analyze_alias(&self, alias: &Alias<'src>) -> CompilationResult<'src, ()> { let name = alias.name.lexeme(); if let Some(original) = self.aliases.get(name) { @@ -162,6 +183,17 @@ impl<'a> Analyzer<'a> { Ok(()) } + + fn analyze_set(&self, set: &Set<'src>) -> CompilationResult<'src, ()> { + if let Some(original) = self.sets.get(set.name.lexeme()) { + return Err(set.name.error(DuplicateSet { + setting: original.name.lexeme(), + first: original.name.line, + })); + } + + Ok(()) + } } #[cfg(test)] diff --git a/src/assignment_evaluator.rs b/src/assignment_evaluator.rs index 57c0d84..77b0b1d 100644 --- a/src/assignment_evaluator.rs +++ b/src/assignment_evaluator.rs @@ -8,6 +8,7 @@ pub(crate) struct AssignmentEvaluator<'a: 'b, 'b> { pub(crate) scope: &'b BTreeMap<&'a str, (bool, String)>, pub(crate) working_directory: &'b Path, pub(crate) overrides: &'b BTreeMap, + pub(crate) settings: &'b Settings<'b>, } impl<'a, 'b> AssignmentEvaluator<'a, 'b> { @@ -17,10 +18,12 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> { dotenv: &'b BTreeMap, assignments: &BTreeMap<&'a str, Assignment<'a>>, overrides: &BTreeMap, + settings: &'b Settings<'b>, ) -> RunResult<'a, BTreeMap<&'a str, (bool, String)>> { let mut evaluator = AssignmentEvaluator { evaluated: empty(), scope: &empty(), + settings, overrides, config, assignments, @@ -134,12 +137,12 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> { raw: &str, token: &Token<'a>, ) -> RunResult<'a, String> { - let mut cmd = Command::new(&self.config.shell); + let mut cmd = self.settings.shell_command(&self.config.shell); + + cmd.arg(raw); cmd.current_dir(self.working_directory); - cmd.arg("-cu").arg(raw); - cmd.export_environment_variables(self.scope, dotenv)?; cmd.stdin(process::Stdio::inherit()); diff --git a/src/common.rs b/src/common.rs index 125120a..02a61b7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -32,7 +32,7 @@ pub(crate) use snafu::{ResultExt, Snafu}; pub(crate) use unicode_width::UnicodeWidthChar; // modules -pub(crate) use crate::{config_error, keyword, search_error}; +pub(crate) use crate::{config_error, keyword, search_error, setting}; // functions pub(crate) use crate::{ @@ -60,10 +60,11 @@ pub(crate) use crate::{ parameter::Parameter, parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search::Search, - search_config::SearchConfig, search_error::SearchError, shebang::Shebang, - show_whitespace::ShowWhitespace, state::State, string_literal::StringLiteral, - subcommand::Subcommand, table::Table, token::Token, token_kind::TokenKind, use_color::UseColor, - variables::Variables, verbosity::Verbosity, warning::Warning, + search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting, + settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, state::State, + string_literal::StringLiteral, subcommand::Subcommand, table::Table, token::Token, + token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity, + warning::Warning, }; // type aliases diff --git a/src/compilation_error.rs b/src/compilation_error.rs index 80eafb4..c1df686 100644 --- a/src/compilation_error.rs +++ b/src/compilation_error.rs @@ -109,6 +109,15 @@ impl Display for CompilationError<'_> { self.line.ordinal() )?; } + DuplicateSet { setting, first } => { + writeln!( + f, + "Setting `{}` first set on line {} is redefined on line {}", + setting, + first.ordinal(), + self.line.ordinal(), + )?; + } DependencyHasParameters { recipe, dependency } => { writeln!( f, @@ -184,6 +193,9 @@ impl Display for CompilationError<'_> { UnknownFunction { function } => { writeln!(f, "Call to unknown function `{}`", function)?; } + UnknownSetting { setting } => { + writeln!(f, "Unknown setting `{}`", setting)?; + } UnknownStartOfToken => { writeln!(f, "Unknown start of token:")?; } diff --git a/src/compilation_error_kind.rs b/src/compilation_error_kind.rs index 5a1b378..a89dd37 100644 --- a/src/compilation_error_kind.rs +++ b/src/compilation_error_kind.rs @@ -37,6 +37,10 @@ pub(crate) enum CompilationErrorKind<'a> { DuplicateVariable { variable: &'a str, }, + DuplicateSet { + setting: &'a str, + first: usize, + }, ExtraLeadingWhitespace, FunctionArgumentCountMismatch { function: &'a str, @@ -84,6 +88,9 @@ pub(crate) enum CompilationErrorKind<'a> { function: &'a str, }, UnknownStartOfToken, + UnknownSetting { + setting: &'a str, + }, UnpairedCarriageReturn, UnterminatedInterpolation, UnterminatedString, diff --git a/src/item.rs b/src/item.rs index 1309a1b..cb078a7 100644 --- a/src/item.rs +++ b/src/item.rs @@ -6,4 +6,5 @@ pub(crate) enum Item<'src> { Alias(Alias<'src>), Assignment(Assignment<'src>), Recipe(Recipe<'src>), + Set(Set<'src>), } diff --git a/src/justfile.rs b/src/justfile.rs index 8c17339..1402ee2 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -1,14 +1,15 @@ use crate::common::*; #[derive(Debug, PartialEq)] -pub(crate) struct Justfile<'a> { - pub(crate) recipes: Table<'a, Recipe<'a>>, - pub(crate) assignments: Table<'a, Assignment<'a>>, - pub(crate) aliases: Table<'a, Alias<'a>>, - pub(crate) warnings: Vec>, +pub(crate) struct Justfile<'src> { + pub(crate) recipes: Table<'src, Recipe<'src>>, + pub(crate) assignments: Table<'src, Assignment<'src>>, + pub(crate) aliases: Table<'src, Alias<'src>>, + pub(crate) settings: Settings<'src>, + pub(crate) warnings: Vec>, } -impl<'a> Justfile<'a> { +impl<'src> Justfile<'src> { pub(crate) fn first(&self) -> Option<&Recipe> { let mut first: Option<&Recipe> = None; for recipe in self.recipes.values() { @@ -27,7 +28,7 @@ impl<'a> Justfile<'a> { self.recipes.len() } - pub(crate) fn suggest(&self, name: &str) -> Option<&'a str> { + pub(crate) fn suggest(&self, name: &str) -> Option<&'src str> { let mut suggestions = self .recipes .keys() @@ -43,12 +44,12 @@ impl<'a> Justfile<'a> { } pub(crate) fn run( - &'a self, - config: &'a Config, - working_directory: &'a Path, - overrides: &'a BTreeMap, - arguments: &'a Vec, - ) -> RunResult<'a, ()> { + &'src self, + config: &'src Config, + working_directory: &'src Path, + overrides: &'src BTreeMap, + arguments: &'src Vec, + ) -> RunResult<'src, ()> { let argvec: Vec<&str> = if !arguments.is_empty() { arguments.iter().map(|argument| argument.as_str()).collect() } else if let Some(recipe) = self.first() { @@ -86,6 +87,7 @@ impl<'a> Justfile<'a> { &dotenv, &self.assignments, overrides, + &self.settings, )?; if let Subcommand::Evaluate { .. } = config.subcommand { @@ -142,6 +144,7 @@ impl<'a> Justfile<'a> { } let context = RecipeContext { + settings: &self.settings, config, scope, working_directory, @@ -159,7 +162,7 @@ impl<'a> Justfile<'a> { self.aliases.get(name) } - pub(crate) fn get_recipe(&self, name: &str) -> Option<&Recipe<'a>> { + pub(crate) fn get_recipe(&self, name: &str) -> Option<&Recipe<'src>> { if let Some(recipe) = self.recipes.get(name) { Some(recipe) } else if let Some(alias) = self.aliases.get(name) { @@ -171,11 +174,11 @@ impl<'a> Justfile<'a> { fn run_recipe<'b>( &self, - context: &'b RecipeContext<'a>, - recipe: &Recipe<'a>, - arguments: &[&'a str], + context: &'b RecipeContext<'src>, + recipe: &Recipe<'src>, + arguments: &[&'src str], dotenv: &BTreeMap, - ran: &mut BTreeSet<&'a str>, + ran: &mut BTreeSet<&'src str>, overrides: &BTreeMap, ) -> RunResult<()> { for dependency_name in &recipe.dependencies { @@ -190,7 +193,7 @@ impl<'a> Justfile<'a> { } } -impl<'a> Display for Justfile<'a> { +impl<'src> Display for Justfile<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len(); for (name, assignment) in &self.assignments { diff --git a/src/keyword.rs b/src/keyword.rs index 0fa4ba0..8bf8371 100644 --- a/src/keyword.rs +++ b/src/keyword.rs @@ -1,2 +1,5 @@ pub(crate) const ALIAS: &str = "alias"; pub(crate) const EXPORT: &str = "export"; +pub(crate) const SET: &str = "set"; + +pub(crate) const SHELL: &str = "shell"; diff --git a/src/lexer.rs b/src/lexer.rs index 3427cf2..d809140 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -387,6 +387,8 @@ impl<'a> Lexer<'a> { fn lex_normal(&mut self, start: char) -> CompilationResult<'a, ()> { match start { '@' => self.lex_single(At), + '[' => self.lex_single(BracketL), + ']' => self.lex_single(BracketR), '=' => self.lex_single(Equals), ',' => self.lex_single(Comma), ':' => self.lex_colon(), @@ -759,6 +761,8 @@ mod tests { match kind { // Fixed lexemes At => "@", + BracketL => "[", + BracketR => "]", Colon => ":", ColonEquals => ":=", Comma => ",", @@ -1604,6 +1608,12 @@ mod tests { ), } + test! { + name: brackets, + text: "][", + tokens: (BracketR, BracketL), + } + error! { name: tokenize_space_then_tab, input: "a: diff --git a/src/lib.rs b/src/lib.rs index 85fbeb0..5d41582 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,9 @@ mod runtime_error; mod search; mod search_config; mod search_error; +mod set; +mod setting; +mod settings; mod shebang; mod show_whitespace; mod state; diff --git a/src/node.rs b/src/node.rs index 9a7aff0..609c6cc 100644 --- a/src/node.rs +++ b/src/node.rs @@ -21,6 +21,7 @@ impl<'src> Node<'src> for Item<'src> { Item::Alias(alias) => alias.tree(), Item::Assignment(assignment) => assignment.tree(), Item::Recipe(recipe) => recipe.tree(), + Item::Set(set) => set.tree(), } } } @@ -141,6 +142,26 @@ impl<'src> Node<'src> for Fragment<'src> { } } +impl<'src> Node<'src> for Set<'src> { + fn tree(&self) -> Tree<'src> { + let mut set = Tree::atom(keyword::SET); + + set.push_mut(self.name.lexeme()); + + use Setting::*; + match &self.value { + Shell(setting::Shell { command, arguments }) => { + set.push_mut(Tree::string(&command.cooked)); + for argument in arguments { + set.push_mut(Tree::string(&argument.cooked)); + } + } + } + + set + } +} + impl<'src> Node<'src> for Warning<'src> { fn tree(&self) -> Tree<'src> { match self { diff --git a/src/parser.rs b/src/parser.rs index b45e6d9..6a7aa8f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -135,6 +135,17 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { } } + /// Return an error if the next token is not one of kinds `kinds`. + fn expect_any(&mut self, expected: &[TokenKind]) -> CompilationResult<'src, Token<'src>> { + for expected in expected.iter().cloned() { + if let Some(token) = self.accept(expected)? { + return Ok(token); + } + } + + Err(self.unexpected_token(expected)?) + } + /// Return an unexpected token error if the next token is not an EOL fn expect_eol(&mut self) -> CompilationResult<'src, ()> { self.accept(Comment)?; @@ -269,6 +280,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { items.push(Item::Recipe(self.parse_recipe(doc, false)?)); } } + keyword::SET => { + if self.next_are(&[Identifier, Identifier, ColonEquals]) { + items.push(Item::Set(self.parse_set()?)); + } else { + items.push(Item::Recipe(self.parse_recipe(doc, false)?)); + } + } _ => { if self.next_are(&[Identifier, Equals]) { warnings.push(Warning::DeprecatedEquals { @@ -380,7 +398,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { /// Parse a string literal, e.g. `"FOO"` fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> { - let token = self.presume_any(&[StringRaw, StringCooked])?; + let token = self.expect_any(&[StringRaw, StringCooked])?; let raw = &token.lexeme()[1..token.lexeme().len() - 1]; @@ -580,12 +598,56 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { Ok(lines) } + + /// Parse a setting + fn parse_set(&mut self) -> CompilationResult<'src, Set<'src>> { + self.presume_name(keyword::SET)?; + let name = Name::from_identifier(self.presume(Identifier)?); + self.presume(ColonEquals)?; + match name.lexeme() { + keyword::SHELL => { + self.expect(BracketL)?; + + let command = self.parse_string_literal()?; + + let mut arguments = Vec::new(); + + let mut comma = false; + + if self.accepted(Comma)? { + comma = true; + while !self.next_is(BracketR) { + arguments.push(self.parse_string_literal().expected(&[BracketR])?); + + if !self.accepted(Comma)? { + comma = false; + break; + } + comma = true; + } + } + + self + .expect(BracketR) + .expected(if comma { &[] } else { &[Comma] })?; + + Ok(Set { + value: Setting::Shell(setting::Shell { command, arguments }), + name, + }) + } + _ => Err(name.error(CompilationErrorKind::UnknownSetting { + setting: name.lexeme(), + })), + } + } } #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; use testing::unindent; use CompilationErrorKind::*; @@ -1368,6 +1430,42 @@ mod tests { tree: (justfile (recipe a (body ("foo"))) (recipe b)), } + test! { + name: set_shell_no_arguments, + text: "set shell := ['tclsh']", + tree: (justfile (set shell "tclsh")), + } + + test! { + name: set_shell_no_arguments_cooked, + text: "set shell := [\"tclsh\"]", + tree: (justfile (set shell "tclsh")), + } + + test! { + name: set_shell_no_arguments_trailing_comma, + text: "set shell := ['tclsh',]", + tree: (justfile (set shell "tclsh")), + } + + test! { + name: set_shell_with_one_argument, + text: "set shell := ['bash', '-cu']", + tree: (justfile (set shell "bash" "-cu")), + } + + test! { + name: set_shell_with_one_argument_trailing_comma, + text: "set shell := ['bash', '-cu',]", + tree: (justfile (set shell "bash" "-cu")), + } + + test! { + name: set_shell_with_two_arguments, + text: "set shell := ['bash', '-cu', '-l']", + tree: (justfile (set shell "bash" "-cu" "-l")), + } + error! { name: alias_syntax_multiple_rhs, input: "alias foo = bar baz", @@ -1529,4 +1627,93 @@ mod tests { width: 1, kind: ParameterFollowsVariadicParameter{parameter: "e"}, } + + error! { + name: set_shell_empty, + input: "set shell := []", + offset: 14, + line: 0, + column: 14, + width: 1, + kind: UnexpectedToken { + expected: vec![StringCooked, StringRaw], + found: BracketR, + }, + } + + error! { + name: set_shell_non_literal_first, + input: "set shell := ['bar' + 'baz']", + offset: 20, + line: 0, + column: 20, + width: 1, + kind: UnexpectedToken { + expected: vec![BracketR, Comma], + found: Plus, + }, + } + + error! { + name: set_shell_non_literal_second, + input: "set shell := ['biz', 'bar' + 'baz']", + offset: 27, + line: 0, + column: 27, + width: 1, + kind: UnexpectedToken { + expected: vec![BracketR, Comma], + found: Plus, + }, + } + + error! { + name: set_shell_bad_comma, + input: "set shell := ['bash',", + offset: 21, + line: 0, + column: 21, + width: 0, + kind: UnexpectedToken { + expected: vec![BracketR, StringCooked, StringRaw], + found: Eof, + }, + } + + error! { + name: set_shell_bad, + input: "set shell := ['bash'", + offset: 20, + line: 0, + column: 20, + width: 0, + kind: UnexpectedToken { + expected: vec![BracketR, Comma], + found: Eof, + }, + } + + error! { + name: set_unknown, + input: "set shall := []", + offset: 4, + line: 0, + column: 4, + width: 5, + kind: UnknownSetting { + setting: "shall", + }, + } + + error! { + name: set_shell_non_string, + input: "set shall := []", + offset: 4, + line: 0, + column: 4, + width: 5, + kind: UnknownSetting { + setting: "shall", + }, + } } diff --git a/src/recipe.rs b/src/recipe.rs index dfdd4f8..5c8d425 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -1,6 +1,6 @@ use crate::common::*; -use std::process::{Command, ExitStatus, Stdio}; +use std::process::{ExitStatus, Stdio}; /// Return a `RuntimeError::Signal` if the process was terminated by a signal, /// otherwise return an `RuntimeError::UnknownFailure` @@ -90,6 +90,7 @@ impl<'a> Recipe<'a> { evaluated: empty(), working_directory: context.working_directory, scope: &context.scope, + settings: &context.settings, overrides, config, dotenv, @@ -274,11 +275,11 @@ impl<'a> Recipe<'a> { continue; } - let mut cmd = Command::new(&config.shell); + let mut cmd = context.settings.shell_command(&config.shell); cmd.current_dir(context.working_directory); - cmd.arg("-cu").arg(command); + cmd.arg(command); if config.quiet { cmd.stderr(Stdio::null()); diff --git a/src/recipe_context.rs b/src/recipe_context.rs index a05fa08..7c45b2c 100644 --- a/src/recipe_context.rs +++ b/src/recipe_context.rs @@ -4,4 +4,5 @@ pub(crate) struct RecipeContext<'a> { pub(crate) config: &'a Config, pub(crate) scope: BTreeMap<&'a str, (bool, String)>, pub(crate) working_directory: &'a Path, + pub(crate) settings: &'a Settings<'a>, } diff --git a/src/set.rs b/src/set.rs new file mode 100644 index 0000000..4f45df9 --- /dev/null +++ b/src/set.rs @@ -0,0 +1,13 @@ +use crate::common::*; + +#[derive(Debug)] +pub(crate) struct Set<'src> { + pub(crate) name: Name<'src>, + pub(crate) value: Setting<'src>, +} + +impl<'src> Keyed<'src> for Set<'src> { + fn key(&self) -> &'src str { + self.name.lexeme() + } +} diff --git a/src/setting.rs b/src/setting.rs new file mode 100644 index 0000000..abb9851 --- /dev/null +++ b/src/setting.rs @@ -0,0 +1,12 @@ +use crate::common::*; + +#[derive(Debug)] +pub(crate) enum Setting<'src> { + Shell(Shell<'src>), +} + +#[derive(Debug, PartialEq)] +pub(crate) struct Shell<'src> { + pub(crate) command: StringLiteral<'src>, + pub(crate) arguments: Vec>, +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..7ebd56c --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,30 @@ +use crate::common::*; + +#[derive(Debug, PartialEq)] +pub(crate) struct Settings<'src> { + pub(crate) shell: Option>, +} + +impl<'src> Settings<'src> { + pub(crate) fn new() -> Settings<'src> { + Settings { shell: None } + } + + pub(crate) fn shell_command(&self, default_shell: &str) -> Command { + if let Some(shell) = &self.shell { + let mut cmd = Command::new(shell.command.cooked.as_ref()); + + for argument in &shell.arguments { + cmd.arg(argument.cooked.as_ref()); + } + + cmd + } else { + let mut cmd = Command::new(default_shell); + + cmd.arg("-cu"); + + cmd + } + } +} diff --git a/src/token_kind.rs b/src/token_kind.rs index 0c4211e..52802f4 100644 --- a/src/token_kind.rs +++ b/src/token_kind.rs @@ -4,6 +4,8 @@ use crate::common::*; pub(crate) enum TokenKind { At, Backtick, + BracketL, + BracketR, Colon, ColonEquals, Comma, @@ -34,6 +36,8 @@ impl Display for TokenKind { match *self { At => "'@'", Backtick => "backtick", + BracketL => "'['", + BracketR => "']'", Colon => "':'", ColonEquals => "':='", Comma => "','", diff --git a/tests/integration.rs b/tests/integration.rs index 7df99f4..d32ea42 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -350,6 +350,22 @@ _y: stdout: "a b c d\n", } +test! { + name: set_shell, + justfile: " + set shell := ['echo', '-n'] + + x := `bar` + + foo: + echo {{x}} + echo foo + ", + args: (), + stdout: "echo barecho foo", + stderr: "echo bar\necho foo\n", +} + test! { name: select, justfile: "b: diff --git a/tests/shell.rs b/tests/shell.rs index f3cf8f5..179387d 100644 --- a/tests/shell.rs +++ b/tests/shell.rs @@ -14,9 +14,9 @@ recipe default=`DEFAULT`: "; /// Test that --shell correctly sets the shell -#[cfg(unix)] #[test] -fn shell() { +#[cfg_attr(windows, ignore)] +fn flag() { let tmp = tmptree! { justfile: JUSTFILE, shell: "#!/usr/bin/env bash\necho \"$@\"", @@ -24,8 +24,11 @@ fn shell() { let shell = tmp.path().join("shell"); - let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700); - std::fs::set_permissions(&shell, permissions).unwrap(); + #[cfg(not(windows))] + { + let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700); + std::fs::set_permissions(&shell, permissions).unwrap(); + } let output = Command::new(executable_path("just")) .current_dir(tmp.path()) @@ -35,6 +38,63 @@ fn shell() { .unwrap(); let stdout = "-cu -cu EXPRESSION\n-cu -cu DEFAULT\n-cu RECIPE\n"; + assert_stdout(&output, stdout); +} + +const JUSTFILE_CMD: &str = r#" + +set shell := ["cmd.exe", "/C"] + +x := `Echo` + +recipe: + REM foo + Echo "{{x}}" +"#; + +/// Test that we can use `set shell` to use cmd.exe on windows +#[test] +#[cfg_attr(unix, ignore)] +fn cmd() { + let tmp = tmptree! { + justfile: JUSTFILE_CMD, + }; + + let output = Command::new(executable_path("just")) + .current_dir(tmp.path()) + .output() + .unwrap(); + + let stdout = "\\\"ECHO is on.\\\"\r\n"; + + assert_stdout(&output, stdout); +} + +const JUSTFILE_POWERSHELL: &str = r#" + +set shell := ["powershell.exe", "-c"] + +x := `Write-Host "Hello, world!"` + +recipe: + For ($i=0; $i -le 10; $i++) { Write-Host $i } + Write-Host "{{x}}" +"#; + +/// Test that we can use `set shell` to use cmd.exe on windows +#[test] +#[cfg_attr(unix, ignore)] +fn powershell() { + let tmp = tmptree! { + justfile: JUSTFILE_POWERSHELL, + }; + + let output = Command::new(executable_path("just")) + .current_dir(tmp.path()) + .output() + .unwrap(); + + let stdout = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\nHello, world!\n"; assert_stdout(&output, stdout); }