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:
parent
177516bcbe
commit
e80bf34d9a
@ -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)]
|
||||
|
@ -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<String, String>,
|
||||
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<String, String>,
|
||||
assignments: &BTreeMap<&'a str, Assignment<'a>>,
|
||||
overrides: &BTreeMap<String, String>,
|
||||
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());
|
||||
|
@ -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
|
||||
|
@ -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:")?;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -6,4 +6,5 @@ pub(crate) enum Item<'src> {
|
||||
Alias(Alias<'src>),
|
||||
Assignment(Assignment<'src>),
|
||||
Recipe(Recipe<'src>),
|
||||
Set(Set<'src>),
|
||||
}
|
||||
|
@ -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<Warning<'a>>,
|
||||
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<Warning<'src>>,
|
||||
}
|
||||
|
||||
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<String, String>,
|
||||
arguments: &'a Vec<String>,
|
||||
) -> RunResult<'a, ()> {
|
||||
&'src self,
|
||||
config: &'src Config,
|
||||
working_directory: &'src Path,
|
||||
overrides: &'src BTreeMap<String, String>,
|
||||
arguments: &'src Vec<String>,
|
||||
) -> 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<String, String>,
|
||||
ran: &mut BTreeSet<&'a str>,
|
||||
ran: &mut BTreeSet<&'src str>,
|
||||
overrides: &BTreeMap<String, String>,
|
||||
) -> 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 {
|
||||
|
@ -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";
|
||||
|
10
src/lexer.rs
10
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:
|
||||
|
@ -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;
|
||||
|
21
src/node.rs
21
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 {
|
||||
|
189
src/parser.rs
189
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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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>,
|
||||
}
|
||||
|
13
src/set.rs
Normal file
13
src/set.rs
Normal 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
12
src/setting.rs
Normal 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
30
src/settings.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 => "','",
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user