Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow empty [script] attribute and add set script-interpreter #2264

Merged
merged 1 commit into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ lexiclean = "0.0.1"
libc = "0.2.0"
log = "0.4.4"
num_cpus = "1.15.0"
once_cell = "1.19.0"
percent-encoding = "2.3.1"
rand = "0.8.5"
regex = "1.10.4"
Expand Down
21 changes: 12 additions & 9 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,17 +186,20 @@ impl<'src> Analyzer<'src> {

let root = paths.get(root).unwrap();

let unstable_features = recipes
.values()
.flat_map(|recipe| &recipe.attributes)
.filter_map(|attribute| {
let mut unstable_features = BTreeSet::new();

for recipe in recipes.values() {
for attribute in &recipe.attributes {
if let Attribute::Script(_) = attribute {
Some(UnstableFeature::ScriptAttribute)
} else {
None
unstable_features.insert(UnstableFeature::ScriptAttribute);
break;
}
})
.collect();
}
}

if settings.script_interpreter.is_some() {
unstable_features.insert(UnstableFeature::ScriptInterpreterSetting);
}

Ok(Justfile {
aliases,
Expand Down
47 changes: 18 additions & 29 deletions src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub(crate) enum Attribute<'src> {
NoQuiet,
PositionalArguments,
Private,
Script(Vec<StringLiteral<'src>>),
Script(Option<Interpreter<'src>>),
Unix,
Windows,
}
Expand All @@ -39,7 +39,7 @@ impl AttributeDiscriminant {
| Self::Private
| Self::Unix
| Self::Windows => 0..=0,
Self::Script => 1..=usize::MAX,
Self::Script => 0..=usize::MAX,
}
}
}
Expand Down Expand Up @@ -84,7 +84,13 @@ impl<'src> Attribute<'src> {
AttributeDiscriminant::NoQuiet => Self::NoQuiet,
AttributeDiscriminant::PositionalArguments => Self::PositionalArguments,
AttributeDiscriminant::Private => Self::Private,
AttributeDiscriminant::Script => Self::Script(arguments),
AttributeDiscriminant::Script => Self::Script({
let mut arguments = arguments.into_iter();
arguments.next().map(|command| Interpreter {
command,
arguments: arguments.collect(),
})
}),
AttributeDiscriminant::Unix => Self::Unix,
AttributeDiscriminant::Windows => Self::Windows,
})
Expand All @@ -93,14 +99,18 @@ impl<'src> Attribute<'src> {
pub(crate) fn name(&self) -> &'static str {
self.into()
}
}

impl<'src> Display for Attribute<'src> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name())?;

fn arguments(&self) -> &[StringLiteral] {
match self {
Self::Confirm(Some(argument))
| Self::Doc(Some(argument))
| Self::Extension(argument)
| Self::Group(argument) => slice::from_ref(argument),
Self::Script(arguments) => arguments,
| Self::Group(argument) => write!(f, "({argument})")?,
Self::Script(Some(shell)) => write!(f, "({shell})")?,
Self::Confirm(None)
| Self::Doc(None)
| Self::Linux
Expand All @@ -110,30 +120,9 @@ impl<'src> Attribute<'src> {
| Self::NoQuiet
| Self::PositionalArguments
| Self::Private
| Self::Script(None)
| Self::Unix
| Self::Windows => &[],
}
}
}

impl<'src> Display for Attribute<'src> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name())?;

let arguments = self.arguments();

for (i, argument) in arguments.iter().enumerate() {
if i == 0 {
write!(f, "(")?;
} else {
write!(f, ", ")?;
}

write!(f, "{argument}")?;

if i + 1 == arguments.len() {
write!(f, ")")?;
}
| Self::Windows => {}
}

Ok(())
Expand Down
30 changes: 16 additions & 14 deletions src/executor.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::*;

