Add [extension: 'EXT'] attribute to set shebang recipe script file extension (#2256)

This commit is contained in:
Casey Rodarmor 2024-07-15 13:08:28 -07:00 committed by GitHub
parent ea26e451fa
commit a3693f3e8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 125 additions and 32 deletions

View File

@ -1712,6 +1712,7 @@ Recipes may be annotated with attributes that change their behavior.
| `[confirm]`<sup>1.17.0</sup> | Require confirmation prior to executing recipe. | | `[confirm]`<sup>1.17.0</sup> | Require confirmation prior to executing recipe. |
| `[confirm('PROMPT')]`<sup>1.23.0</sup> | Require confirmation prior to executing recipe with a custom prompt. | | `[confirm('PROMPT')]`<sup>1.23.0</sup> | Require confirmation prior to executing recipe with a custom prompt. |
| `[doc('DOC')]`<sup>1.27.0</sup> | Set recipe's [documentation comment](#documentation-comments) to `DOC`. | | `[doc('DOC')]`<sup>1.27.0</sup> | Set recipe's [documentation comment](#documentation-comments) to `DOC`. |
| `[extension('EXT')]`<sup>master</sup> | Set shebang recipe script's file extension to `EXT`. `EXT` should include a period if one is desired. |
| `[group('NAME')]`<sup>1.27.0</sup> | Put recipe in [recipe group](#recipe-groups) `NAME`. | | `[group('NAME')]`<sup>1.27.0</sup> | Put recipe in [recipe group](#recipe-groups) `NAME`. |
| `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. | | `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. |
| `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. | | `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. |

View File

@ -255,6 +255,20 @@ impl<'src> Analyzer<'src> {
continued = line.is_continuation(); continued = line.is_continuation();
} }
if !recipe.shebang {
if let Some(attribute) = recipe
.attributes
.iter()
.find(|attribute| matches!(attribute, Attribute::Extension(_)))
{
return Err(recipe.name.error(InvalidAttribute {
item_kind: "Recipe",
item_name: recipe.name.lexeme(),
attribute: attribute.clone(),
}));
}
}
Ok(()) Ok(())
} }

View File

