1
1
mirror of https://github.com/casey/just.git synced 2024-11-23 02:44:56 +03:00

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

This commit is contained in:
Casey Rodarmor 2024-07-18 23:02:22 -07:00 committed by GitHub
parent 0cd38464f1
commit 14489c0376
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 224 additions and 149 deletions

1
Cargo.lock generated
View File

@ -612,6 +612,7 @@ dependencies = [
"libc",
"log",
"num_cpus",
"once_cell",
"percent-encoding",
"pretty_assertions",
"rand",

View File

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

View File

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

View File

@ -20,7 +20,7 @@ pub(crate) enum Attribute<'src> {
NoQuiet,
PositionalArguments,
Private,
Script(Vec<StringLiteral<'src>>),
Script(Option<Interpreter<'src>>),
Unix,
Windows,
}
@ -39,7 +39,7 @@ impl AttributeDiscriminant {
| Self::Private
| Self::Unix
| Self::Windows => 0..=0,
Self::Script => 1..=usize::MAX,
Self::Script => 0..=usize::MAX,
}
}
}
@ -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,
})
@ -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
@ -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(())

View File

@ -1,7 +1,7 @@
use super::*;
pub(crate) enum Executor<'a> {
Command(Vec<&'a str>),
Command(&'a Interpreter<'a>),
Shebang(Shebang<'a>),
}
@ -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);
@ -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(),
};
@ -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 {
@ -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
);
}

29
src/interpreter.rs Normal file
View File

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

View File

@ -21,6 +21,7 @@ pub(crate) enum Keyword {
Mod,
PositionalArguments,
Quiet,
ScriptInterpreter,
Set,
Shell,
Tempdir,

View File

@ -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,
@ -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},
@ -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,
@ -155,6 +156,7 @@ mod executor;
mod expression;
mod fragment;
mod function;
mod interpreter;
mod interrupt_guard;
mod interrupt_handler;
mod item;
@ -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;

View File

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

View File

@ -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,
};
@ -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()?;
@ -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!"]`

View File

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

View File