pub(crate) enum Executor<'a> {
Command(Vec<&'a str>),
Command(&'a Interpreter<'a>),
Shebang(Shebang<'a>),
}

Expand All @@ -13,15 +13,15 @@ impl<'a> Executor<'a> {
working_directory: Option<&Path>,
) -> RunResult<'src, Command> {
match self {
Self::Command(args) => {
let mut command = Command::new(args[0]);
Self::Command(interpreter) => {
let mut command = Command::new(&interpreter.command.cooked);

if let Some(working_directory) = working_directory {
command.current_dir(working_directory);
}

for arg in &args[1..] {
command.arg(arg);
for arg in &interpreter.arguments {
command.arg(&arg.cooked);
}

command.arg(path);
Expand Down Expand Up @@ -49,7 +49,7 @@ impl<'a> Executor<'a> {
pub(crate) fn script_filename(&self, recipe: &str, extension: Option<&str>) -> String {
let extension = extension.unwrap_or_else(|| {
let interpreter = match self {
Self::Command(args) => args[0],
Self::Command(interpreter) => &interpreter.command.cooked,
Self::Shebang(shebang) => shebang.interpreter_filename(),
};

Expand All @@ -65,14 +65,12 @@ impl<'a> Executor<'a> {

pub(crate) fn error<'src>(&self, io_error: io::Error, recipe: &'src str) -> Error<'src> {
match self {
Self::Command(args) => {
let mut command = String::new();
Self::Command(Interpreter { command, arguments }) => {
let mut command = command.cooked.clone();

for (i, arg) in args.iter().enumerate() {
if i > 0 {
command.push(' ');
}
command.push_str(arg);
for arg in arguments {
command.push(' ');
command.push_str(&arg.cooked);
}

Error::Script {
Expand Down Expand Up @@ -152,7 +150,11 @@ mod tests {
expected
);
assert_eq!(
Executor::Command(vec![interpreter]).script_filename(recipe, extension),
Executor::Command(&Interpreter {
command: StringLiteral::from_raw(interpreter),
arguments: Vec::new()
})
.script_filename(recipe, extension),
expected
);
}
Expand Down
29 changes: 29 additions & 0 deletions src/interpreter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use super::*;

#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub(crate) struct Interpreter<'src> {
pub(crate) arguments: Vec<StringLiteral<'src>>,
pub(crate) command: StringLiteral<'src>,
}

impl<'src> Interpreter<'src> {
pub(crate) fn default_script_interpreter() -> &'static Interpreter<'static> {
static INSTANCE: Lazy<Interpreter<'static>> = Lazy::new(|| Interpreter {
arguments: vec![StringLiteral::from_raw("-eu")],
command: StringLiteral::from_raw("sh"),
});
&INSTANCE
}
}

impl<'src> Display for Interpreter<'src> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.command)?;

for argument in &self.arguments {
write!(f, ", {argument}")?;
}

Ok(())
}
}
1 change: 1 addition & 0 deletions src/keyword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub(crate) enum Keyword {
Mod,
PositionalArguments,
Quiet,
ScriptInterpreter,
Set,
Shell,
Tempdir,
Expand Down
36 changes: 19 additions & 17 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,22 @@ pub(crate) use {
constants::constants, count::Count, delimiter::Delimiter, dependency::Dependency,
dump_format::DumpFormat, enclosure::Enclosure, error::Error, evaluator::Evaluator,
execution_context::ExecutionContext, executor::Executor, expression::Expression,
fragment::Fragment, function::Function, interrupt_guard::InterruptGuard,
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyed::Keyed,
keyword::Keyword, lexer::Lexer, line::Line, list::List, load_dotenv::load_dotenv,
loader::Loader, module_path::ModulePath, name::Name, namepath::Namepath, ordinal::Ordinal,
output::output, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
parser::Parser, platform::Platform, platform_interface::PlatformInterface, position::Position,
positional::Positional, ran::Ran, range_ext::RangeExt, recipe::Recipe,
recipe_resolver::RecipeResolver, recipe_signature::RecipeSignature, scope::Scope,
search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
setting::Setting, settings::Settings, shebang::Shebang, shell::Shell,
show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, unstable_feature::UnstableFeature, use_color::UseColor,
variables::Variables, verbosity::Verbosity, warning::Warning,
fragment::Fragment, function::Function, interpreter::Interpreter,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List,
load_dotenv::load_dotenv, loader::Loader, module_path::ModulePath, name::Name,
namepath::Namepath, ordinal::Ordinal, output::output, output_error::OutputError,
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
platform_interface::PlatformInterface, position::Position, positional::Positional, ran::Ran,
range_ext::RangeExt, recipe::Recipe, recipe_resolver::RecipeResolver,
recipe_signature::RecipeSignature, scope::Scope, search::Search, search_config::SearchConfig,
search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang,
show_whitespace::ShowWhitespace, source::Source, string_delimiter::StringDelimiter,
string_kind::StringKind, string_literal::StringLiteral, subcommand::Subcommand,
suggestion::Suggestion, table::Table, thunk::Thunk, token::Token, token_kind::TokenKind,
unresolved_dependency::UnresolvedDependency, unresolved_recipe::UnresolvedRecipe,
unstable_feature::UnstableFeature, use_color::UseColor, variables::Variables,
verbosity::Verbosity, warning::Warning,
},
camino::Utf8Path,
clap::ValueEnum,
Expand All @@ -53,6 +54,7 @@ pub(crate) use {
lexiclean::Lexiclean,
libc::EXIT_FAILURE,
log::{info, warn},
once_cell::sync::Lazy,
regex::Regex,
serde::{
ser::{SerializeMap, SerializeSeq},
Expand All @@ -75,7 +77,6 @@ pub(crate) use {
path::{self, Path, PathBuf},
process::{self, Command, ExitStatus, Stdio},
rc::Rc,
slice,
str::{self, Chars},
sync::{Mutex, MutexGuard, OnceLock},
vec,
Expand Down Expand Up @@ -155,6 +156,7 @@ mod executor;
mod expression;
mod fragment;
mod function;
mod interpreter;
mod interrupt_guard;
mod interrupt_handler;
mod item;
Expand Down Expand Up @@ -193,9 +195,9 @@ mod set;
mod setting;
mod settings;
mod shebang;
mod shell;
mod show_whitespace;
mod source;
mod string_delimiter;
mod string_kind;
mod string_literal;
mod subcommand;
Expand Down
7 changes: 4 additions & 3 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,15 +299,16 @@ impl<'src> Node<'src> for Set<'src> {
| Setting::IgnoreComments(value) => {
set.push_mut(value.to_string());
}
Setting::Shell(Shell { command, arguments })
| Setting::WindowsShell(Shell { command, arguments }) => {
Setting::ScriptInterpreter(Interpreter { command, arguments })
| Setting::Shell(Interpreter { command, arguments })
| Setting::WindowsShell(Interpreter { command, arguments }) => {
set.push_mut(Tree::string(&command.cooked));
for argument in arguments {
set.push_mut(Tree::string(&argument.cooked));
}
}
Setting::DotenvFilename(value) | Setting::DotenvPath(value) | Setting::Tempdir(value) => {
set.push_mut(Tree::string(value));
set.push_mut(Tree::string(&value.cooked));
}
}

Expand Down
17 changes: 9 additions & 8 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -961,11 +961,12 @@ impl<'run, 'src> Parser<'run, 'src> {
self.expect(ColonEquals)?;

let set_value = match keyword {
Keyword::DotenvFilename => Some(Setting::DotenvFilename(self.parse_string_literal()?.cooked)),
Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_string_literal()?.cooked)),
Keyword::Shell => Some(Setting::Shell(self.parse_shell()?)),
Keyword::Tempdir => Some(Setting::Tempdir(self.parse_string_literal()?.cooked)),
Keyword::WindowsShell => Some(Setting::WindowsShell(self.parse_shell()?)),
Keyword::DotenvFilename => Some(Setting::DotenvFilename(self.parse_string_literal()?)),
Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_string_literal()?)),
Keyword::ScriptInterpreter => Some(Setting::ScriptInterpreter(self.parse_interpreter()?)),
Keyword::Shell => Some(Setting::Shell(self.parse_interpreter()?)),
Keyword::Tempdir => Some(Setting::Tempdir(self.parse_string_literal()?)),
Keyword::WindowsShell => Some(Setting::WindowsShell(self.parse_interpreter()?)),
_ => None,
};

Expand All @@ -978,8 +979,8 @@ impl<'run, 'src> Parser<'run, 'src> {
}))
}

/// Parse a shell setting value
fn parse_shell(&mut self) -> CompileResult<'src, Shell<'src>> {
/// Parse interpreter setting value, i.e., `['sh', '-eu']`
fn parse_interpreter(&mut self) -> CompileResult<'src, Interpreter<'src>> {
self.expect(BracketL)?;

let command = self.parse_string_literal()?;
Expand All @@ -998,7 +999,7 @@ impl<'run, 'src> Parser<'run, 'src> {

self.expect(BracketR)?;

Ok(Shell { arguments, command })
Ok(Interpreter { arguments, command })
}

/// Item attributes, i.e., `[macos]` or `[confirm: "warning!"]`
Expand Down
9 changes: 7 additions & 2 deletions src/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,12 +342,17 @@ impl<'src, D> Recipe<'src, D> {
return Ok(());
}

let executor = if let Some(Attribute::Script(args)) = self
let executor = if let Some(Attribute::Script(interpreter)) = self
.attributes
.iter()
.find(|attribute| matches!(attribute, Attribute::Script(_)))
{
Executor::Command(args.iter().map(|arg| arg.cooked.as_str()).collect())
Executor::Command(
interpreter
.as_ref()
.or(context.settings.script_interpreter.as_ref())
.unwrap_or_else(|| Interpreter::default_script_interpreter()),
)
} else {
let line = evaluated_lines
.first()
Expand Down
Loading