@ -11,6 +11,7 @@ use super::*;
pub(crate) enum Attribute<'src> { pub(crate) enum Attribute<'src> {
Confirm(Option<StringLiteral<'src>>), Confirm(Option<StringLiteral<'src>>),
Doc(Option<StringLiteral<'src>>), Doc(Option<StringLiteral<'src>>),
Extension(StringLiteral<'src>),
Group(StringLiteral<'src>), Group(StringLiteral<'src>),
Linux, Linux,
Macos, Macos,
@ -27,7 +28,7 @@ impl AttributeDiscriminant {
fn argument_range(self) -> RangeInclusive<usize> { fn argument_range(self) -> RangeInclusive<usize> {
match self { match self {
Self::Confirm | Self::Doc => 0..=1, Self::Confirm | Self::Doc => 0..=1,
Self::Group => 1..=1, Self::Group | Self::Extension => 1..=1,
Self::Linux Self::Linux
| Self::Macos | Self::Macos
| Self::NoCd | Self::NoCd
@ -46,8 +47,6 @@ impl<'src> Attribute<'src> {
name: Name<'src>, name: Name<'src>,
argument: Option<StringLiteral<'src>>, argument: Option<StringLiteral<'src>>,
) -> CompileResult<'src, Self> { ) -> CompileResult<'src, Self> {
use AttributeDiscriminant::*;
let discriminant = name let discriminant = name
.lexeme() .lexeme()
.parse::<AttributeDiscriminant>() .parse::<AttributeDiscriminant>()
@ -72,18 +71,19 @@ impl<'src> Attribute<'src> {
} }
Ok(match discriminant { Ok(match discriminant {
Confirm => Self::Confirm(argument), AttributeDiscriminant::Confirm => Self::Confirm(argument),
Doc => Self::Doc(argument), AttributeDiscriminant::Doc => Self::Doc(argument),
Group => Self::Group(argument.unwrap()), AttributeDiscriminant::Extension => Self::Extension(argument.unwrap()),
Linux => Self::Linux, AttributeDiscriminant::Group => Self::Group(argument.unwrap()),
Macos => Self::Macos, AttributeDiscriminant::Linux => Self::Linux,
NoCd => Self::NoCd, AttributeDiscriminant::Macos => Self::Macos,
NoExitMessage => Self::NoExitMessage, AttributeDiscriminant::NoCd => Self::NoCd,
NoQuiet => Self::NoQuiet, AttributeDiscriminant::NoExitMessage => Self::NoExitMessage,
PositionalArguments => Self::PositionalArguments, AttributeDiscriminant::NoQuiet => Self::NoQuiet,
Private => Self::Private, AttributeDiscriminant::PositionalArguments => Self::PositionalArguments,
Unix => Self::Unix, AttributeDiscriminant::Private => Self::Private,
Windows => Self::Windows, AttributeDiscriminant::Unix => Self::Unix,
AttributeDiscriminant::Windows => Self::Windows,
}) })
} }
@ -93,9 +93,8 @@ impl<'src> Attribute<'src> {
fn argument(&self) -> Option<&StringLiteral> { fn argument(&self) -> Option<&StringLiteral> {
match self { match self {
Self::Confirm(prompt) => prompt.as_ref(), Self::Confirm(argument) | Self::Doc(argument) => argument.as_ref(),
Self::Doc(doc) => doc.as_ref(), Self::Extension(argument) | Self::Group(argument) => Some(argument),
Self::Group(group) => Some(group),
Self::Linux Self::Linux
| Self::Macos | Self::Macos
| Self::NoCd | Self::NoCd

View File

@ -368,7 +368,16 @@ impl<'src, D> Recipe<'src, D> {
io_error: error, io_error: error,
})?; })?;
let mut path = tempdir.path().to_path_buf(); let mut path = tempdir.path().to_path_buf();
path.push(shebang.script_filename(self.name()));
let extension = self.attributes.iter().find_map(|attribute| {
if let Attribute::Extension(extension) = attribute {
Some(extension.cooked.as_str())
} else {
None
}
});
path.push(shebang.script_filename(self.name(), extension));
{ {
let mut f = fs::File::create(&path).map_err(|error| Error::TempdirIo { let mut f = fs::File::create(&path).map_err(|error| Error::TempdirIo {

View File

@ -38,12 +38,14 @@ impl<'line> Shebang<'line> {
.unwrap_or(self.interpreter) .unwrap_or(self.interpreter)
} }
pub(crate) fn script_filename(&self, recipe: &str) -> String { pub(crate) fn script_filename(&self, recipe: &str, extension: Option<&str>) -> String {
match self.interpreter_filename() { let extension = extension.unwrap_or_else(|| match self.interpreter_filename() {
"cmd" | "cmd.exe" => format!("{recipe}.bat"), "cmd" | "cmd.exe" => ".bat",
"powershell" | "powershell.exe" | "pwsh" | "pwsh.exe" => format!("{recipe}.ps1"), "powershell" | "powershell.exe" | "pwsh" | "pwsh.exe" => ".ps1",
_ => recipe.to_owned(), _ => "",
} });
format!("{recipe}{extension}")
} }
pub(crate) fn include_shebang_line(&self) -> bool { pub(crate) fn include_shebang_line(&self) -> bool {
@ -138,7 +140,9 @@ mod tests {
#[test] #[test]
fn powershell_script_filename() { fn powershell_script_filename() {
assert_eq!( assert_eq!(
Shebang::new("#!powershell").unwrap().script_filename("foo"), Shebang::new("#!powershell")
.unwrap()
.script_filename("foo", None),
"foo.ps1" "foo.ps1"
); );
} }
@ -146,7 +150,7 @@ mod tests {
#[test] #[test]
fn pwsh_script_filename() { fn pwsh_script_filename() {
assert_eq!( assert_eq!(
Shebang::new("#!pwsh").unwrap().script_filename("foo"), Shebang::new("#!pwsh").unwrap().script_filename("foo", None),
"foo.ps1" "foo.ps1"
); );
} }
@ -156,7 +160,7 @@ mod tests {
assert_eq!( assert_eq!(
Shebang::new("#!powershell.exe") Shebang::new("#!powershell.exe")
.unwrap() .unwrap()
.script_filename("foo"), .script_filename("foo", None),
"foo.ps1" "foo.ps1"
); );
} }
@ -164,7 +168,9 @@ mod tests {
#[test] #[test]
fn pwsh_exe_script_filename() { fn pwsh_exe_script_filename() {
assert_eq!( assert_eq!(
Shebang::new("#!pwsh.exe").unwrap().script_filename("foo"), Shebang::new("#!pwsh.exe")
.unwrap()
.script_filename("foo", None),
"foo.ps1" "foo.ps1"
); );
} }
@ -172,7 +178,7 @@ mod tests {
#[test] #[test]
fn cmd_script_filename() { fn cmd_script_filename() {
assert_eq!( assert_eq!(
Shebang::new("#!cmd").unwrap().script_filename("foo"), Shebang::new("#!cmd").unwrap().script_filename("foo", None),
"foo.bat" "foo.bat"
); );
} }
@ -180,14 +186,19 @@ mod tests {
#[test] #[test]
fn cmd_exe_script_filename() { fn cmd_exe_script_filename() {
assert_eq!( assert_eq!(
Shebang::new("#!cmd.exe").unwrap().script_filename("foo"), Shebang::new("#!cmd.exe")
.unwrap()
.script_filename("foo", None),
"foo.bat" "foo.bat"
); );
} }
#[test] #[test]
fn plain_script_filename() { fn plain_script_filename() {
assert_eq!(Shebang::new("#!bar").unwrap().script_filename("foo"), "foo"); assert_eq!(
Shebang::new("#!bar").unwrap().script_filename("foo", None),
"foo"
);
} }
#[test] #[test]
@ -211,4 +222,26 @@ mod tests {
fn include_shebang_line_other_windows() { fn include_shebang_line_other_windows() {
assert!(!Shebang::new("#!foo -c").unwrap().include_shebang_line()); assert!(!Shebang::new("#!foo -c").unwrap().include_shebang_line());
} }
#[test]
fn filename_with_extension() {
assert_eq!(
Shebang::new("#!bar")
.unwrap()
.script_filename("foo", Some(".sh")),
"foo.sh"
);
assert_eq!(
Shebang::new("#!pwsh.exe")
.unwrap()
.script_filename("foo", Some(".sh")),
"foo.sh"
);
assert_eq!(
Shebang::new("#!cmd.exe")
.unwrap()
.script_filename("foo", Some(".sh")),
"foo.sh"
);
}
} }

View File

@ -193,3 +193,40 @@ fn doc_multiline() {
) )
.run(); .run();
} }
#[test]
fn extension() {
Test::new()
.justfile(
"
[extension: '.txt']
baz:
#!/bin/sh
echo $0
",
)
.stdout_regex(r"*baz\.txt\n")
.run();
}
#[test]
fn extension_on_linewise_error() {
Test::new()
.justfile(
"
[extension: '.txt']
baz:
",
)
.stderr(
"
error: Recipe `baz` has invalid attribute `extension`
justfile:2:1
2 baz:
^^^
",
)
.status(EXIT_FAILURE)
.run();
}