diff --git a/README.md b/README.md index 767b682..a03bbce 100644 --- a/README.md +++ b/README.md @@ -1057,9 +1057,7 @@ Done! #### 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"`. - - `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"`. For example: @@ -1143,67 +1141,47 @@ The executable is at: /bin/just #### String Manipulation -- `capitalize(s)`1.7.0 - Convert first character of `s` to uppercase and the rest to lowercase. - -- `kebabcase(s)`1.7.0 - Convert `s` to `kebab-case`. - -- `lowercamelcase(s)`1.7.0 - 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. - - `replace(s, from, to)` - Replace all occurrences of `from` in `s` to `to`. - -- `shoutykebabcase(s)`1.7.0 - Convert `s` to `SHOUTY-KEBAB-CASE`. - -- `shoutysnakecase(s)`1.7.0 - Convert `s` to `SHOUTY_SNAKE_CASE`. - -- `snakecase(s)`1.7.0 - Convert `s` to `snake_case`. - -- `titlecase(s)`1.7.0 - Convert `s` to `Title Case`. - - `trim(s)` - Remove leading and 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_matches(s, pat)` - Repeatedly remove suffixes of `s` matching `pat`. - - `trim_start(s)` - Remove leading whitespace from `s`. - - `trim_start_match(s, pat)` - Remove prefix of `s` matching `pat`. - - `trim_start_matches(s, pat)` - Repeatedly remove prefixes of `s` matching `pat`. +#### Case Conversion + +- `capitalize(s)`1.7.0 - Convert first character of `s` to uppercase and the rest to lowercase. +- `kebabcase(s)`1.7.0 - Convert `s` to `kebab-case`. +- `lowercamelcase(s)`1.7.0 - Convert `s` to `lowerCamelCase`. +- `lowercase(s)` - Convert `s` to lowercase. +- `shoutykebabcase(s)`1.7.0 - Convert `s` to `SHOUTY-KEBAB-CASE`. +- `shoutysnakecase(s)`1.7.0 - Convert `s` to `SHOUTY_SNAKE_CASE`. +- `snakecase(s)`1.7.0 - Convert `s` to `snake_case`. +- `titlecase(s)`1.7.0 - Convert `s` to `Title Case`. +- `uppercamelcase(s)`1.7.0 - Convert `s` to `UpperCamelCase`. - `uppercase(s)` - Convert `s` to uppercase. -- `uppercamelcase(s)`1.7.0 - Convert `s` to `UpperCamelCase`. #### Path Manipulation ##### Fallible - `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`. - - `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`. - - `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`. These functions can fail, for example if a path does not have an extension, which will halt execution. ##### 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`. +- `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 @@ -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. - `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 Backticks can be used to store the result of commands: diff --git a/src/analyzer.rs b/src/analyzer.rs index a57c0e5..237b674 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -30,8 +30,10 @@ impl<'src> Analyzer<'src> { } Item::Comment(_) => (), Item::Recipe(recipe) => { - Self::analyze_recipe(&recipe)?; - recipes.push(recipe); + if recipe.enabled() { + Self::analyze_recipe(&recipe)?; + recipes.push(recipe); + } } Item::Set(set) => { self.analyze_set(&set)?; diff --git a/src/attribute.rs b/src/attribute.rs index 1256b54..375f2bb 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -1,13 +1,34 @@ use super::*; -#[derive(EnumString)] -#[strum(serialize_all = "kebab_case")] +#[derive( + EnumString, PartialEq, Debug, Copy, Clone, Serialize, Ord, PartialOrd, Eq, IntoStaticStr, +)] +#[strum(serialize_all = "kebab-case")] +#[serde(rename_all = "kebab-case")] pub(crate) enum Attribute { + Linux, + Macos, NoExitMessage, + Unix, + Windows, } impl Attribute { pub(crate) fn from_name(name: Name) -> Option { 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"); + } } diff --git a/src/compile_error.rs b/src/compile_error.rs index 1709985..abdb934 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -96,6 +96,15 @@ impl Display for CompileError<'_> { 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 } => { write!( f, diff --git a/src/compile_error_kind.rs b/src/compile_error_kind.rs index 3d63f4a..a6cc5f0 100644 --- a/src/compile_error_kind.rs +++ b/src/compile_error_kind.rs @@ -25,6 +25,10 @@ pub(crate) enum CompileErrorKind<'src> { alias: &'src str, first: usize, }, + DuplicateAttribute { + attribute: &'src str, + first: usize, + }, DuplicateParameter { recipe: &'src str, parameter: &'src str, diff --git a/src/error.rs b/src/error.rs index 682f00d..886cedf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -35,7 +35,7 @@ pub(crate) enum Error<'src> { recipe: &'src str, line_number: Option, code: i32, - suppress_message: bool, + print_message: bool, }, CommandInvoke { binary: OsString, @@ -169,11 +169,11 @@ impl<'src> Error<'src> { } } - pub(crate) fn suppress_message(&self) -> bool { - matches!( + pub(crate) fn print_message(&self) -> bool { + !matches!( self, Error::Code { - suppress_message: true, + print_message: false, .. } ) diff --git a/src/justfile.rs b/src/justfile.rs index 77c02b9..15d4c5b 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -472,13 +472,13 @@ mod tests { recipe, line_number, code, - suppress_message, + print_message, }, check: { assert_eq!(recipe, "a"); assert_eq!(code, 200); assert_eq!(line_number, None); - assert!(!suppress_message); + assert!(print_message); } } @@ -493,13 +493,13 @@ mod tests { recipe, line_number, code, - suppress_message, + print_message, }, check: { assert_eq!(recipe, "fail"); assert_eq!(code, 100); assert_eq!(line_number, Some(2)); - assert!(!suppress_message); + assert!(print_message); } } @@ -514,13 +514,13 @@ mod tests { recipe, line_number, code, - suppress_message, + print_message, }, check: { assert_eq!(recipe, "a"); assert_eq!(code, 150); assert_eq!(line_number, Some(2)); - assert!(!suppress_message); + assert!(print_message); } } @@ -672,13 +672,13 @@ mod tests { error: Code { recipe, line_number, - suppress_message, + print_message, .. }, check: { assert_eq!(recipe, "wut"); assert_eq!(line_number, Some(7)); - assert!(!suppress_message); + assert!(print_message); } } diff --git a/src/parser.rs b/src/parser.rs index 291ed3d..f7c7e2f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -345,20 +345,25 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { items.push(Item::Assignment(self.parse_assignment(false)?)); } else { 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)? { let doc = pop_doc_comment(&mut items, eol_since_last_comment); - items.push(Item::Recipe(self.parse_recipe(doc, true, false)?)); - } else if self.accepted(BracketL)? { - let Attribute::NoExitMessage = self.parse_attribute_name()?; - self.expect(BracketR)?; - self.expect_eol()?; + items.push(Item::Recipe(self.parse_recipe( + doc, + true, + BTreeSet::new(), + )?)); + } else if let Some(attributes) = self.parse_attributes()? { let quiet = self.accepted(At)?; 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 { return Err(self.unexpected_token()?); } @@ -602,7 +607,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { &mut self, doc: Option<&'src str>, quiet: bool, - suppress_exit_error_messages: bool, + attributes: BTreeSet, ) -> CompileResult<'src, UnresolvedRecipe<'src>> { let name = self.parse_name()?; @@ -666,7 +671,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { parameters: positional.into_iter().chain(variadic).collect(), private: name.lexeme().starts_with('_'), shebang: body.first().map_or(false, Line::is_shebang), - suppress_exit_error_messages, + attributes, priors, body, dependencies, @@ -831,14 +836,33 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { Ok(Shell { arguments, command }) } - /// Parse a recipe attribute name - fn parse_attribute_name(&mut self) -> CompileResult<'src, Attribute> { - let name = self.parse_name()?; - Attribute::from_name(name).ok_or_else(|| { - name.error(CompileErrorKind::UnknownAttribute { - attribute: name.lexeme(), - }) - }) + /// Parse recipe attributes + fn parse_attributes(&mut self) -> CompileResult<'src, Option>> { + let mut attributes = BTreeMap::new(); + + while self.accepted(BracketL)? { + let name = self.parse_name()?; + let attribute = Attribute::from_name(name).ok_or_else(|| { + name.error(CompileErrorKind::UnknownAttribute { + 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())) + } } } diff --git a/src/recipe.rs b/src/recipe.rs index 10baf48..7da2a61 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -21,6 +21,7 @@ fn error_from_signal(recipe: &str, line_number: Option, exit_status: Exit /// A recipe, e.g. `foo: bar baz` #[derive(PartialEq, Debug, Clone, Serialize)] pub(crate) struct Recipe<'src, D = Dependency<'src>> { + pub(crate) attributes: BTreeSet, pub(crate) body: Vec>, pub(crate) dependencies: Vec, pub(crate) doc: Option<&'src str>, @@ -30,7 +31,6 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> { pub(crate) private: bool, pub(crate) quiet: bool, pub(crate) shebang: bool, - pub(crate) suppress_exit_error_messages: bool, } impl<'src, D> Recipe<'src, D> { @@ -66,6 +66,24 @@ impl<'src, D> Recipe<'src, D> { !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>( &self, context: &RecipeContext<'src, 'run>, @@ -192,12 +210,11 @@ impl<'src, D> Recipe<'src, D> { Ok(exit_status) => { if let Some(code) = exit_status.code() { if code != 0 && !infallible_command { - let suppress_message = self.suppress_exit_error_messages; return Err(Error::Code { recipe: self.name(), line_number: Some(line_number), code, - suppress_message, + print_message: self.print_exit_message(), }); } } else { @@ -327,12 +344,11 @@ impl<'src, D> Recipe<'src, D> { if code == 0 { Ok(()) } else { - let suppress_message = self.suppress_exit_error_messages; Err(Error::Code { recipe: self.name(), line_number: None, 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)?; } + for attribute in &self.attributes { + writeln!(f, "[{}]", attribute.to_str())?; + } + if self.quiet { write!(f, "@{}", self.name)?; } else { diff --git a/src/run.rs b/src/run.rs index 9989d80..950fa0b 100644 --- a/src/run.rs +++ b/src/run.rs @@ -29,7 +29,7 @@ pub fn run() -> Result<(), i32> { config .and_then(|config| config.run(&loader)) .map_err(|error| { - if !verbosity.quiet() && !error.suppress_message() { + if !verbosity.quiet() && error.print_message() { eprintln!("{}", error.color_display(color.stderr())); } error.code().unwrap_or(EXIT_FAILURE) diff --git a/src/unresolved_recipe.rs b/src/unresolved_recipe.rs index faa1717..0a49443 100644 --- a/src/unresolved_recipe.rs +++ b/src/unresolved_recipe.rs @@ -53,7 +53,7 @@ impl<'src> UnresolvedRecipe<'src> { quiet: self.quiet, shebang: self.shebang, priors: self.priors, - suppress_exit_error_messages: self.suppress_exit_error_messages, + attributes: self.attributes, dependencies, }) } diff --git a/tests/attributes.rs b/tests/attributes.rs new file mode 100644 index 0000000..993e9ee --- /dev/null +++ b/tests/attributes.rs @@ -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(); +} diff --git a/tests/json.rs b/tests/json.rs index 72c0acc..9f5eef6 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -27,6 +27,7 @@ fn alias() { "assignments": {}, "recipes": { "foo": { + "attributes": [], "body": [], "dependencies": [], "doc": null, @@ -36,7 +37,6 @@ fn alias() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, } }, "settings": { @@ -102,6 +102,7 @@ fn body() { "first": "foo", "recipes": { "foo": { + "attributes": [], "body": [ ["bar"], ["abc", ["xyz"], "def"], @@ -114,7 +115,6 @@ fn body() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, } }, "settings": { @@ -147,6 +147,7 @@ fn dependencies() { "first": "foo", "recipes": { "bar": { + "attributes": [], "doc": null, "name": "bar", "body": [], @@ -159,7 +160,6 @@ fn dependencies() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, }, "foo": { "body": [], @@ -171,7 +171,7 @@ fn dependencies() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], } }, "settings": { @@ -246,7 +246,7 @@ fn dependency_argument() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], }, "foo": { "body": [], @@ -265,7 +265,7 @@ fn dependency_argument() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], } }, "settings": { @@ -322,7 +322,7 @@ fn duplicate_recipes() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], } }, "settings": { @@ -361,7 +361,7 @@ fn doc_comment() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], } }, "settings": { @@ -424,6 +424,7 @@ fn parameters() { "assignments": {}, "recipes": { "a": { + "attributes": [], "body": [], "dependencies": [], "doc": null, @@ -433,7 +434,6 @@ fn parameters() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, }, "b": { "body": [], @@ -452,7 +452,7 @@ fn parameters() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], }, "c": { "body": [], @@ -471,7 +471,7 @@ fn parameters() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], }, "d": { "body": [], @@ -490,7 +490,7 @@ fn parameters() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], }, "e": { "body": [], @@ -509,7 +509,7 @@ fn parameters() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], }, "f": { "body": [], @@ -528,7 +528,7 @@ fn parameters() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], }, }, "settings": { @@ -571,7 +571,7 @@ fn priors() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], }, "b": { "body": [], @@ -590,7 +590,7 @@ fn priors() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], "parameters": [], "priors": 1, }, @@ -603,7 +603,7 @@ fn priors() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], "parameters": [], "priors": 0, }, @@ -644,7 +644,7 @@ fn private() { "private": true, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], } }, "settings": { @@ -683,7 +683,7 @@ fn quiet() { "private": false, "quiet": true, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], } }, "settings": { @@ -741,7 +741,7 @@ fn settings() { "private": false, "quiet": false, "shebang": true, - "suppress_exit_error_messages": false, + "attributes": [], } }, "settings": { @@ -786,7 +786,7 @@ fn shebang() { "private": false, "quiet": false, "shebang": true, - "suppress_exit_error_messages": false, + "attributes": [], } }, "settings": { @@ -825,7 +825,7 @@ fn simple() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": false, + "attributes": [], } }, "settings": { @@ -858,6 +858,7 @@ fn attribute() { "first": "foo", "recipes": { "foo": { + "attributes": ["no-exit-message"], "body": [], "dependencies": [], "doc": null, @@ -867,7 +868,6 @@ fn attribute() { "private": false, "quiet": false, "shebang": false, - "suppress_exit_error_messages": true, } }, "settings": { diff --git a/tests/lib.rs b/tests/lib.rs index 3eb692b..7a999fe 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -33,6 +33,7 @@ mod test; mod allow_duplicate_recipes; mod assert_stdout; mod assert_success; +mod attributes; mod byte_order_mark; mod changelog; mod choose; @@ -60,6 +61,7 @@ mod line_prefixes; mod misc; mod multibyte_char; mod no_exit_message; +mod os_attributes; mod parser; mod positional_arguments; mod quiet; diff --git a/tests/no_exit_message.rs b/tests/no_exit_message.rs index 0752302..e3abb4a 100644 --- a/tests/no_exit_message.rs +++ b/tests/no_exit_message.rs @@ -1,4 +1,4 @@ -use libc::EXIT_FAILURE; +use super::*; test! { name: recipe_exit_message_suppressed, @@ -86,7 +86,7 @@ hello: @exit 100 "#, stderr: r#" -error: Expected '@' or identifier, but found comment +error: Expected '@', '[', or identifier, but found comment | 2 | # This is a doc comment | ^^^^^^^^^^^^^^^^^^^^^^^ @@ -103,7 +103,7 @@ test! { hello: @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, } diff --git a/tests/os_attributes.rs b/tests/os_attributes.rs new file mode 100644 index 0000000..8172985 --- /dev/null +++ b/tests/os_attributes.rs @@ -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(); +}