@ -4,20 +4,21 @@ use super::*;
pub(crate) enum Setting<'src> {
AllowDuplicateRecipes(bool),
AllowDuplicateVariables(bool),
DotenvFilename(String),
DotenvFilename(StringLiteral<'src>),
DotenvLoad(bool),
DotenvPath(String),
DotenvPath(StringLiteral<'src>),
DotenvRequired(bool),
Export(bool),
Fallback(bool),
IgnoreComments(bool),
PositionalArguments(bool),
Quiet(bool),
Shell(Shell<'src>),
Tempdir(String),
ScriptInterpreter(Interpreter<'src>),
Shell(Interpreter<'src>),
Tempdir(StringLiteral<'src>),
Unstable(bool),
WindowsPowerShell(bool),
WindowsShell(Shell<'src>),
WindowsShell(Interpreter<'src>),
}
impl<'src> Display for Setting<'src> {
@ -34,9 +35,11 @@ impl<'src> Display for Setting<'src> {
| Self::Quiet(value)
| Self::Unstable(value)
| Self::WindowsPowerShell(value) => write!(f, "{value}"),
Self::Shell(shell) | Self::WindowsShell(shell) => write!(f, "{shell}"),
Self::ScriptInterpreter(shell) | Self::Shell(shell) | Self::WindowsShell(shell) => {
write!(f, "[{shell}]")
}
Self::DotenvFilename(value) | Self::DotenvPath(value) | Self::Tempdir(value) => {
write!(f, "{value:?}")
write!(f, "{value}")
}
}
}

View File

@ -18,11 +18,13 @@ pub(crate) struct Settings<'src> {
pub(crate) ignore_comments: bool,
pub(crate) positional_arguments: bool,
pub(crate) quiet: bool,
pub(crate) shell: Option<Shell<'src>>,
#[serde(skip)]
pub(crate) script_interpreter: Option<Interpreter<'src>>,
pub(crate) shell: Option<Interpreter<'src>>,
pub(crate) tempdir: Option<String>,
pub(crate) unstable: bool,
pub(crate) windows_powershell: bool,
pub(crate) windows_shell: Option<Shell<'src>>,
pub(crate) windows_shell: Option<Interpreter<'src>>,
}
impl<'src> Settings<'src> {
@ -38,13 +40,13 @@ impl<'src> Settings<'src> {
settings.allow_duplicate_variables = allow_duplicate_variables;
}
Setting::DotenvFilename(filename) => {
settings.dotenv_filename = Some(filename);
settings.dotenv_filename = Some(filename.cooked);
}
Setting::DotenvLoad(dotenv_load) => {
settings.dotenv_load = dotenv_load;
}
Setting::DotenvPath(path) => {
settings.dotenv_path = Some(PathBuf::from(path));
settings.dotenv_path = Some(PathBuf::from(path.cooked));
}
Setting::DotenvRequired(dotenv_required) => {
settings.dotenv_required = dotenv_required;
@ -64,6 +66,9 @@ impl<'src> Settings<'src> {
Setting::Quiet(quiet) => {
settings.quiet = quiet;
}
Setting::ScriptInterpreter(script_interpreter) => {
settings.script_interpreter = Some(script_interpreter);
}
Setting::Shell(shell) => {
settings.shell = Some(shell);
}
@ -77,7 +82,7 @@ impl<'src> Settings<'src> {
settings.windows_shell = Some(windows_shell);
}
Setting::Tempdir(tempdir) => {
settings.tempdir = Some(tempdir);
settings.tempdir = Some(tempdir.cooked);
}
}
}
@ -204,7 +209,7 @@ mod tests {
#[test]
fn shell_cooked() {
let settings = Settings {
shell: Some(Shell {
shell: Some(Interpreter {
command: StringLiteral {
kind: StringKind::from_token_start("\"").unwrap(),
raw: "asdf.exe",

View File

@ -1,19 +0,0 @@
use super::*;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub(crate) struct Shell<'src> {
pub(crate) arguments: Vec<StringLiteral<'src>>,
pub(crate) command: StringLiteral<'src>,
}
impl<'src> Display for Shell<'src> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "[{}", self.command)?;
for argument in &self.arguments {
write!(f, ", {argument}")?;
}
write!(f, "]")
}
}

6
src/string_delimiter.rs Normal file
View File

@ -0,0 +1,6 @@
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
pub(crate) enum StringDelimiter {
Backtick,
QuoteDouble,
QuoteSingle,
}

View File

@ -2,15 +2,8 @@ use super::*;
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
pub(crate) struct StringKind {
delimiter: StringDelimiter,
indented: bool,
}
#[derive(Debug, PartialEq, Clone, Copy, Ord, PartialOrd, Eq)]
enum StringDelimiter {
Backtick,
QuoteDouble,
QuoteSingle,
pub(crate) delimiter: StringDelimiter,
pub(crate) indented: bool,
}
impl StringKind {

View File

@ -8,7 +8,21 @@ pub(crate) struct StringLiteral<'src> {
pub(crate) raw: &'src str,
}
impl Display for StringLiteral<'_> {
impl<'src> StringLiteral<'src> {
pub(crate) fn from_raw(raw: &'src str) -> Self {
Self {
cooked: raw.into(),
expand: false,
kind: StringKind {
delimiter: StringDelimiter::QuoteSingle,
indented: false,
},
raw,
}
}
}
impl<'src> Display for StringLiteral<'src> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if self.expand {
write!(f, "x")?;

View File

@ -4,6 +4,7 @@ use super::*;
pub(crate) enum UnstableFeature {
FormatSubcommand,
ScriptAttribute,
ScriptInterpreterSetting,
}
impl Display for UnstableFeature {
@ -11,6 +12,9 @@ impl Display for UnstableFeature {
match self {
Self::FormatSubcommand => write!(f, "The `--fmt` command is currently unstable."),
Self::ScriptAttribute => write!(f, "The `[script]` attribute is currently unstable."),
Self::ScriptInterpreterSetting => {
write!(f, "The `script-interpreter` setting is currently unstable.")
}
}
}
}

View File

@ -8,7 +8,6 @@ fn unstable() {
[script('sh', '-u')]
foo:
echo FOO
",
)
.stderr_regex(r"error: The `\[script\]` attribute is currently unstable\..*")
@ -16,6 +15,15 @@ fn unstable() {
.run();
}
#[test]
fn script_interpreter_setting_is_unstable() {
Test::new()
.justfile("set script-interpreter := ['sh']")
.status(EXIT_FAILURE)
.stderr_regex(r"error: The `script-interpreter` setting is currently unstable\..*")
.run();
}
#[test]
fn runs_with_command() {
Test::new()
@ -73,30 +81,6 @@ fn with_arguments() {
.run();
}
#[test]
fn requires_argument() {
Test::new()
.justfile(
"
set unstable
[script]
foo:
",
)
.stderr(
"
error: Attribute `script` got 0 arguments but takes at least 1 argument
justfile:3:2
3 [script]
^^^^^^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn not_allowed_with_shebang() {
Test::new()
@ -298,3 +282,53 @@ c
)
.run();
}
#[test]
fn no_arguments_with_default_script_interpreter() {
Test::new()
.justfile(
"
set unstable
[script]
foo:
case $- in
*e*) echo '-e is set';;
esac
case $- in
*u*) echo '-u is set';;
esac
",
)
.stdout(
"
-e is set
-u is set
",
)
.run();
}
#[test]
fn no_arguments_with_non_default_script_interpreter() {
Test::new()
.justfile(
"
set unstable
set script-interpreter := ['sh']
[script]
foo:
case $- in
*e*) echo '-e is set';;
esac
case $- in
*u*) echo '-u is set';;
esac
",
)
.run();
}