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.
This commit is contained in:
Casey Rodarmor 2019-11-10 23:17:47 -08:00 committed by GitHub
parent 177516bcbe
commit e80bf34d9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 468 additions and 48 deletions

View File

@ -2,28 +2,33 @@ use crate::common::*;
use CompilationErrorKind::*; use CompilationErrorKind::*;
pub(crate) struct Analyzer<'a> { pub(crate) struct Analyzer<'src> {
recipes: Table<'a, Recipe<'a>>, recipes: Table<'src, Recipe<'src>>,
assignments: Table<'a, Assignment<'a>>, assignments: Table<'src, Assignment<'src>>,
aliases: Table<'a, Alias<'a>>, aliases: Table<'src, Alias<'src>>,
sets: Table<'src, Set<'src>>,
} }
impl<'a> Analyzer<'a> { impl<'src> Analyzer<'src> {
pub(crate) fn analyze(module: Module<'a>) -> CompilationResult<'a, Justfile> { pub(crate) fn analyze(module: Module<'src>) -> CompilationResult<'src, Justfile> {
let analyzer = Analyzer::new(); let analyzer = Analyzer::new();
analyzer.justfile(module) analyzer.justfile(module)
} }
pub(crate) fn new() -> Analyzer<'a> { pub(crate) fn new() -> Analyzer<'src> {
Analyzer { Analyzer {
recipes: empty(), recipes: empty(),
assignments: empty(), assignments: empty(),
aliases: 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 { for item in module.items {
match item { match item {
Item::Alias(alias) => { Item::Alias(alias) => {
@ -38,6 +43,10 @@ impl<'a> Analyzer<'a> {
self.analyze_recipe(&recipe)?; self.analyze_recipe(&recipe)?;
self.recipes.insert(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)?; 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 { Ok(Justfile {
warnings: module.warnings, warnings: module.warnings,
recipes,
assignments,
aliases, 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()) { if let Some(original) = self.recipes.get(recipe.name.lexeme()) {
return Err(recipe.name.token().error(DuplicateRecipe { return Err(recipe.name.token().error(DuplicateRecipe {
recipe: original.name(), recipe: original.name(),
@ -141,7 +162,7 @@ impl<'a> Analyzer<'a> {
Ok(()) 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()) { if self.assignments.contains_key(assignment.name.lexeme()) {
return Err(assignment.name.token().error(DuplicateVariable { return Err(assignment.name.token().error(DuplicateVariable {
variable: assignment.name.lexeme(), variable: assignment.name.lexeme(),
@ -150,7 +171,7 @@ impl<'a> Analyzer<'a> {
Ok(()) Ok(())
} }
fn analyze_alias(&self, alias: &Alias<'a>) -> CompilationResult<'a, ()> { fn analyze_alias(&self, alias: &Alias<'src>) -> CompilationResult<'src, ()> {
let name = alias.name.lexeme(); let name = alias.name.lexeme();
if let Some(original) = self.aliases.get(name) { if let Some(original) = self.aliases.get(name) {
@ -162,6 +183,17 @@ impl<'a> Analyzer<'a> {
Ok(()) 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)] #[cfg(test)]

View File

@ -8,6 +8,7 @@ pub(crate) struct AssignmentEvaluator<'a: 'b, 'b> {
pub(crate) scope: &'b BTreeMap<&'a str, (bool, String)>, pub(crate) scope: &'b BTreeMap<&'a str, (bool, String)>,
pub(crate) working_directory: &'b Path, pub(crate) working_directory: &'b Path,
pub(crate) overrides: &'b BTreeMap<String, String>, pub(crate) overrides: &'b BTreeMap<String, String>,
pub(crate) settings: &'b Settings<'b>,
} }
impl<'a, 'b> AssignmentEvaluator<'a, 'b> { impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
@ -17,10 +18,12 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
dotenv: &'b BTreeMap<String, String>, dotenv: &'b BTreeMap<String, String>,
assignments: &BTreeMap<&'a str, Assignment<'a>>, assignments: &BTreeMap<&'a str, Assignment<'a>>,
overrides: &BTreeMap<String, String>, overrides: &BTreeMap<String, String>,
settings: &'b Settings<'b>,
) -> RunResult<'a, BTreeMap<&'a str, (bool, String)>> { ) -> RunResult<'a, BTreeMap<&'a str, (bool, String)>> {
let mut evaluator = AssignmentEvaluator { let mut evaluator = AssignmentEvaluator {
evaluated: empty(), evaluated: empty(),
scope: &empty(), scope: &empty(),
settings,
overrides, overrides,
config, config,
assignments, assignments,
@ -134,12 +137,12 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
raw: &str, raw: &str,
token: &Token<'a>, token: &Token<'a>,
) -> RunResult<'a, String> { ) -> 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.current_dir(self.working_directory);
cmd.arg("-cu").arg(raw);
cmd.export_environment_variables(self.scope, dotenv)?; cmd.export_environment_variables(self.scope, dotenv)?;
cmd.stdin(process::Stdio::inherit()); cmd.stdin(process::Stdio::inherit());

View File

@ -32,7 +32,7 @@ pub(crate) use snafu::{ResultExt, Snafu};
pub(crate) use unicode_width::UnicodeWidthChar; pub(crate) use unicode_width::UnicodeWidthChar;
// modules // modules
pub(crate) use crate::{config_error, keyword, search_error}; pub(crate) use crate::{config_error, keyword, search_error, setting};
// functions // functions
pub(crate) use crate::{ pub(crate) use crate::{
@ -60,10 +60,11 @@ pub(crate) use crate::{
parameter::Parameter, parser::Parser, platform::Platform, position::Position, parameter::Parameter, parser::Parser, platform::Platform, position::Position,
positional::Positional, recipe::Recipe, recipe_context::RecipeContext, positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search::Search, recipe_resolver::RecipeResolver, runtime_error::RuntimeError, search::Search,
search_config::SearchConfig, search_error::SearchError, shebang::Shebang, search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
show_whitespace::ShowWhitespace, state::State, string_literal::StringLiteral, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, state::State,
subcommand::Subcommand, table::Table, token::Token, token_kind::TokenKind, use_color::UseColor, string_literal::StringLiteral, subcommand::Subcommand, table::Table, token::Token,
variables::Variables, verbosity::Verbosity, warning::Warning, token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity,
warning::Warning,
}; };
// type aliases // type aliases

View File

@ -109,6 +109,15 @@ impl Display for CompilationError<'_> {
self.line.ordinal() 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 } => { DependencyHasParameters { recipe, dependency } => {
writeln!( writeln!(
f, f,
@ -184,6 +193,9 @@ impl Display for CompilationError<'_> {
UnknownFunction { function } => { UnknownFunction { function } => {
writeln!(f, "Call to unknown function `{}`", function)?; writeln!(f, "Call to unknown function `{}`", function)?;
} }
UnknownSetting { setting } => {
writeln!(f, "Unknown setting `{}`", setting)?;
}
UnknownStartOfToken => { UnknownStartOfToken => {
writeln!(f, "Unknown start of token:")?; writeln!(f, "Unknown start of token:")?;
} }

View File

@ -37,6 +37,10 @@ pub(crate) enum CompilationErrorKind<'a> {
DuplicateVariable { DuplicateVariable {
variable: &'a str, variable: &'a str,
}, },
DuplicateSet {
setting: &'a str,
first: usize,
},
ExtraLeadingWhitespace, ExtraLeadingWhitespace,
FunctionArgumentCountMismatch { FunctionArgumentCountMismatch {
function: &'a str, function: &'a str,
@ -84,6 +88,9 @@ pub(crate) enum CompilationErrorKind<'a> {
function: &'a str, function: &'a str,
}, },
UnknownStartOfToken, UnknownStartOfToken,
UnknownSetting {
setting: &'a str,
},
UnpairedCarriageReturn, UnpairedCarriageReturn,
UnterminatedInterpolation, UnterminatedInterpolation,
UnterminatedString, UnterminatedString,

View File

@ -6,4 +6,5 @@ pub(crate) enum Item<'src> {
Alias(Alias<'src>), Alias(Alias<'src>),
Assignment(Assignment<'src>), Assignment(Assignment<'src>),
Recipe(Recipe<'src>), Recipe(Recipe<'src>),
Set(Set<'src>),
} }

View File

@ -1,14 +1,15 @@
use crate::common::*; use crate::common::*;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub(crate) struct Justfile<'a> { pub(crate) struct Justfile<'src> {
pub(crate) recipes: Table<'a, Recipe<'a>>, pub(crate) recipes: Table<'src, Recipe<'src>>,
pub(crate) assignments: Table<'a, Assignment<'a>>, pub(crate) assignments: Table<'src, Assignment<'src>>,
pub(crate) aliases: Table<'a, Alias<'a>>, pub(crate) aliases: Table<'src, Alias<'src>>,
pub(crate) warnings: Vec<Warning<'a>>, pub(crate) settings: Settings<'src>,
pub(crate) warnings: Vec<Warning<'src>>,
} }
impl<'a> Justfile<'a> { impl<'src> Justfile<'src> {
pub(crate) fn first(&self) -> Option<&Recipe> { pub(crate) fn first(&self) -> Option<&Recipe> {
let mut first: Option<&Recipe> = None; let mut first: Option<&Recipe> = None;
for recipe in self.recipes.values() { for recipe in self.recipes.values() {
@ -27,7 +28,7 @@ impl<'a> Justfile<'a> {
self.recipes.len() 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 let mut suggestions = self
.recipes .recipes
.keys() .keys()
@ -43,12 +44,12 @@ impl<'a> Justfile<'a> {
} }
pub(crate) fn run( pub(crate) fn run(
&'a self, &'src self,
config: &'a Config, config: &'src Config,
working_directory: &'a Path, working_directory: &'src Path,
overrides: &'a BTreeMap<String, String>, overrides: &'src BTreeMap<String, String>,
arguments: &'a Vec<String>, arguments: &'src Vec<String>,
) -> RunResult<'a, ()> { ) -> RunResult<'src, ()> {
let argvec: Vec<&str> = if !arguments.is_empty() { let argvec: Vec<&str> = if !arguments.is_empty() {
arguments.iter().map(|argument| argument.as_str()).collect() arguments.iter().map(|argument| argument.as_str()).collect()
} else if let Some(recipe) = self.first() { } else if let Some(recipe) = self.first() {
@ -86,6 +87,7 @@ impl<'a> Justfile<'a> {
&dotenv, &dotenv,
&self.assignments, &self.assignments,
overrides, overrides,
&self.settings,
)?; )?;
if let Subcommand::Evaluate { .. } = config.subcommand { if let Subcommand::Evaluate { .. } = config.subcommand {
@ -142,6 +144,7 @@ impl<'a> Justfile<'a> {
} }
let context = RecipeContext { let context = RecipeContext {
settings: &self.settings,
config, config,
scope, scope,
working_directory, working_directory,
@ -159,7 +162,7 @@ impl<'a> Justfile<'a> {
self.aliases.get(name) 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) { if let Some(recipe) = self.recipes.get(name) {
Some(recipe) Some(recipe)
} else if let Some(alias) = self.aliases.get(name) { } else if let Some(alias) = self.aliases.get(name) {
@ -171,11 +174,11 @@ impl<'a> Justfile<'a> {
fn run_recipe<'b>( fn run_recipe<'b>(
&self, &self,
context: &'b RecipeContext<'a>, context: &'b RecipeContext<'src>,
recipe: &Recipe<'a>, recipe: &Recipe<'src>,
arguments: &[&'a str], arguments: &[&'src str],
dotenv: &BTreeMap<String, String>, dotenv: &BTreeMap<String, String>,
ran: &mut BTreeSet<&'a str>, ran: &mut BTreeSet<&'src str>,
overrides: &BTreeMap<String, String>, overrides: &BTreeMap<String, String>,
) -> RunResult<()> { ) -> RunResult<()> {
for dependency_name in &recipe.dependencies { 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> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len(); let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
for (name, assignment) in &self.assignments { for (name, assignment) in &self.assignments {

View File

@ -1,2 +1,5 @@
pub(crate) const ALIAS: &str = "alias"; pub(crate) const ALIAS: &str = "alias";
pub(crate) const EXPORT: &str = "export"; pub(crate) const EXPORT: &str = "export";
pub(crate) const SET: &str = "set";
pub(crate) const SHELL: &str = "shell";

View File

@ -387,6 +387,8 @@ impl<'a> Lexer<'a> {
fn lex_normal(&mut self, start: char) -> CompilationResult<'a, ()> { fn lex_normal(&mut self, start: char) -> CompilationResult<'a, ()> {
match start { match start {
'@' => self.lex_single(At), '@' => self.lex_single(At),
'[' => self.lex_single(BracketL),
']' => self.lex_single(BracketR),
'=' => self.lex_single(Equals), '=' => self.lex_single(Equals),
',' => self.lex_single(Comma), ',' => self.lex_single(Comma),
':' => self.lex_colon(), ':' => self.lex_colon(),
@ -759,6 +761,8 @@ mod tests {
match kind { match kind {
// Fixed lexemes // Fixed lexemes
At => "@", At => "@",
BracketL => "[",
BracketR => "]",
Colon => ":", Colon => ":",
ColonEquals => ":=", ColonEquals => ":=",
Comma => ",", Comma => ",",
@ -1604,6 +1608,12 @@ mod tests {
), ),
} }
test! {
name: brackets,
text: "][",
tokens: (BracketR, BracketL),
}
error! { error! {
name: tokenize_space_then_tab, name: tokenize_space_then_tab,
input: "a: input: "a:

View File

@ -72,6 +72,9 @@ mod runtime_error;
mod search; mod search;
mod search_config; mod search_config;
mod search_error; mod search_error;
mod set;
mod setting;
mod settings;
mod shebang; mod shebang;
mod show_whitespace; mod show_whitespace;
mod state; mod state;

View File

@ -21,6 +21,7 @@ impl<'src> Node<'src> for Item<'src> {
Item::Alias(alias) => alias.tree(), Item::Alias(alias) => alias.tree(),
Item::Assignment(assignment) => assignment.tree(), Item::Assignment(assignment) => assignment.tree(),
Item::Recipe(recipe) => recipe.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> { impl<'src> Node<'src> for Warning<'src> {
fn tree(&self) -> Tree<'src> { fn tree(&self) -> Tree<'src> {
match self { match self {

View File

@ -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 /// Return an unexpected token error if the next token is not an EOL
fn expect_eol(&mut self) -> CompilationResult<'src, ()> { fn expect_eol(&mut self) -> CompilationResult<'src, ()> {
self.accept(Comment)?; self.accept(Comment)?;
@ -269,6 +280,13 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
items.push(Item::Recipe(self.parse_recipe(doc, false)?)); 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]) { if self.next_are(&[Identifier, Equals]) {
warnings.push(Warning::DeprecatedEquals { warnings.push(Warning::DeprecatedEquals {
@ -380,7 +398,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
/// Parse a string literal, e.g. `"FOO"` /// Parse a string literal, e.g. `"FOO"`
fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> { 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]; let raw = &token.lexeme()[1..token.lexeme().len() - 1];
@ -580,12 +598,56 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
Ok(lines) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use pretty_assertions::assert_eq;
use testing::unindent; use testing::unindent;
use CompilationErrorKind::*; use CompilationErrorKind::*;
@ -1368,6 +1430,42 @@ mod tests {
tree: (justfile (recipe a (body ("foo"))) (recipe b)), 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! { error! {
name: alias_syntax_multiple_rhs, name: alias_syntax_multiple_rhs,
input: "alias foo = bar baz", input: "alias foo = bar baz",
@ -1529,4 +1627,93 @@ mod tests {
width: 1, width: 1,
kind: ParameterFollowsVariadicParameter{parameter: "e"}, 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",
},
}
} }

View File

@ -1,6 +1,6 @@
use crate::common::*; 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, /// Return a `RuntimeError::Signal` if the process was terminated by a signal,
/// otherwise return an `RuntimeError::UnknownFailure` /// otherwise return an `RuntimeError::UnknownFailure`
@ -90,6 +90,7 @@ impl<'a> Recipe<'a> {
evaluated: empty(), evaluated: empty(),
working_directory: context.working_directory, working_directory: context.working_directory,
scope: &context.scope, scope: &context.scope,
settings: &context.settings,
overrides, overrides,
config, config,
dotenv, dotenv,
@ -274,11 +275,11 @@ impl<'a> Recipe<'a> {
continue; continue;
} }
let mut cmd = Command::new(&config.shell); let mut cmd = context.settings.shell_command(&config.shell);
cmd.current_dir(context.working_directory); cmd.current_dir(context.working_directory);
cmd.arg("-cu").arg(command); cmd.arg(command);
if config.quiet { if config.quiet {
cmd.stderr(Stdio::null()); cmd.stderr(Stdio::null());

View File

@ -4,4 +4,5 @@ pub(crate) struct RecipeContext<'a> {
pub(crate) config: &'a Config, pub(crate) config: &'a Config,
pub(crate) scope: BTreeMap<&'a str, (bool, String)>, pub(crate) scope: BTreeMap<&'a str, (bool, String)>,
pub(crate) working_directory: &'a Path, pub(crate) working_directory: &'a Path,
pub(crate) settings: &'a Settings<'a>,
} }

13
src/set.rs Normal file
View File

@ -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()
}
}

12
src/setting.rs Normal file
View File

@ -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<StringLiteral<'src>>,
}

30
src/settings.rs Normal file
View File

@ -0,0 +1,30 @@
use crate::common::*;
#[derive(Debug, PartialEq)]
pub(crate) struct Settings<'src> {
pub(crate) shell: Option<setting::Shell<'src>>,
}
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
}
}
}

View File

@ -4,6 +4,8 @@ use crate::common::*;
pub(crate) enum TokenKind { pub(crate) enum TokenKind {
At, At,
Backtick, Backtick,
BracketL,
BracketR,
Colon, Colon,
ColonEquals, ColonEquals,
Comma, Comma,
@ -34,6 +36,8 @@ impl Display for TokenKind {
match *self { match *self {
At => "'@'", At => "'@'",
Backtick => "backtick", Backtick => "backtick",
BracketL => "'['",
BracketR => "']'",
Colon => "':'", Colon => "':'",
ColonEquals => "':='", ColonEquals => "':='",
Comma => "','", Comma => "','",

View File

@ -350,6 +350,22 @@ _y:
stdout: "a b c d\n", 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! { test! {
name: select, name: select,
justfile: "b: justfile: "b:

View File

@ -14,9 +14,9 @@ recipe default=`DEFAULT`:
"; ";
/// Test that --shell correctly sets the shell /// Test that --shell correctly sets the shell
#[cfg(unix)]
#[test] #[test]
fn shell() { #[cfg_attr(windows, ignore)]
fn flag() {
let tmp = tmptree! { let tmp = tmptree! {
justfile: JUSTFILE, justfile: JUSTFILE,
shell: "#!/usr/bin/env bash\necho \"$@\"", shell: "#!/usr/bin/env bash\necho \"$@\"",
@ -24,8 +24,11 @@ fn shell() {
let shell = tmp.path().join("shell"); let shell = tmp.path().join("shell");
let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700); #[cfg(not(windows))]
std::fs::set_permissions(&shell, permissions).unwrap(); {
let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700);
std::fs::set_permissions(&shell, permissions).unwrap();
}
let output = Command::new(executable_path("just")) let output = Command::new(executable_path("just"))
.current_dir(tmp.path()) .current_dir(tmp.path())
@ -35,6 +38,63 @@ fn shell() {
.unwrap(); .unwrap();
let stdout = "-cu -cu EXPRESSION\n-cu -cu DEFAULT\n-cu RECIPE\n"; 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); assert_stdout(&output, stdout);
} }