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::*;
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)]

View File

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

View File

@ -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

View File

@ -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:")?;
}

View File

@ -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,

View File

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

View File

@ -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 {

View File

@ -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";

View File

@ -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:

View File

@ -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;

View File

@ -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 {

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
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",
},
}
}

View File

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

View File

@ -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
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 {
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 => "','",

View File

@ -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:

View File

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