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

Add [script] attribute #2259

Merged
merged 23 commits into from
Jul 18, 2024
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Yay, all your tests passed!
[available for most popular shells](#shell-completion-scripts).

- Recipes can be written in
[arbitrary languages](#writing-recipes-in-other-languages), like Python or NodeJS.
[arbitrary languages](#shebang-recipes), like Python or NodeJS.

- `just` can be invoked from any subdirectory, not just the directory that
contains the `justfile`.
Expand Down Expand Up @@ -1721,6 +1721,7 @@ Recipes may be annotated with attributes that change their behavior.
| `[no-quiet]`<sup>1.23.0</sup> | Override globally quiet recipes and always echo out the recipe. |
| `[positional-arguments]`<sup>1.29.0</sup> | Turn on [positional arguments](#positional-arguments) for this recipe. |
| `[private]`<sup>1.10.0</sup> | See [Private Recipes](#private-recipes). |
| `[script(COMMAND)]`<sup>master</sup> | Execute recipe as a script interpreted by `COMMAND`. See [script recipes](#script-recipes) for more details. |
| `[unix]`<sup>1.8.0</sup> | Enable recipe on Unixes. (Includes MacOS). |
| `[windows]`<sup>1.8.0</sup> | Enable recipe on Windows. |

Expand Down Expand Up @@ -2443,7 +2444,7 @@ This has limitations, since recipe `c` is run with an entirely new invocation
of `just`: Assignments will be recalculated, dependencies might run twice, and
command line arguments will not be propagated to the child `just` process.

### Writing Recipes in Other Languages
### Shebang Recipes

Recipes that start with `#!` are called shebang recipes, and are executed by
saving the recipe body to a file and running it. This lets you write recipes in
Expand Down Expand Up @@ -2514,6 +2515,20 @@ the final argument. For example, on Windows, if a recipe starts with `#! py`,
the final command the OS runs will be something like `py
C:\Temp\PATH_TO_SAVED_RECIPE_BODY`.

### Script Recipes

Recipes with a `[script(COMMAND)]` attribute<sup>master</sup> are run as
scripts interpreted by `COMMAND`. This avoids some of the issues with shebang
recipes, such as the use of `cygpath` on Windows, the need to use
`/usr/bin/env`, and inconsistences in shebang line splitting across Unix OSs.

The body of the recipe is evaluated, written to disk in the temporary
directory, and run by passing its path as an argument to `COMMAND`.

The `[script(…)]` attribute is unstable, so you'll need to use `set unstable`,
set the `JUST_UNSTABLE` environment variable, or pass `--unstable` on the
command line.

### Safer Bash Shebang Recipes

If you're writing a `bash` shebang recipe, consider adding `set -euxo
Expand Down
18 changes: 15 additions & 3 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,18 @@ impl<'src> Analyzer<'src> {

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

let unstable_features = recipes
.values()
.flat_map(|recipe| &recipe.attributes)
.filter_map(|attribute| {
if let Attribute::Script(_) = attribute {
Some(UnstableFeature::ScriptAttribute)
} else {
None
}
})
.collect();

Ok(Justfile {
aliases,
assignments: self.assignments,
Expand All @@ -208,7 +220,7 @@ impl<'src> Analyzer<'src> {
settings,
source: root.into(),
unexports,
unstable_features: BTreeSet::new(),
unstable_features,
warnings,
})
}
Expand Down Expand Up @@ -242,7 +254,7 @@ impl<'src> Analyzer<'src> {

let mut continued = false;
for line in &recipe.body {
if !recipe.shebang && !continued {
if !recipe.is_script() && !continued {
if let Some(Fragment::Text { token }) = line.fragments.first() {
let text = token.lexeme();

Expand All @@ -255,7 +267,7 @@ impl<'src> Analyzer<'src> {
continued = line.is_continuation();
}

if !recipe.shebang {
if !recipe.is_script() {
if let Some(attribute) = recipe
.attributes
.iter()
Expand Down
47 changes: 34 additions & 13 deletions src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(crate) enum Attribute<'src> {
NoQuiet,
PositionalArguments,
Private,
Script(Vec<StringLiteral<'src>>),
Unix,
Windows,
}
Expand All @@ -38,14 +39,15 @@ impl AttributeDiscriminant {
| Self::Private
| Self::Unix
| Self::Windows => 0..=0,
Self::Script => 1..=usize::MAX,
}
}
}

impl<'src> Attribute<'src> {
pub(crate) fn new(
name: Name<'src>,
argument: Option<StringLiteral<'src>>,
arguments: Vec<StringLiteral<'src>>,
) -> CompileResult<'src, Self> {
let discriminant = name
.lexeme()
Expand All @@ -57,7 +59,7 @@ impl<'src> Attribute<'src> {
})
})?;

let found = argument.as_ref().iter().count();
let found = arguments.len();
let range = discriminant.argument_range();
if !range.contains(&found) {
return Err(
Expand All @@ -71,17 +73,18 @@ impl<'src> Attribute<'src> {
}

Ok(match discriminant {
AttributeDiscriminant::Confirm => Self::Confirm(argument),
AttributeDiscriminant::Doc => Self::Doc(argument),
AttributeDiscriminant::Extension => Self::Extension(argument.unwrap()),
AttributeDiscriminant::Group => Self::Group(argument.unwrap()),
AttributeDiscriminant::Confirm => Self::Confirm(arguments.into_iter().next()),
AttributeDiscriminant::Doc => Self::Doc(arguments.into_iter().next()),
AttributeDiscriminant::Extension => Self::Extension(arguments.into_iter().next().unwrap()),
AttributeDiscriminant::Group => Self::Group(arguments.into_iter().next().unwrap()),
AttributeDiscriminant::Linux => Self::Linux,
AttributeDiscriminant::Macos => Self::Macos,
AttributeDiscriminant::NoCd => Self::NoCd,
AttributeDiscriminant::NoExitMessage => Self::NoExitMessage,
AttributeDiscriminant::NoQuiet => Self::NoQuiet,
AttributeDiscriminant::PositionalArguments => Self::PositionalArguments,
AttributeDiscriminant::Private => Self::Private,
AttributeDiscriminant::Script => Self::Script(arguments),
AttributeDiscriminant::Unix => Self::Unix,
AttributeDiscriminant::Windows => Self::Windows,
})
Expand All @@ -91,28 +94,46 @@ impl<'src> Attribute<'src> {
self.into()
}

fn argument(&self) -> Option<&StringLiteral> {
fn arguments(&self) -> &[StringLiteral] {
match self {
Self::Confirm(argument) | Self::Doc(argument) => argument.as_ref(),
Self::Extension(argument) | Self::Group(argument) => Some(argument),
Self::Linux
Self::Confirm(Some(argument))
| Self::Doc(Some(argument))
| Self::Extension(argument)
| Self::Group(argument) => slice::from_ref(argument),
Self::Script(arguments) => arguments,
Self::Confirm(None)
| Self::Doc(None)
| Self::Linux
| Self::Macos
| Self::NoCd
| Self::NoExitMessage
| Self::NoQuiet
| Self::PositionalArguments
| Self::Private
| Self::Unix
| Self::Windows => None,
| Self::Windows => &[],
}
}
}

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

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

Ok(())
Expand Down
4 changes: 4 additions & 0 deletions src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ impl Display for CompileError<'_> {
)
}
}
ShebangAndScriptAttribute { recipe } => write!(
f,
"Recipe `{recipe}` has both shebang line and `[script]` attribute"
),
ShellExpansion { err } => write!(f, "Shell expansion failed: {err}"),
RequiredParameterFollowsDefaultParameter { parameter } => write!(
f,
Expand Down
3 changes: 3 additions & 0 deletions src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ pub(crate) enum CompileErrorKind<'src> {
RequiredParameterFollowsDefaultParameter {
parameter: &'src str,
},
ShebangAndScriptAttribute {
recipe: &'src str,
},
ShellExpansion {
err: shellexpand::LookupError<env::VarError>,
},
Expand Down
12 changes: 10 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,19 @@ pub(crate) enum Error<'src> {
RegexCompile {
source: regex::Error,
},
Script {
command: String,
io_error: io::Error,
recipe: &'src str,
},
Search {
search_error: SearchError,
},
Shebang {
recipe: &'src str,
command: String,
argument: Option<String>,
command: String,
io_error: io::Error,
recipe: &'src str,
},
Signal {
recipe: &'src str,
Expand Down Expand Up @@ -413,6 +418,9 @@ impl<'src> ColorDisplay for Error<'src> {
RuntimeDirIo { io_error, path } => {
write!(f, "I/O error in runtime dir `{}`: {io_error}", path.display())?;
}
Script { command, io_error, recipe } => {
write!(f, "Recipe `{recipe}` with command `{command}` execution error: {io_error}")?;
}
Search { search_error } => Display::fmt(search_error, f)?,
Shebang { recipe, command, argument, io_error} => {
if let Some(argument) = argument {
Expand Down
Loading