Skip to content

Commit

Permalink
Add OS Configuration Attributes (#1387)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Oct 31, 2022
1 parent c834fb1 commit 73777f7
Show file tree
Hide file tree
Showing 16 changed files with 342 additions and 100 deletions.
82 changes: 48 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -1143,67 +1141,47 @@ The executable is at: /bin/just

#### 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.

- `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_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`.

- `uppercase(s)` - Convert `s` to uppercase.
#### 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.


#### 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

Expand All @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
25 changes: 23 additions & 2 deletions src/attribute.rs
Original file line number Diff line number Diff line change
@@ -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<Attribute> {
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");
}
}
9 changes: 9 additions & 0 deletions src/compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/compile_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub(crate) enum Error<'src> {
recipe: &'src str,
line_number: Option<usize>,
code: i32,
suppress_message: bool,
print_message: bool,
},
CommandInvoke {
binary: OsString,
Expand Down Expand Up @@ -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,
..
}
)
Expand Down
16 changes: 8 additions & 8 deletions src/justfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand Down
58 changes: 41 additions & 17 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?);
}
Expand Down Expand Up @@ -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<Attribute>,
) -> CompileResult<'src, UnresolvedRecipe<'src>> {
let name = self.parse_name()?;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<BTreeSet<Attribute>>> {
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()))
}
}
}

Expand Down
Loading

0 comments on commit 73777f7

Please sign in to comment.