Add [extension: 'EXT']
attribute to set shebang recipe script file extension (#2256)
This commit is contained in:
parent
ea26e451fa
commit
a3693f3e8f
@ -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. |
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user