Add OS Configuration Attributes (#1387)

This commit is contained in:
Casey Rodarmor 2022-10-31 00:52:03 -07:00 committed by GitHub
parent c834fb1e4e
commit 73777f7183
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 342 additions and 100 deletions

View File

@ -1057,9 +1057,7 @@ Done!
#### System Information #### System Information
- `arch()` — Instruction set architecture. Possible values are: `"aarch64"`, `"arm"`, `"asmjs"`, `"hexagon"`, `"mips"`, `"msp430"`, `"powerpc"`, `"powerpc64"`, `"s390x"`, `"sparc"`, `"wasm32"`, `"x86"`, `"x86_64"`, and `"xcore"`. - `arch()` — Instruction set architecture. Possible values are: `"aarch64"`, `"arm"`, `"asmjs"`, `"hexagon"`, `"mips"`, `"msp430"`, `"powerpc"`, `"powerpc64"`, `"s390x"`, `"sparc"`, `"wasm32"`, `"x86"`, `"x86_64"`, and `"xcore"`.
- `os()` — Operating system. Possible values are: `"android"`, `"bitrig"`, `"dragonfly"`, `"emscripten"`, `"freebsd"`, `"haiku"`, `"ios"`, `"linux"`, `"macos"`, `"netbsd"`, `"openbsd"`, `"solaris"`, and `"windows"`. - `os()` — Operating system. Possible values are: `"android"`, `"bitrig"`, `"dragonfly"`, `"emscripten"`, `"freebsd"`, `"haiku"`, `"ios"`, `"linux"`, `"macos"`, `"netbsd"`, `"openbsd"`, `"solaris"`, and `"windows"`.
- `os_family()` — Operating system family; possible values are: `"unix"` and `"windows"`. - `os_family()` — Operating system family; possible values are: `"unix"` and `"windows"`.
For example: For example:
@ -1143,67 +1141,47 @@ The executable is at: /bin/just
#### String Manipulation #### String Manipulation
- `capitalize(s)`<sup>1.7.0</sup> - Convert first character of `s` to uppercase and the rest to lowercase.
- `kebabcase(s)`<sup>1.7.0</sup> - Convert `s` to `kebab-case`.
- `lowercamelcase(s)`<sup>1.7.0</sup> - Convert `s` to `lowerCamelCase`.
- `lowercase(s)` - Convert `s` to lowercase.
- `quote(s)` - Replace all single quotes with `'\''` and prepend and append single quotes to `s`. This is sufficient to escape special characters for many shells, including most Bourne shell descendants. - `quote(s)` - Replace all single quotes with `'\''` and prepend and append single quotes to `s`. This is sufficient to escape special characters for many shells, including most Bourne shell descendants.
- `replace(s, from, to)` - Replace all occurrences of `from` in `s` to `to`. - `replace(s, from, to)` - Replace all occurrences of `from` in `s` to `to`.
- `shoutykebabcase(s)`<sup>1.7.0</sup> - Convert `s` to `SHOUTY-KEBAB-CASE`.
- `shoutysnakecase(s)`<sup>1.7.0</sup> - Convert `s` to `SHOUTY_SNAKE_CASE`.
- `snakecase(s)`<sup>1.7.0</sup> - Convert `s` to `snake_case`.
- `titlecase(s)`<sup>1.7.0</sup> - Convert `s` to `Title Case`.
- `trim(s)` - Remove leading and trailing whitespace from `s`. - `trim(s)` - Remove leading and trailing whitespace from `s`.
- `trim_end(s)` - Remove trailing whitespace from `s`. - `trim_end(s)` - Remove trailing whitespace from `s`.
- `trim_end_match(s, pat)` - Remove suffix of `s` matching `pat`. - `trim_end_match(s, pat)` - Remove suffix of `s` matching `pat`.
- `trim_end_matches(s, pat)` - Repeatedly remove suffixes of `s` matching `pat`. - `trim_end_matches(s, pat)` - Repeatedly remove suffixes of `s` matching `pat`.
- `trim_start(s)` - Remove leading whitespace from `s`. - `trim_start(s)` - Remove leading whitespace from `s`.
- `trim_start_match(s, pat)` - Remove prefix of `s` matching `pat`. - `trim_start_match(s, pat)` - Remove prefix of `s` matching `pat`.
- `trim_start_matches(s, pat)` - Repeatedly remove prefixes of `s` matching `pat`. - `trim_start_matches(s, pat)` - Repeatedly remove prefixes of `s` matching `pat`.
#### Case Conversion
- `capitalize(s)`<sup>1.7.0</sup> - Convert first character of `s` to uppercase and the rest to lowercase.
- `kebabcase(s)`<sup>1.7.0</sup> - Convert `s` to `kebab-case`.
- `lowercamelcase(s)`<sup>1.7.0</sup> - Convert `s` to `lowerCamelCase`.
- `lowercase(s)` - Convert `s` to lowercase.
- `shoutykebabcase(s)`<sup>1.7.0</sup> - Convert `s` to `SHOUTY-KEBAB-CASE`.
- `shoutysnakecase(s)`<sup>1.7.0</sup> - Convert `s` to `SHOUTY_SNAKE_CASE`.
- `snakecase(s)`<sup>1.7.0</sup> - Convert `s` to `snake_case`.
- `titlecase(s)`<sup>1.7.0</sup> - Convert `s` to `Title Case`.
- `uppercamelcase(s)`<sup>1.7.0</sup> - Convert `s` to `UpperCamelCase`.
- `uppercase(s)` - Convert `s` to uppercase. - `uppercase(s)` - Convert `s` to uppercase.
- `uppercamelcase(s)`<sup>1.7.0</sup> - Convert `s` to `UpperCamelCase`.
#### Path Manipulation #### Path Manipulation
##### Fallible ##### Fallible
- `absolute_path(path)` - Absolute path to relative `path` in the working directory. `absolute_path("./bar.txt")` in directory `/foo` is `/foo/bar.txt`. - `absolute_path(path)` - Absolute path to relative `path` in the working directory. `absolute_path("./bar.txt")` in directory `/foo` is `/foo/bar.txt`.
- `extension(path)` - Extension of `path`. `extension("/foo/bar.txt")` is `txt`. - `extension(path)` - Extension of `path`. `extension("/foo/bar.txt")` is `txt`.
- `file_name(path)` - File name of `path` with any leading directory components removed. `file_name("/foo/bar.txt")` is `bar.txt`. - `file_name(path)` - File name of `path` with any leading directory components removed. `file_name("/foo/bar.txt")` is `bar.txt`.
- `file_stem(path)` - File name of `path` without extension. `file_stem("/foo/bar.txt")` is `bar`. - `file_stem(path)` - File name of `path` without extension. `file_stem("/foo/bar.txt")` is `bar`.
- `parent_directory(path)` - Parent directory of `path`. `parent_directory("/foo/bar.txt")` is `/foo`. - `parent_directory(path)` - Parent directory of `path`. `parent_directory("/foo/bar.txt")` is `/foo`.
- `without_extension(path)` - `path` without extension. `without_extension("/foo/bar.txt")` is `/foo/bar`. - `without_extension(path)` - `path` without extension. `without_extension("/foo/bar.txt")` is `/foo/bar`.
These functions can fail, for example if a path does not have an extension, which will halt execution. These functions can fail, for example if a path does not have an extension, which will halt execution.
##### Infallible ##### Infallible
- `join(a, b…)` - *This function uses `/` on Unix and `\` on Windows, which can be lead to unwanted behavior. The `/` operator, e.g., `a / b`, which always uses `/`, should be considered as a replacement unless `\`s are specifically desired on Windows.* Join path `a` with path `b`. `join("foo/bar", "baz")` is `foo/bar/baz`. Accepts two or more arguments.
- `clean(path)` - Simplify `path` by removing extra path separators, intermediate `.` components, and `..` where possible. `clean("foo//bar")` is `foo/bar`, `clean("foo/..")` is `.`, `clean("foo/./bar")` is `foo/bar`. - `clean(path)` - Simplify `path` by removing extra path separators, intermediate `.` components, and `..` where possible. `clean("foo//bar")` is `foo/bar`, `clean("foo/..")` is `.`, `clean("foo/./bar")` is `foo/bar`.
- `join(a, b…)` - *This function uses `/` on Unix and `\` on Windows, which can be lead to unwanted behavior. The `/` operator, e.g., `a / b`, which always uses `/`, should be considered as a replacement unless `\`s are specifically desired on Windows.* Join path `a` with path `b`. `join("foo/bar", "baz")` is `foo/bar/baz`. Accepts two or more arguments.
#### Filesystem Access #### Filesystem Access
@ -1219,6 +1197,42 @@ These functions can fail, for example if a path does not have an extension, whic
- `sha256_file(path)` - Return the SHA-256 hash of the file at `path` as a hexadecimal string. - `sha256_file(path)` - Return the SHA-256 hash of the file at `path` as a hexadecimal string.
- `uuid()` - Return a randomly generated UUID. - `uuid()` - Return a randomly generated UUID.
### Recipe Attributes
Recipes may be annotated with attributes that change their behavior.
| Name | Description |
| ------------------- | ------------------------------------------------- |
| `[no-exit-message]` | Don't print an error message when a recipe fails. |
| `[linux]` | Enable recipe on Linux. |
| `[macos]` | Enable recipe on MacOS. |
| `[unix]` | Enable recipe on Unixes. |
| `[windows]` | Enable recipe on Windows. |
#### Enabling and Disabling Recipes
The `[linux]`, `[macos]`, `[unix]`, and `[windows]` attributes are
configuration attributes. By default, recipes are always enabled. A recipe with
one or more configuration attributes will only be enabled when one or more of
those configurations is active.
This can be used to write `justfile`s that behave differently depending on
which operating system they run on. The `run` recipe in this `justfile` will
compile and run `main.c`, using a different C compiler and using the correct
output binary name for that compiler depending on the operating system:
```make
[unix]
run:
cc main.c
./a.out
[windows]
run:
cl main.c
main.exe
```
### Command Evaluation Using Backticks ### Command Evaluation Using Backticks
Backticks can be used to store the result of commands: Backticks can be used to store the result of commands:

View File

@ -30,9 +30,11 @@ impl<'src> Analyzer<'src> {
} }
Item::Comment(_) => (), Item::Comment(_) => (),
Item::Recipe(recipe) => { Item::Recipe(recipe) => {
if recipe.enabled() {
Self::analyze_recipe(&recipe)?; Self::analyze_recipe(&recipe)?;
recipes.push(recipe); recipes.push(recipe);
} }
}
Item::Set(set) => { Item::Set(set) => {
self.analyze_set(&set)?; self.analyze_set(&set)?;
self.sets.insert(set); self.sets.insert(set);

View File

@ -1,13 +1,34 @@
use super::*; use super::*;
#[derive(EnumString)] #[derive(
#[strum(serialize_all = "kebab_case")] EnumString, PartialEq, Debug, Copy, Clone, Serialize, Ord, PartialOrd, Eq, IntoStaticStr,
)]
#[strum(serialize_all = "kebab-case")]
#[serde(rename_all = "kebab-case")]
pub(crate) enum Attribute { pub(crate) enum Attribute {
Linux,
Macos,
NoExitMessage, NoExitMessage,
Unix,
Windows,
} }
impl Attribute { impl Attribute {
pub(crate) fn from_name(name: Name) -> Option<Attribute> { pub(crate) fn from_name(name: Name) -> Option<Attribute> {
name.lexeme().parse().ok() name.lexeme().parse().ok()
} }
pub(crate) fn to_str(self) -> &'static str {
self.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_str() {
assert_eq!(Attribute::NoExitMessage.to_str(), "no-exit-message");
}
} }

View File

@ -96,6 +96,15 @@ impl Display for CompileError<'_> {
self.token.line.ordinal(), self.token.line.ordinal(),
)?; )?;
} }
DuplicateAttribute { attribute, first } => {
write!(
f,
"Recipe attribute `{}` first used on line {} is duplicated on line {}",
attribute,
first.ordinal(),
self.token.line.ordinal(),
)?;
}
DuplicateParameter { recipe, parameter } => { DuplicateParameter { recipe, parameter } => {
write!( write!(
f, f,

View File

@ -25,6 +25,10 @@ pub(crate) enum CompileErrorKind<'src> {
alias: &'src str, alias: &'src str,
first: usize, first: usize,
}, },
DuplicateAttribute {
attribute: &'src str,
first: usize,
},
DuplicateParameter { DuplicateParameter {
recipe: &'src str, recipe: &'src str,
parameter: &'src str, parameter: &'src str,

View File

@ -35,7 +35,7 @@ pub(crate) enum Error<'src> {
recipe: &'src str, recipe: &'src str,
line_number: Option<usize>, line_number: Option<usize>,
code: i32, code: i32,
suppress_message: bool, print_message: bool,
}, },
CommandInvoke { CommandInvoke {
binary: OsString, binary: OsString,
@ -169,11 +169,11 @@ impl<'src> Error<'src> {
} }
} }
pub(crate) fn suppress_message(&self) -> bool { pub(crate) fn print_message(&self) -> bool {
matches!( !matches!(
self, self,
Error::Code { Error::Code {
suppress_message: true, print_message: false,
.. ..
} }
) )

View File

@ -472,13 +472,13 @@ mod tests {
recipe, recipe,
line_number, line_number,
code, code,
suppress_message, print_message,
}, },
check: { check: {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(line_number, None); assert_eq!(line_number, None);
assert!(!suppress_message); assert!(print_message);
} }
} }
@ -493,13 +493,13 @@ mod tests {
recipe, recipe,
line_number, line_number,
code, code,
suppress_message, print_message,
}, },
check: { check: {
assert_eq!(recipe, "fail"); assert_eq!(recipe, "fail");
assert_eq!(code, 100); assert_eq!(code, 100);
assert_eq!(line_number, Some(2)); assert_eq!(line_number, Some(2));
assert!(!suppress_message); assert!(print_message);
} }
} }
@ -514,13 +514,13 @@ mod tests {
recipe, recipe,
line_number, line_number,
code, code,
suppress_message, print_message,
}, },
check: { check: {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(code, 150); assert_eq!(code, 150);
assert_eq!(line_number, Some(2)); assert_eq!(line_number, Some(2));
assert!(!suppress_message); assert!(print_message);
} }
} }
@ -672,13 +672,13 @@ mod tests {
error: Code { error: Code {
recipe, recipe,
line_number, line_number,
suppress_message, print_message,
.. ..
}, },
check: { check: {
assert_eq!(recipe, "wut"); assert_eq!(recipe, "wut");
assert_eq!(line_number, Some(7)); assert_eq!(line_number, Some(7));
assert!(!suppress_message); assert!(print_message);
} }
} }

View File

@ -345,20 +345,25 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
items.push(Item::Assignment(self.parse_assignment(false)?)); items.push(Item::Assignment(self.parse_assignment(false)?));
} else { } else {
let doc = pop_doc_comment(&mut items, eol_since_last_comment); let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, false, false)?)); items.push(Item::Recipe(self.parse_recipe(
doc,
false,
BTreeSet::new(),
)?));
} }
} }
} }
} else if self.accepted(At)? { } else if self.accepted(At)? {
let doc = pop_doc_comment(&mut items, eol_since_last_comment); let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, true, false)?)); items.push(Item::Recipe(self.parse_recipe(
} else if self.accepted(BracketL)? { doc,
let Attribute::NoExitMessage = self.parse_attribute_name()?; true,
self.expect(BracketR)?; BTreeSet::new(),
self.expect_eol()?; )?));
} else if let Some(attributes) = self.parse_attributes()? {
let quiet = self.accepted(At)?; let quiet = self.accepted(At)?;
let doc = pop_doc_comment(&mut items, eol_since_last_comment); let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, quiet, true)?)); items.push(Item::Recipe(self.parse_recipe(doc, quiet, attributes)?));
} else { } else {
return Err(self.unexpected_token()?); return Err(self.unexpected_token()?);
} }
@ -602,7 +607,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
&mut self, &mut self,
doc: Option<&'src str>, doc: Option<&'src str>,
quiet: bool, quiet: bool,
suppress_exit_error_messages: bool, attributes: BTreeSet<Attribute>,
) -> CompileResult<'src, UnresolvedRecipe<'src>> { ) -> CompileResult<'src, UnresolvedRecipe<'src>> {
let name = self.parse_name()?; let name = self.parse_name()?;
@ -666,7 +671,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
parameters: positional.into_iter().chain(variadic).collect(), parameters: positional.into_iter().chain(variadic).collect(),
private: name.lexeme().starts_with('_'), private: name.lexeme().starts_with('_'),
shebang: body.first().map_or(false, Line::is_shebang), shebang: body.first().map_or(false, Line::is_shebang),
suppress_exit_error_messages, attributes,
priors, priors,
body, body,
dependencies, dependencies,
@ -831,14 +836,33 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
Ok(Shell { arguments, command }) Ok(Shell { arguments, command })
} }
/// Parse a recipe attribute name /// Parse recipe attributes
fn parse_attribute_name(&mut self) -> CompileResult<'src, Attribute> { fn parse_attributes(&mut self) -> CompileResult<'src, Option<BTreeSet<Attribute>>> {
let mut attributes = BTreeMap::new();
while self.accepted(BracketL)? {
let name = self.parse_name()?; let name = self.parse_name()?;
Attribute::from_name(name).ok_or_else(|| { let attribute = Attribute::from_name(name).ok_or_else(|| {
name.error(CompileErrorKind::UnknownAttribute { name.error(CompileErrorKind::UnknownAttribute {
attribute: name.lexeme(), attribute: name.lexeme(),
}) })
}) })?;
if let Some(line) = attributes.get(&attribute) {
return Err(name.error(CompileErrorKind::DuplicateAttribute {
attribute: name.lexeme(),
first: *line,
}));
}
attributes.insert(attribute, name.line);
self.expect(BracketR)?;
self.expect_eol()?;
}
if attributes.is_empty() {
Ok(None)
} else {
Ok(Some(attributes.into_keys().collect()))
}
} }
} }

View File

@ -21,6 +21,7 @@ fn error_from_signal(recipe: &str, line_number: Option<usize>, exit_status: Exit
/// A recipe, e.g. `foo: bar baz` /// A recipe, e.g. `foo: bar baz`
#[derive(PartialEq, Debug, Clone, Serialize)] #[derive(PartialEq, Debug, Clone, Serialize)]
pub(crate) struct Recipe<'src, D = Dependency<'src>> { pub(crate) struct Recipe<'src, D = Dependency<'src>> {
pub(crate) attributes: BTreeSet<Attribute>,
pub(crate) body: Vec<Line<'src>>, pub(crate) body: Vec<Line<'src>>,
pub(crate) dependencies: Vec<D>, pub(crate) dependencies: Vec<D>,
pub(crate) doc: Option<&'src str>, pub(crate) doc: Option<&'src str>,
@ -30,7 +31,6 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> {
pub(crate) private: bool, pub(crate) private: bool,
pub(crate) quiet: bool, pub(crate) quiet: bool,
pub(crate) shebang: bool, pub(crate) shebang: bool,
pub(crate) suppress_exit_error_messages: bool,
} }
impl<'src, D> Recipe<'src, D> { impl<'src, D> Recipe<'src, D> {
@ -66,6 +66,24 @@ impl<'src, D> Recipe<'src, D> {
!self.private !self.private
} }
pub(crate) fn enabled(&self) -> bool {
let windows = self.attributes.contains(&Attribute::Windows);
let linux = self.attributes.contains(&Attribute::Linux);
let macos = self.attributes.contains(&Attribute::Macos);
let unix = self.attributes.contains(&Attribute::Unix);
(!windows && !linux && !macos && !unix)
|| (cfg!(target_os = "windows") && windows)
|| (cfg!(target_os = "linux") && (linux || unix))
|| (cfg!(target_os = "macos") && (macos || unix))
|| (cfg!(windows) && windows)
|| (cfg!(unix) && unix)
}
fn print_exit_message(&self) -> bool {
!self.attributes.contains(&Attribute::NoExitMessage)
}
pub(crate) fn run<'run>( pub(crate) fn run<'run>(
&self, &self,
context: &RecipeContext<'src, 'run>, context: &RecipeContext<'src, 'run>,
@ -192,12 +210,11 @@ impl<'src, D> Recipe<'src, D> {
Ok(exit_status) => { Ok(exit_status) => {
if let Some(code) = exit_status.code() { if let Some(code) = exit_status.code() {
if code != 0 && !infallible_command { if code != 0 && !infallible_command {
let suppress_message = self.suppress_exit_error_messages;
return Err(Error::Code { return Err(Error::Code {
recipe: self.name(), recipe: self.name(),
line_number: Some(line_number), line_number: Some(line_number),
code, code,
suppress_message, print_message: self.print_exit_message(),
}); });
} }
} else { } else {
@ -327,12 +344,11 @@ impl<'src, D> Recipe<'src, D> {
if code == 0 { if code == 0 {
Ok(()) Ok(())
} else { } else {
let suppress_message = self.suppress_exit_error_messages;
Err(Error::Code { Err(Error::Code {
recipe: self.name(), recipe: self.name(),
line_number: None, line_number: None,
code, code,
suppress_message, print_message: self.print_exit_message(),
}) })
} }
}, },
@ -353,6 +369,10 @@ impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
writeln!(f, "# {}", doc)?; writeln!(f, "# {}", doc)?;
} }
for attribute in &self.attributes {
writeln!(f, "[{}]", attribute.to_str())?;
}
if self.quiet { if self.quiet {
write!(f, "@{}", self.name)?; write!(f, "@{}", self.name)?;
} else { } else {

View File

@ -29,7 +29,7 @@ pub fn run() -> Result<(), i32> {
config config
.and_then(|config| config.run(&loader)) .and_then(|config| config.run(&loader))
.map_err(|error| { .map_err(|error| {
if !verbosity.quiet() && !error.suppress_message() { if !verbosity.quiet() && error.print_message() {
eprintln!("{}", error.color_display(color.stderr())); eprintln!("{}", error.color_display(color.stderr()));
} }
error.code().unwrap_or(EXIT_FAILURE) error.code().unwrap_or(EXIT_FAILURE)

View File

@ -53,7 +53,7 @@ impl<'src> UnresolvedRecipe<'src> {
quiet: self.quiet, quiet: self.quiet,
shebang: self.shebang, shebang: self.shebang,
priors: self.priors, priors: self.priors,
suppress_exit_error_messages: self.suppress_exit_error_messages, attributes: self.attributes,
dependencies, dependencies,
}) })
} }

43
tests/attributes.rs Normal file
View File

@ -0,0 +1,43 @@
use super::*;
#[test]
fn all() {
Test::new()
.justfile(
"
[macos]
[windows]
[linux]
[unix]
[no-exit-message]
foo:
exit 1
",
)
.stderr("exit 1\n")
.status(1)
.run();
}
#[test]
fn duplicate_attributes_are_disallowed() {
Test::new()
.justfile(
"
[no-exit-message]
[no-exit-message]
foo:
echo bar
",
)
.stderr(
"
error: Recipe attribute `no-exit-message` first used on line 1 is duplicated on line 2
|
2 | [no-exit-message]
| ^^^^^^^^^^^^^^^
",
)
.status(1)
.run();
}

View File

@ -27,6 +27,7 @@ fn alias() {
"assignments": {}, "assignments": {},
"recipes": { "recipes": {
"foo": { "foo": {
"attributes": [],
"body": [], "body": [],
"dependencies": [], "dependencies": [],
"doc": null, "doc": null,
@ -36,7 +37,6 @@ fn alias() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false,
} }
}, },
"settings": { "settings": {
@ -102,6 +102,7 @@ fn body() {
"first": "foo", "first": "foo",
"recipes": { "recipes": {
"foo": { "foo": {
"attributes": [],
"body": [ "body": [
["bar"], ["bar"],
["abc", ["xyz"], "def"], ["abc", ["xyz"], "def"],
@ -114,7 +115,6 @@ fn body() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false,
} }
}, },
"settings": { "settings": {
@ -147,6 +147,7 @@ fn dependencies() {
"first": "foo", "first": "foo",
"recipes": { "recipes": {
"bar": { "bar": {
"attributes": [],
"doc": null, "doc": null,
"name": "bar", "name": "bar",
"body": [], "body": [],
@ -159,7 +160,6 @@ fn dependencies() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false,
}, },
"foo": { "foo": {
"body": [], "body": [],
@ -171,7 +171,7 @@ fn dependencies() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
} }
}, },
"settings": { "settings": {
@ -246,7 +246,7 @@ fn dependency_argument() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
}, },
"foo": { "foo": {
"body": [], "body": [],
@ -265,7 +265,7 @@ fn dependency_argument() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
} }
}, },
"settings": { "settings": {
@ -322,7 +322,7 @@ fn duplicate_recipes() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
} }
}, },
"settings": { "settings": {
@ -361,7 +361,7 @@ fn doc_comment() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
} }
}, },
"settings": { "settings": {
@ -424,6 +424,7 @@ fn parameters() {
"assignments": {}, "assignments": {},
"recipes": { "recipes": {
"a": { "a": {
"attributes": [],
"body": [], "body": [],
"dependencies": [], "dependencies": [],
"doc": null, "doc": null,
@ -433,7 +434,6 @@ fn parameters() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false,
}, },
"b": { "b": {
"body": [], "body": [],
@ -452,7 +452,7 @@ fn parameters() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
}, },
"c": { "c": {
"body": [], "body": [],
@ -471,7 +471,7 @@ fn parameters() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
}, },
"d": { "d": {
"body": [], "body": [],
@ -490,7 +490,7 @@ fn parameters() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
}, },
"e": { "e": {
"body": [], "body": [],
@ -509,7 +509,7 @@ fn parameters() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
}, },
"f": { "f": {
"body": [], "body": [],
@ -528,7 +528,7 @@ fn parameters() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
}, },
}, },
"settings": { "settings": {
@ -571,7 +571,7 @@ fn priors() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
}, },
"b": { "b": {
"body": [], "body": [],
@ -590,7 +590,7 @@ fn priors() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
"parameters": [], "parameters": [],
"priors": 1, "priors": 1,
}, },
@ -603,7 +603,7 @@ fn priors() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
"parameters": [], "parameters": [],
"priors": 0, "priors": 0,
}, },
@ -644,7 +644,7 @@ fn private() {
"private": true, "private": true,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
} }
}, },
"settings": { "settings": {
@ -683,7 +683,7 @@ fn quiet() {
"private": false, "private": false,
"quiet": true, "quiet": true,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
} }
}, },
"settings": { "settings": {
@ -741,7 +741,7 @@ fn settings() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": true, "shebang": true,
"suppress_exit_error_messages": false, "attributes": [],
} }
}, },
"settings": { "settings": {
@ -786,7 +786,7 @@ fn shebang() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": true, "shebang": true,
"suppress_exit_error_messages": false, "attributes": [],
} }
}, },
"settings": { "settings": {
@ -825,7 +825,7 @@ fn simple() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": false, "attributes": [],
} }
}, },
"settings": { "settings": {
@ -858,6 +858,7 @@ fn attribute() {
"first": "foo", "first": "foo",
"recipes": { "recipes": {
"foo": { "foo": {
"attributes": ["no-exit-message"],
"body": [], "body": [],
"dependencies": [], "dependencies": [],
"doc": null, "doc": null,
@ -867,7 +868,6 @@ fn attribute() {
"private": false, "private": false,
"quiet": false, "quiet": false,
"shebang": false, "shebang": false,
"suppress_exit_error_messages": true,
} }
}, },
"settings": { "settings": {

View File

@ -33,6 +33,7 @@ mod test;
mod allow_duplicate_recipes; mod allow_duplicate_recipes;
mod assert_stdout; mod assert_stdout;
mod assert_success; mod assert_success;
mod attributes;
mod byte_order_mark; mod byte_order_mark;
mod changelog; mod changelog;
mod choose; mod choose;
@ -60,6 +61,7 @@ mod line_prefixes;
mod misc; mod misc;
mod multibyte_char; mod multibyte_char;
mod no_exit_message; mod no_exit_message;
mod os_attributes;
mod parser; mod parser;
mod positional_arguments; mod positional_arguments;
mod quiet; mod quiet;

View File

@ -1,4 +1,4 @@
use libc::EXIT_FAILURE; use super::*;
test! { test! {
name: recipe_exit_message_suppressed, name: recipe_exit_message_suppressed,
@ -86,7 +86,7 @@ hello:
@exit 100 @exit 100
"#, "#,
stderr: r#" stderr: r#"
error: Expected '@' or identifier, but found comment error: Expected '@', '[', or identifier, but found comment
| |
2 | # This is a doc comment 2 | # This is a doc comment
| ^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^
@ -103,7 +103,7 @@ test! {
hello: hello:
@exit 100 @exit 100
"#, "#,
stderr: "error: Expected '@' or identifier, but found end of line\n |\n2 | \n | ^\n", stderr: "error: Expected '@', '[', or identifier, but found end of line\n |\n2 | \n | ^\n",
status: EXIT_FAILURE, status: EXIT_FAILURE,
} }

103
tests/os_attributes.rs Normal file
View File

@ -0,0 +1,103 @@
use super::*;
#[test]
fn os_family() {
Test::new()
.justfile(
"
[unix]
foo:
echo bar
[windows]
foo:
echo baz
",
)
.stdout(if cfg!(unix) {
"bar\n"
} else if cfg!(windows) {
"baz\n"
} else {
panic!("unexpected os family")
})
.stderr(if cfg!(unix) {
"echo bar\n"
} else if cfg!(windows) {
"echo baz\n"
} else {
panic!("unexpected os family")
})
.run();
}
#[test]
fn os() {
Test::new()
.justfile(
"
[macos]
foo:
echo bar
[windows]
foo:
echo baz
[linux]
foo:
echo quxx
",
)
.stdout(if cfg!(target_os = "macos") {
"bar\n"
} else if cfg!(windows) {
"baz\n"
} else if cfg!(target_os = "linux") {
"quxx\n"
} else {
panic!("unexpected os family")
})
.stderr(if cfg!(target_os = "macos") {
"echo bar\n"
} else if cfg!(windows) {
"echo baz\n"
} else if cfg!(target_os = "linux") {
"echo quxx\n"
} else {
panic!("unexpected os family")
})
.run();
}
#[test]
fn all() {
Test::new()
.justfile(
"
[macos]
[windows]
[linux]
[unix]
foo:
echo bar
",
)
.stdout("bar\n")
.stderr("echo bar\n")
.run();
}
#[test]
fn none() {
Test::new()
.justfile(
"
foo:
echo bar
",
)
.stdout("bar\n")
.stderr("echo bar\n")
.run();
}