Compare commits

...

7 Commits

Author SHA1 Message Date
Greg Shuflin
ba89f94e7e Use groups in justfile 2024-07-16 11:27:29 -07:00
Casey Rodarmor
a3693f3e8f
Add [extension: 'EXT'] attribute to set shebang recipe script file extension (#2256) 2024-07-15 13:08:28 -07:00
Casey Rodarmor
ea26e451fa
Suppress mod doc comment with empty [doc] attribute (#2254) 2024-07-15 16:27:48 +00:00
Greg Shuflin
d5ebc9515e
Allow [doc] annotation on modules (#2247) 2024-07-14 22:15:22 -07:00
Casey Rodarmor
023b126eb2
Release 1.31.0 (#2251)
- Bump version: 1.30.1 → 1.31.0
- Update changelog
- Update changelog contributor credits
- Update dependencies
- Update version references in readme
2024-07-14 21:29:13 +00:00
Casey Rodarmor
687007a723
Stabilize modules (#2250) 2024-07-14 21:22:03 +00:00
Casey Rodarmor
6747c79082
Print space before submodules in --list with groups (#2244) 2024-07-14 02:20:35 +00:00
33 changed files with 518 additions and 350 deletions

View File

@ -1,6 +1,23 @@
Changelog
=========
[1.31.0](https://github.com/casey/just/releases/tag/1.31.0) - 2024-07-14
------------------------------------------------------------------------
### Stabilized
- Stabilize modules ([#2250](https://github.com/casey/just/pull/2250) by [casey](https://github.com/casey))
### Added
- Allow `mod` path to be directory containing module source ([#2238](https://github.com/casey/just/pull/2238) by [casey](https://github.com/casey))
- Allow enabling unstable features with `set unstable` ([#2237](https://github.com/casey/just/pull/2237) by [casey](https://github.com/casey))
- Allow abbreviating functions ending in `_directory` to `_dir` ([#2235](https://github.com/casey/just/pull/2235) by [casey](https://github.com/casey))
### Fixed
- Lexiclean search directory so `..` does not check the current directory ([#2236](https://github.com/casey/just/pull/2236) by [casey](https://github.com/casey))
### Misc
- Print space before submodules in `--list` with groups ([#2244](https://github.com/casey/just/pull/2244) by [casey](https://github.com/casey))
[1.30.1](https://github.com/casey/just/releases/tag/1.30.1) - 2024-07-06
------------------------------------------------------------------------

78
Cargo.lock generated
View File

@ -127,9 +127,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "blake3"
version = "1.5.1"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52"
checksum = "3d08263faac5cde2a4d52b513dadb80846023aade56fcd8fc99ba73ba8050e92"
dependencies = [
"arrayref",
"arrayvec",
@ -137,7 +137,7 @@ dependencies = [
"cfg-if",
"constant_time_eq",
"memmap2",
"rayon",
"rayon-core",
]
[[package]]
@ -174,9 +174,9 @@ checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
[[package]]
name = "cc"
version = "1.0.104"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490"
checksum = "9711f33475c22aab363b05564a17d7b789bf3dfec5ebabb586adee56f0e271b5"
[[package]]
name = "cfg-if"
@ -221,9 +221,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.8"
version = "4.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d"
checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
dependencies = [
"clap_builder",
"clap_derive",
@ -231,9 +231,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.8"
version = "4.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708"
checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942"
dependencies = [
"anstream",
"anstyle",
@ -244,11 +244,11 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.7"
version = "4.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d598e88f6874d4b888ed40c71efbcbf4076f1dfbae128a08a8c9e45f710605d"
checksum = "5b4be9c4c4b1f30b78d8a750e0822b6a6102d97e62061c583a6c1dea2dfb33ae"
dependencies = [
"clap 4.5.8",
"clap 4.5.9",
]
[[package]]
@ -260,7 +260,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.69",
"syn 2.0.71",
]
[[package]]
@ -275,7 +275,7 @@ version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f50dde5bc0c853d6248de457e5eb6e5a674a54b93810a34ded88d882ca1fe2de"
dependencies = [
"clap 4.5.8",
"clap 4.5.9",
"roff",
]
@ -591,13 +591,13 @@ dependencies = [
[[package]]
name = "just"
version = "1.30.1"
version = "1.31.0"
dependencies = [
"ansi_term",
"blake3",
"camino",
"chrono",
"clap 4.5.8",
"clap 4.5.9",
"clap_complete",
"clap_mangen",
"ctrlc",
@ -846,16 +846,6 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
@ -975,7 +965,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.69",
"syn 2.0.71",
]
[[package]]
@ -1021,23 +1011,23 @@ dependencies = [
[[package]]
name = "snafu"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418b8136fec49956eba89be7da2847ec1909df92a9ae4178b5ff0ff092c8d95e"
checksum = "2b835cb902660db3415a672d862905e791e54d306c6e8189168c7f3d9ae1c79d"
dependencies = [
"snafu-derive",
]
[[package]]
name = "snafu-derive"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a4812a669da00d17d8266a0439eddcacbc88b17f732f927e52eeb9d196f7fb5"
checksum = "38d1e02fca405f6280643174a50c942219f0bbf4dbf7d480f1dd864d6f211ae5"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.69",
"syn 2.0.71",
]
[[package]]
@ -1095,7 +1085,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.69",
"syn 2.0.71",
]
[[package]]
@ -1111,9 +1101,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.69"
version = "2.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6"
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
dependencies = [
"proc-macro2",
"quote",
@ -1168,22 +1158,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.61"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.69",
"syn 2.0.71",
]
[[package]]
@ -1240,9 +1230,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.9.1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [
"getrandom",
]
@ -1286,7 +1276,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.69",
"syn 2.0.71",
"wasm-bindgen-shared",
]
@ -1308,7 +1298,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.69",
"syn 2.0.71",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]

View File

@ -1,6 +1,6 @@
[package]
name = "just"
version = "1.30.1"
version = "1.31.0"
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
autotests = false
categories = ["command-line-utilities", "development-tools"]

View File

@ -667,10 +667,10 @@ $ cat foo.just
mod bar
$ cat bar.just
baz:
$ just --unstable foo bar
$ just foo bar
Available recipes:
baz
$ just --unstable foo::bar
$ just foo::bar
Available recipes:
baz
```
@ -820,7 +820,7 @@ foo:
| `positional-arguments` | boolean | `false` | Pass positional arguments. |
| `shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. |
| `tempdir` | string | - | Create temporary directories in `tempdir` instead of the system default temporary directory. |
| `unstable`<sup>master</sup> | boolean | `false` | Enable unstable features. |
| `unstable`<sup>1.31.0</sup> | boolean | `false` | Enable unstable features. |
| `windows-powershell` | boolean | `false` | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead. |
| `windows-shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. |
@ -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('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`. |
| `[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`. |
| `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. |
| `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. |
@ -3154,9 +3155,11 @@ Missing source files for optional imports do not produce an error.
### Modules<sup>1.19.0</sup>
A `justfile` can declare modules using `mod` statements. `mod` statements are
currently unstable, so you'll need to use the `--unstable` flag,
`set unstable`, or set the `JUST_UNSTABLE` environment variable to use them.
A `justfile` can declare modules using `mod` statements.
`mod` statements were stabilized in `just`<sup>1.31.0</sup>. In earlier
versions, you'll need to use the `--unstable` flag, `set unstable`, or set the
`JUST_UNSTABLE` environment variable to use them.
If you have the following `justfile`:
@ -3181,14 +3184,14 @@ uses its own settings.
Recipes in submodules can be invoked as subcommands:
```sh
$ just --unstable bar b
$ just bar b
B
```
Or with path syntax:
```sh
$ just --unstable bar::b
$ just bar::b
B
```

View File

@ -13,16 +13,24 @@ export JUST_LOG := log
watch +args='test':
cargo watch --clear --exec '{{ args }}'
[group: "testing"]
[doc: "Run just test suite"]
test:
cargo test
ci: build-book
ci: lint build-book
cargo test --all
# Run lint checks
[group: "code-checking"]
lint:
cargo clippy --all --all-targets -- --deny warnings
cargo fmt --all -- --check
./bin/forbid
cargo fmt --all -- --check
cargo update --locked --package just
[group: "testing"]
[doc: "Run fuzz tests"]
fuzz:
cargo +nightly fuzz run fuzz-compiler
@ -30,33 +38,46 @@ run:
cargo run
# only run tests matching PATTERN
[group: "testing"]
filter PATTERN:
cargo test {{PATTERN}}
# Build just
[group: "developer-workflow"]
build:
cargo build
[group: "code-checking"]
fmt:
cargo fmt --all
[group: "code-checking"]
shellcheck:
shellcheck www/install.sh
# Generate the just manpage
[group: "documentation"]
man:
mkdir -p man
cargo run -- --man > man/just.1
# View the Just manpage
[group: "documentation"]
view-man: man
man man/just.1
# add git log messages to changelog
[group: "developer-workflow"]
update-changelog:
echo >> CHANGELOG.md
git log --pretty='format:- %s' >> CHANGELOG.md
# Update the contributors file
[group: "developer-workflow"]
update-contributors:
cargo run --release --package update-contributors
[group: "code-checking"]
check: fmt clippy test forbid
#!/usr/bin/env bash
set -euxo pipefail
@ -64,7 +85,11 @@ check: fmt clippy test forbid
VERSION=`sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1`
grep "^\[$VERSION\]" CHANGELOG.md
outdated:
cargo outdated -R
# publish current GitHub master branch
[group: "developer-workflow"]
publish:
#!/usr/bin/env bash
set -euxo pipefail
@ -79,13 +104,19 @@ publish:
cd ../..
rm -rf tmp/release
[group: "documentation"]
[doc: "Search for master version note subscripts in the README"]
readme-version-notes:
grep '<sup>master</sup>' README.md
[group: "developer-workflow"]
[doc: "Push to GitHub"]
push: check
! git branch | grep '* master'
git push github
[group: "developer-workflow"]
[doc: "Create a pull request"]
pr: push
gh pr create --web
@ -111,6 +142,7 @@ install-dev-deps:
cargo install mdbook mdbook-linkcheck
# everyone's favorite animate paper clip
[group: "code-checking"]
clippy:
cargo clippy --all --all-targets --all-features
@ -118,16 +150,19 @@ forbid:
./bin/forbid
# count non-empty lines of code
[group: "developer-workflow"]
sloc:
@cat src/*.rs | sed '/^\s*$/d' | wc -l
replace FROM TO:
sd '{{FROM}}' '{{TO}}' src/*.rs
[group: "demo-recipes"]
test-quine:
cargo run -- quine
# make a quine, compile it, and verify it
[group: "demo-recipes"]
quine:
mkdir -p tmp
@echo '{{quine-text}}' > tmp/gen0.c
@ -155,19 +190,24 @@ quine-text := '
}
'
[group: "documentation"]
render-readme:
#!/usr/bin/env ruby
require 'github/markup'
$rendered = GitHub::Markup.render("README.adoc", File.read("README.adoc"))
File.write('tmp/README.html', $rendered)
[group: "documentation"]
watch-readme:
just render-readme
fswatch -ro README.adoc | xargs -n1 -I{} just render-readme
# Test shell completions
[group: "testing"]
test-completions:
./tests/completions/just.bash
[group: "documentation"]
build-book:
cargo run --package generate-book
mdbook build book/en
@ -185,6 +225,7 @@ convert-integration-test test:
-e 's/\.run\(\)/.run();/'
# run all polyglot recipes
[group: "demo-recipes"]
polyglot: _python _js _perl _sh _ruby
_python:
@ -214,9 +255,11 @@ _ruby:
puts "Hello from ruby!"
# Print working directory, for demonstration purposes!
[group: "demo-recipes"]
pwd:
echo {{invocation_directory()}}
[group: "testing"]
test-bash-completions:
rm -rf tmp
mkdir -p tmp/bin

View File

@ -10,7 +10,7 @@ pub(crate) struct Analyzer<'src> {
impl<'src> Analyzer<'src> {
pub(crate) fn analyze(
asts: &HashMap<PathBuf, Ast<'src>>,
doc: Option<&'src str>,
doc: Option<String>,
loaded: &[PathBuf],
name: Option<Name<'src>>,
paths: &HashMap<PathBuf, PathBuf>,
@ -22,7 +22,7 @@ impl<'src> Analyzer<'src> {
fn justfile(
mut self,
asts: &HashMap<PathBuf, Ast<'src>>,
doc: Option<&'src str>,
doc: Option<String>,
loaded: &[PathBuf],
name: Option<Name<'src>>,
paths: &HashMap<PathBuf, PathBuf>,
@ -37,8 +37,6 @@ impl<'src> Analyzer<'src> {
let mut warnings = Vec::new();
let mut unstable = BTreeSet::new();
let mut modules: Table<Justfile> = Table::new();
let mut unexports: HashSet<String> = HashSet::new();
@ -92,15 +90,27 @@ impl<'src> Analyzer<'src> {
absolute,
name,
doc,
attributes,
..
} => {
unstable.insert(Unstable::Modules);
let mut doc_attr: Option<&str> = None;
for attribute in attributes {
if let Attribute::Doc(ref doc) = attribute {
doc_attr = Some(doc.as_ref().map(|s| s.cooked.as_ref()).unwrap_or_default());
} else {
return Err(name.token.error(InvalidAttribute {
item_kind: "Module",
item_name: name.lexeme(),
attribute: attribute.clone(),
}));
}
}
if let Some(absolute) = absolute {
define(*name, "module", false)?;
modules.insert(Self::analyze(
asts,
*doc,
doc_attr.or(*doc).map(ToOwned::to_owned),
loaded,
Some(*name),
paths,
@ -198,7 +208,7 @@ impl<'src> Analyzer<'src> {
settings,
source: root.into(),
unexports,
unstable,
unstable_features: BTreeSet::new(),
warnings,
})
}
@ -245,16 +255,29 @@ impl<'src> Analyzer<'src> {
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(())
}
fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> {
let name = alias.name.lexeme();
for attribute in &alias.attributes {
if *attribute != Attribute::Private {
return Err(alias.name.token.error(AliasInvalidAttribute {
alias: name,
return Err(alias.name.token.error(InvalidAttribute {
item_kind: "Alias",
item_name: alias.name.lexeme(),
attribute: attribute.clone(),
}));
}

View File

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

View File

@ -32,13 +32,6 @@ impl Display for CompileError<'_> {
use CompileErrorKind::*;
match &*self.kind {
AliasInvalidAttribute { alias, attribute } => {
write!(
f,
"Alias `{alias}` has invalid attribute `{}`",
attribute.name(),
)
}
AliasShadowsRecipe { alias, recipe_line } => write!(
f,
"Alias `{alias}` defined on line {} shadows recipe `{alias}` defined on line {}",
@ -150,6 +143,9 @@ impl Display for CompileError<'_> {
write!(f, "Variable {variable} is both exported and unexported")
}
ExtraLeadingWhitespace => write!(f, "Recipe line has extra leading whitespace"),
ExtraneousAttributes { count } => {
write!(f, "Extraneous {}", Count("attribute", *count))
}
FunctionArgumentCountMismatch {
function,
found,
@ -176,6 +172,15 @@ impl Display for CompileError<'_> {
"Internal error, this may indicate a bug in just: {message}\n\
consider filing an issue: https://github.com/casey/just/issues/new"
),
InvalidAttribute {
item_name,
item_kind,
attribute,
} => write!(
f,
"{item_kind} `{item_name}` has invalid attribute `{}`",
attribute.name(),
),
InvalidEscapeSequence { character } => write!(
f,
"`\\{}` is not a valid escape sequence",

View File

@ -2,10 +2,6 @@ use super::*;
#[derive(Debug, PartialEq)]
pub(crate) enum CompileErrorKind<'src> {
AliasInvalidAttribute {
alias: &'src str,
attribute: Attribute<'src>,
},
AliasShadowsRecipe {
alias: &'src str,
recipe_line: usize,
@ -63,6 +59,9 @@ pub(crate) enum CompileErrorKind<'src> {
variable: &'src str,
},
ExtraLeadingWhitespace,
ExtraneousAttributes {
count: usize,
},
FunctionArgumentCountMismatch {
function: &'src str,
found: usize,
@ -76,6 +75,11 @@ pub(crate) enum CompileErrorKind<'src> {
Internal {
message: String,
},
InvalidAttribute {
item_kind: &'static str,
item_name: &'src str,
attribute: Attribute<'src>,
},
InvalidEscapeSequence {
character: char,
},

View File

@ -720,13 +720,15 @@ impl Config {
})
}
pub(crate) fn require_unstable(&self, message: &str) -> RunResult<'static> {
if self.unstable {
pub(crate) fn require_unstable(
&self,
justfile: &Justfile,
unstable_feature: UnstableFeature,
) -> RunResult<'static> {
if self.unstable || justfile.settings.unstable {
Ok(())
} else {
Err(Error::Unstable {
message: message.to_owned(),
})
Err(Error::UnstableFeature { unstable_feature })
}
}

View File

@ -174,8 +174,8 @@ pub(crate) enum Error<'src> {
recipe: String,
suggestion: Option<Suggestion<'src>>,
},
Unstable {
message: String,
UnstableFeature {
unstable_feature: UnstableFeature,
},
WriteJustfile {
justfile: PathBuf,
@ -459,8 +459,8 @@ impl<'src> ColorDisplay for Error<'src> {
write!(f, "\n{suggestion}")?;
}
}
Unstable { message } => {
write!(f, "{message} Invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features.")?;
UnstableFeature { unstable_feature } => {
write!(f, "{unstable_feature} Invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features.")?;
}
WriteJustfile { justfile, io_error } => {
let justfile = justfile.display();

View File

@ -13,6 +13,7 @@ pub(crate) enum Item<'src> {
relative: StringLiteral<'src>,
},
Module {
attributes: BTreeSet<Attribute<'src>>,
absolute: Option<PathBuf>,
doc: Option<&'src str>,
name: Name<'src>,

View File

@ -13,7 +13,7 @@ struct Invocation<'src: 'run, 'run> {
pub(crate) struct Justfile<'src> {
pub(crate) aliases: Table<'src, Alias<'src>>,
pub(crate) assignments: Table<'src, Assignment<'src>>,
pub(crate) doc: Option<&'src str>,
pub(crate) doc: Option<String>,
#[serde(rename = "first", serialize_with = "keyed::serialize_option")]
pub(crate) default: Option<Rc<Recipe<'src>>>,
#[serde(skip)]
@ -26,9 +26,9 @@ pub(crate) struct Justfile<'src> {
#[serde(skip)]
pub(crate) source: PathBuf,
pub(crate) unexports: HashSet<String>,
pub(crate) warnings: Vec<Warning>,
#[serde(skip)]
pub(crate) unstable: BTreeSet<Unstable>,
pub(crate) unstable_features: BTreeSet<UnstableFeature>,
pub(crate) warnings: Vec<Warning>,
}
impl<'src> Justfile<'src> {
@ -228,12 +228,8 @@ impl<'src> Justfile<'src> {
}
pub(crate) fn check_unstable(&self, config: &Config) -> RunResult<'src> {
if !config.unstable && !self.settings.unstable {
if let Some(unstable) = self.unstable.iter().next() {
return Err(Error::Unstable {
message: unstable.message(),
});
}
if let Some(&unstable_feature) = self.unstable_features.iter().next() {
config.require_unstable(self, unstable_feature)?;
}
for module in self.modules.values() {

View File

@ -42,7 +42,7 @@ pub(crate) use {
shell::Shell, show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, unstable::Unstable, use_color::UseColor,
unresolved_recipe::UnresolvedRecipe, unstable_feature::UnstableFeature, use_color::UseColor,
variables::Variables, verbosity::Verbosity, warning::Warning,
},
camino::Utf8Path,
@ -204,7 +204,7 @@ mod token_kind;
mod unindent;
mod unresolved_dependency;
mod unresolved_recipe;
mod unstable;
mod unstable_feature;
mod use_color;
mod variables;
mod verbosity;

View File

@ -321,6 +321,14 @@ impl<'run, 'src> Parser<'run, 'src> {
self.accept(ByteOrderMark)?;
loop {
let mut attributes = self.parse_attributes()?;
let mut take_attributes = || {
attributes
.take()
.map(|(_token, attributes)| attributes)
.unwrap_or_default()
};
let next = self.next()?;
if let Some(comment) = self.accept(Comment)? {
@ -334,7 +342,7 @@ impl<'run, 'src> Parser<'run, 'src> {
} else if self.next_is(Identifier) {
match Keyword::from_lexeme(next.lexeme()) {
Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
items.push(Item::Alias(self.parse_alias(BTreeSet::new())?));
items.push(Item::Alias(self.parse_alias(take_attributes())?));
}
Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
self.presume_keyword(Keyword::Export)?;
@ -388,6 +396,7 @@ impl<'run, 'src> Parser<'run, 'src> {
};
items.push(Item::Module {
attributes: take_attributes(),
absolute: None,
doc,
name,
@ -412,7 +421,7 @@ impl<'run, 'src> Parser<'run, 'src> {
items.push(Item::Recipe(self.parse_recipe(
doc,
false,
BTreeSet::new(),
take_attributes(),
)?));
}
}
@ -422,23 +431,17 @@ impl<'run, 'src> Parser<'run, 'src> {
items.push(Item::Recipe(self.parse_recipe(
doc,
true,
BTreeSet::new(),
take_attributes(),
)?));
} else if let Some(attributes) = self.parse_attributes()? {
let next_keyword = Keyword::from_lexeme(self.next()?.lexeme());
match next_keyword {
Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
items.push(Item::Alias(self.parse_alias(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, attributes)?));
}
}
} else {
return Err(self.unexpected_token()?);
}
if let Some((token, attributes)) = attributes {
return Err(token.error(CompileErrorKind::ExtraneousAttributes {
count: attributes.len(),
}));
}
}
if self.next_token == self.tokens.len() {
@ -989,10 +992,16 @@ impl<'run, 'src> Parser<'run, 'src> {
}
/// Parse recipe attributes
fn parse_attributes(&mut self) -> CompileResult<'src, Option<BTreeSet<Attribute<'src>>>> {
fn parse_attributes(
&mut self,
) -> CompileResult<'src, Option<(Token<'src>, BTreeSet<Attribute<'src>>)>> {
let mut attributes = BTreeMap::new();
while self.accepted(BracketL)? {
let mut token = None;
while let Some(bracket) = self.accept(BracketL)? {
token.get_or_insert(bracket);
loop {
let name = self.parse_name()?;
@ -1029,7 +1038,7 @@ impl<'run, 'src> Parser<'run, 'src> {
if attributes.is_empty() {
Ok(None)
} else {
Ok(Some(attributes.into_keys().collect()))
Ok(Some((token.unwrap(), attributes.into_keys().collect())))
}
}
}

View File

@ -368,7 +368,16 @@ impl<'src, D> Recipe<'src, D> {
io_error: error,
})?;
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 {

View File

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

@ -79,7 +79,7 @@ impl Subcommand {
justfile.run(config, &search, overrides, &[])?;
}
Dump => Self::dump(config, ast, justfile)?,
Format => Self::format(config, &search, src, ast)?,
Format => Self::format(config, &search, src, ast, justfile)?,
Groups => Self::groups(config, justfile),
List { path } => Self::list(config, justfile, path)?,
Show { path } => Self::show(config, justfile, path)?,
@ -337,8 +337,14 @@ impl Subcommand {
Ok(())
}
fn format(config: &Config, search: &Search, src: &str, ast: &Ast) -> RunResult<'static> {
config.require_unstable("The `--fmt` command is currently unstable.")?;
fn format(
config: &Config,
search: &Search,
src: &str,
ast: &Ast,
justfile: &Justfile,
) -> RunResult<'static> {
config.require_unstable(justfile, UnstableFeature::FormatSubcommand)?;
let formatted = ast.to_string();
@ -446,7 +452,7 @@ impl Subcommand {
signature_widths: &BTreeMap<&str, usize>,
) {
if let Some(doc) = doc {
if doc.lines().count() <= 1 {
if !doc.is_empty() && doc.lines().count() <= 1 {
print!(
"{:padding$}{} {}",
"",
@ -539,13 +545,13 @@ impl Subcommand {
ordered.insert(0, None);
}
let no_groups = groups.contains_key(&None) && groups.len() == 1;
for (i, group) in ordered.into_iter().enumerate() {
if i > 0 {
println!();
}
let no_groups = groups.contains_key(&None) && groups.len() == 1;
if !no_groups {
print!("{list_prefix}");
if let Some(group) = &group {
@ -605,12 +611,16 @@ impl Subcommand {
Self::list_module(config, submodule, depth + 1);
}
} else {
for submodule in module.modules(config) {
for (i, submodule) in module.modules(config).into_iter().enumerate() {
if !no_groups && !groups.is_empty() && i == 0 {
println!();
}
print!("{list_prefix}{} ...", submodule.name());
format_doc(
config,
submodule.name(),
submodule.doc,
submodule.doc.as_deref(),
max_signature_width,
&signature_widths,
);

View File

@ -1,12 +0,0 @@
#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)]
pub(crate) enum Unstable {
Modules,
}
impl Unstable {
pub(crate) fn message(self) -> String {
match self {
Self::Modules => "Modules are currently unstable.".into(),
}
}
}

14
src/unstable_feature.rs Normal file
View File

@ -0,0 +1,14 @@
use super::*;
#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)]
pub(crate) enum UnstableFeature {
FormatSubcommand,
}
impl Display for UnstableFeature {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::FormatSubcommand => write!(f, "The `--fmt` command is currently unstable."),
}
}
}

View File

@ -193,3 +193,40 @@ fn doc_multiline() {
)
.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();
}

View File

@ -86,7 +86,6 @@ fn recipes_in_submodules_can_be_chosen() {
.args(["--unstable", "--choose"])
.env("JUST_CHOOSER", "head -n10")
.write("bar.just", "baz:\n echo BAZ")
.test_round_trip(false)
.justfile(
"
mod bar

View File

@ -864,7 +864,6 @@ fn source_file() {
Test::new()
.args(["--evaluate", "x"])
.test_round_trip(false)
.justfile(
"
import 'foo.just'
@ -875,8 +874,7 @@ fn source_file() {
.run();
Test::new()
.args(["--unstable", "foo", "bar"])
.test_round_trip(false)
.args(["foo", "bar"])
.justfile(
"
mod foo
@ -890,8 +888,7 @@ fn source_file() {
#[test]
fn source_directory() {
Test::new()
.args(["--unstable", "foo", "bar"])
.test_round_trip(false)
.args(["foo", "bar"])
.justfile(
"
mod foo
@ -984,9 +981,7 @@ import-outer: import-inner
echo '{{ module_directory() }}'
",
)
.test_round_trip(false)
.args([
"--unstable",
"outer",
"import-outer",
"baz",

View File

@ -17,7 +17,6 @@ fn import_succeeds() {
@echo A
",
)
.test_round_trip(false)
.arg("a")
.stdout("B\nA\n")
.run();
@ -34,7 +33,6 @@ fn missing_import_file_error() {
@echo A
",
)
.test_round_trip(false)
.arg("a")
.status(EXIT_FAILURE)
.stderr(
@ -60,7 +58,6 @@ fn missing_optional_imports_are_ignored() {
@echo A
",
)
.test_round_trip(false)
.arg("a")
.stdout("A\n")
.run();
@ -79,7 +76,6 @@ fn trailing_spaces_after_import_are_ignored() {
@echo A
",
)
.test_round_trip(false)
.stdout("A\n")
.run();
}
@ -99,7 +95,6 @@ fn import_after_recipe() {
import './import.justfile'
",
)
.test_round_trip(false)
.stdout("A\n")
.run();
}
@ -126,7 +121,6 @@ fn import_recipes_are_not_default() {
"import.justfile": "bar:",
})
.justfile("import './import.justfile'")
.test_round_trip(false)
.status(EXIT_FAILURE)
.stderr("error: Justfile contains no default recipe.\n")
.run();
@ -143,7 +137,6 @@ fn listed_recipes_in_imports_are_in_load_order() {
)
.write("import.justfile", "bar:")
.args(["--list", "--unsorted"])
.test_round_trip(false)
.stdout(
"
Available recipes:
@ -190,7 +183,6 @@ fn recipes_in_import_are_overridden_by_recipes_in_parent() {
set allow-duplicate-recipes
",
)
.test_round_trip(false)
.arg("a")
.stdout("ROOT\n")
.run();
@ -216,7 +208,6 @@ fn variables_in_import_are_overridden_by_variables_in_parent() {
@echo {{f}}
",
)
.test_round_trip(false)
.arg("a")
.stdout("bar\n")
.run();
@ -232,7 +223,6 @@ fn import_paths_beginning_with_tilde_are_expanded_to_homdir() {
import '~/mod.just'
",
)
.test_round_trip(false)
.arg("foo")
.stdout("FOOBAR\n")
.env("HOME", "foobar")
@ -248,7 +238,6 @@ fn imports_dump_correctly() {
import './import.justfile'
",
)
.test_round_trip(false)
.arg("--dump")
.stdout("import './import.justfile'\n")
.run();
@ -263,7 +252,6 @@ fn optional_imports_dump_correctly() {
import? './import.justfile'
",
)
.test_round_trip(false)
.arg("--dump")
.stdout("import? './import.justfile'\n")
.run();
@ -279,7 +267,6 @@ fn imports_in_root_run_in_justfile_directory() {
import 'foo/import.justfile'
",
)
.test_round_trip(false)
.arg("bar")
.stdout("BAZ")
.run();
@ -292,8 +279,6 @@ fn imports_in_submodules_run_in_submodule_directory() {
.write("foo/mod.just", "import 'import.just'")
.write("foo/import.just", "bar:\n @cat baz")
.write("foo/baz", "BAZ")
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("bar")
.stdout("BAZ")
@ -306,7 +291,6 @@ fn nested_import_paths_are_relative_to_containing_submodule() {
.justfile("import 'foo/import.just'")
.write("foo/import.just", "import 'bar.just'")
.write("foo/bar.just", "bar:\n @echo BAR")
.test_round_trip(false)
.arg("bar")
.stdout("BAR\n")
.run();
@ -319,8 +303,6 @@ fn recipes_in_nested_imports_run_in_parent_module() {
.write("foo/import.just", "import 'bar/import.just'")
.write("foo/bar/import.just", "bar:\n @cat baz")
.write("baz", "BAZ")
.test_round_trip(false)
.arg("--unstable")
.arg("bar")
.stdout("BAZ")
.run();
@ -339,7 +321,6 @@ fn shebang_recipes_in_imports_in_root_run_in_justfile_directory() {
import 'foo/import.justfile'
",
)
.test_round_trip(false)
.arg("bar")
.stdout("BAZ")
.run();
@ -357,7 +338,6 @@ fn recipes_imported_in_root_run_in_command_line_provided_working_directory() {
"--justfile",
"subdir/a.justfile",
])
.test_round_trip(false)
.stdout("BAZBAZ")
.run();
}

View File

@ -3,7 +3,7 @@ use super::*;
fn case(justfile: &str, value: Value) {
Test::new()
.justfile(justfile)
.args(["--dump", "--dump-format", "json", "--unstable"])
.args(["--dump", "--dump-format", "json"])
.stdout(format!("{}\n", serde_json::to_string(&value).unwrap()))
.run();
}
@ -1110,8 +1110,7 @@ fn module() {
.tree(tree! {
"foo.just": "bar:",
})
.args(["--dump", "--dump-format", "json", "--unstable"])
.test_round_trip(false)
.args(["--dump", "--dump-format", "json"])
.stdout(format!(
"{}\n",
serde_json::to_string(&json!({

View File

@ -12,8 +12,7 @@ fn modules_unsorted() {
mod bar
",
)
.test_round_trip(false)
.args(["--unstable", "--list", "--unsorted"])
.args(["--list", "--unsorted"])
.stdout(
"
Available recipes:
@ -156,8 +155,7 @@ fn list_submodule() {
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--list", "foo"])
.args(["--list", "foo"])
.stdout(
"
Available recipes:
@ -177,8 +175,7 @@ fn list_nested_submodule() {
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--list", "foo", "bar"])
.args(["--list", "foo", "bar"])
.stdout(
"
Available recipes:
@ -195,8 +192,7 @@ fn list_nested_submodule() {
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--list", "foo::bar"])
.args(["--list", "foo::bar"])
.stdout(
"
Available recipes:
@ -209,7 +205,7 @@ fn list_nested_submodule() {
#[test]
fn list_invalid_path() {
Test::new()
.args(["--unstable", "--list", "$hello"])
.args(["--list", "$hello"])
.stderr("error: Invalid module path `$hello`\n")
.status(1)
.run();
@ -218,7 +214,7 @@ fn list_invalid_path() {
#[test]
fn list_unknown_submodule() {
Test::new()
.args(["--unstable", "--list", "hello"])
.args(["--list", "hello"])
.stderr("error: Justfile does not contain submodule `hello`\n")
.status(1)
.run();
@ -236,8 +232,7 @@ fn list_with_groups_in_modules() {
",
)
.write("bar.just", "[group('BAZ')]\nbaz:")
.test_round_trip(false)
.args(["--unstable", "--list", "--list-submodules"])
.args(["--list", "--list-submodules"])
.stdout(
"
Available recipes:
@ -261,8 +256,7 @@ fn list_displays_recipes_in_submodules() {
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--list", "--list-submodules"])
.args(["--list", "--list-submodules"])
.stdout(
"
Available recipes:
@ -285,8 +279,7 @@ fn modules_are_space_separated_in_output() {
mod bar
",
)
.test_round_trip(false)
.args(["--unstable", "--list", "--list-submodules"])
.args(["--list", "--list-submodules"])
.stdout(
"
Available recipes:
@ -319,8 +312,7 @@ barbarbar:
",
)
.justfile("mod foo")
.test_round_trip(false)
.args(["--unstable", "--list", "--list-submodules"])
.args(["--list", "--list-submodules"])
.stdout(
"
Available recipes:
@ -341,8 +333,7 @@ fn nested_modules_are_properly_indented() {
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--list", "--list-submodules"])
.args(["--list", "--list-submodules"])
.stdout(
"
Available recipes:
@ -364,8 +355,7 @@ fn module_doc_rendered() {
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--list"])
.args(["--list"])
.stdout(
"
Available recipes:
@ -393,8 +383,7 @@ fn module_doc_aligned() {
@echo Hi
",
)
.test_round_trip(false)
.args(["--unstable", "--list"])
.args(["--list"])
.stdout(
"
Available recipes:
@ -405,3 +394,47 @@ fn module_doc_aligned() {
)
.run();
}
#[test]
fn space_before_submodules_following_groups() {
Test::new()
.write("foo.just", "")
.justfile(
"
mod foo
[group: 'baz']
bar:
",
)
.args(["--list"])
.stdout(
"
Available recipes:
[baz]
bar
foo ...
",
)
.run();
}
#[test]
fn no_space_before_submodules_not_following_groups() {
Test::new()
.write("foo.just", "")
.justfile(
"
mod foo
",
)
.args(["--list"])
.stdout(
"
Available recipes:
foo ...
",
)
.run();
}

View File

@ -1,16 +1,16 @@
use super::*;
#[test]
fn modules_are_unstable() {
fn modules_are_stable() {
Test::new()
.justfile(
"
mod foo
",
)
.write("foo.just", "")
.stderr_regex("error: Modules are currently unstable..*")
.status(EXIT_FAILURE)
.write("foo.just", "@bar:\n echo ok")
.args(["foo", "bar"])
.stdout("ok\n")
.run();
}
@ -23,8 +23,6 @@ fn default_recipe_in_submodule_must_have_no_arguments() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.stderr("error: Recipe `foo` cannot be used as default recipe since it requires at least 1 argument.\n")
.status(EXIT_FAILURE)
@ -40,8 +38,6 @@ fn module_recipes_can_be_run_as_subcommands() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("FOO\n")
@ -57,8 +53,6 @@ fn module_recipes_can_be_run_with_path_syntax() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo::foo")
.stdout("FOO\n")
.run();
@ -74,8 +68,6 @@ fn nested_module_recipes_can_be_run_with_path_syntax() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo::bar::baz")
.stdout("BAZ\n")
.run();
@ -84,21 +76,18 @@ fn nested_module_recipes_can_be_run_with_path_syntax() {
#[test]
fn invalid_path_syntax() {
Test::new()
.test_round_trip(false)
.arg(":foo::foo")
.stderr("error: Justfile does not contain recipe `:foo::foo`.\n")
.status(EXIT_FAILURE)
.run();
Test::new()
.test_round_trip(false)
.arg("foo::foo:")
.stderr("error: Justfile does not contain recipe `foo::foo:`.\n")
.status(EXIT_FAILURE)
.run();
Test::new()
.test_round_trip(false)
.arg("foo:::foo")
.stderr("error: Justfile does not contain recipe `foo:::foo`.\n")
.status(EXIT_FAILURE)
@ -108,7 +97,6 @@ fn invalid_path_syntax() {
#[test]
fn missing_recipe_after_invalid_path() {
Test::new()
.test_round_trip(false)
.arg(":foo::foo")
.arg("bar")
.stderr("error: Justfile does not contain recipe `:foo::foo`.\n")
@ -126,8 +114,6 @@ fn assignments_are_evaluated_in_modules() {
bar := 'PARENT'
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("CHILD\n")
@ -143,8 +129,6 @@ fn module_subcommand_runs_default_recipe() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.stdout("FOO\n")
.run();
@ -160,8 +144,6 @@ fn modules_can_contain_other_modules() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("bar")
.arg("baz")
@ -179,8 +161,6 @@ fn circular_module_imports_are_detected() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("bar")
.arg("baz")
@ -207,8 +187,6 @@ foo:
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("FOO\n")
@ -229,9 +207,7 @@ foo:
set allow-duplicate-recipes
",
)
.test_round_trip(false)
.status(EXIT_FAILURE)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stderr(
@ -265,9 +241,7 @@ fn modules_conflict_with_recipes() {
^^^
",
)
.test_round_trip(false)
.status(EXIT_FAILURE)
.arg("--unstable")
.run();
}
@ -291,9 +265,7 @@ fn modules_conflict_with_aliases() {
^^^
",
)
.test_round_trip(false)
.status(EXIT_FAILURE)
.arg("--unstable")
.run();
}
@ -309,7 +281,6 @@ fn modules_conflict_with_other_modules() {
bar:
",
)
.test_round_trip(false)
.status(EXIT_FAILURE)
.stderr(
"
@ -320,7 +291,6 @@ fn modules_conflict_with_other_modules() {
^^^
",
)
.arg("--unstable")
.run();
}
@ -333,8 +303,6 @@ fn modules_are_dumped_correctly() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("--dump")
.stdout("mod foo\n")
.run();
@ -349,8 +317,6 @@ fn optional_modules_are_dumped_correctly() {
mod? foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("--dump")
.stdout("mod? foo\n")
.run();
@ -365,8 +331,6 @@ fn modules_can_be_in_subdirectory() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("FOO\n")
@ -382,8 +346,6 @@ fn modules_in_subdirectory_can_be_named_justfile() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("FOO\n")
@ -399,8 +361,6 @@ fn modules_in_subdirectory_can_be_named_justfile_with_any_case() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("FOO\n")
@ -416,8 +376,6 @@ fn modules_in_subdirectory_can_have_leading_dot() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("FOO\n")
@ -434,8 +392,6 @@ fn modules_require_unambiguous_file() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.status(EXIT_FAILURE)
.stderr(
"
@ -458,8 +414,6 @@ fn missing_module_file_error() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.status(EXIT_FAILURE)
.stderr(
"
@ -484,8 +438,6 @@ fn missing_optional_modules_do_not_trigger_error() {
@echo BAR
",
)
.test_round_trip(false)
.arg("--unstable")
.stdout("BAR\n")
.run();
}
@ -501,8 +453,6 @@ fn missing_optional_modules_do_not_conflict() {
",
)
.write("baz.just", "baz:\n @echo BAZ")
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("baz")
.stdout("BAZ\n")
@ -521,8 +471,7 @@ fn root_dotenv_is_available_to_submodules() {
)
.write("foo.just", "foo:\n @echo $DOTENV_KEY")
.write(".env", "DOTENV_KEY=dotenv-value")
.test_round_trip(false)
.args(["--unstable", "foo", "foo"])
.args(["foo", "foo"])
.stdout("dotenv-value\n")
.run();
}
@ -542,8 +491,7 @@ fn dotenv_settings_in_submodule_are_ignored() {
"set dotenv-load := false\nfoo:\n @echo $DOTENV_KEY",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.test_round_trip(false)
.args(["--unstable", "foo", "foo"])
.args(["foo", "foo"])
.stdout("dotenv-value\n")
.run();
}
@ -557,8 +505,6 @@ fn modules_may_specify_path() {
mod foo 'commands/foo.just'
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("FOO\n")
@ -574,8 +520,6 @@ fn modules_may_specify_path_to_directory() {
mod foo 'commands/bar'
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("FOO\n")
@ -591,8 +535,6 @@ fn modules_with_paths_are_dumped_correctly() {
mod foo 'commands/foo.just'
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("--dump")
.stdout("mod foo 'commands/foo.just'\n")
.run();
@ -607,8 +549,6 @@ fn optional_modules_with_paths_are_dumped_correctly() {
mod? foo 'commands/foo.just'
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("--dump")
.stdout("mod? foo 'commands/foo.just'\n")
.run();
@ -623,7 +563,6 @@ fn recipes_may_be_named_mod() {
@echo FOO
",
)
.test_round_trip(false)
.arg("mod")
.arg("bar")
.stdout("FOO\n")
@ -640,8 +579,6 @@ fn submodule_linewise_recipes_run_in_submodule_directory() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("BAR")
@ -658,8 +595,6 @@ fn submodule_shebang_recipes_run_in_submodule_directory() {
mod foo
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("BAR")
@ -676,8 +611,6 @@ fn module_paths_beginning_with_tilde_are_expanded_to_homdir() {
mod foo '~/mod.just'
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.stdout("FOOBAR\n")
@ -697,8 +630,6 @@ fn recipes_with_same_name_are_both_run() {
@echo ROOT
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("foo::bar")
.arg("bar")
.stdout("MODULE\nROOT\n")
@ -708,7 +639,7 @@ fn recipes_with_same_name_are_both_run() {
#[test]
fn submodule_recipe_not_found_error_message() {
Test::new()
.args(["--unstable", "foo::bar"])
.args(["foo::bar"])
.stderr("error: Justfile does not contain submodule `foo`\n")
.status(1)
.run();
@ -723,8 +654,7 @@ fn submodule_recipe_not_found_spaced_error_message() {
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "foo", "baz"])
.args(["foo", "baz"])
.stderr("error: Justfile does not contain recipe `foo baz`.\nDid you mean `bar`?\n")
.status(1)
.run();
@ -739,8 +669,7 @@ fn submodule_recipe_not_found_colon_separated_error_message() {
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "foo::baz"])
.args(["foo::baz"])
.stderr("error: Justfile does not contain recipe `foo::baz`.\nDid you mean `bar`?\n")
.status(1)
.run();
@ -758,7 +687,7 @@ fn colon_separated_path_does_not_run_recipes() {
@echo BAR
",
)
.args(["--unstable", "foo::bar"])
.args(["foo::bar"])
.stderr("error: Expected submodule at `foo` but found recipe.\n")
.status(1)
.run();
@ -779,8 +708,7 @@ fn expected_submodule_but_found_recipe_in_submodule_error() {
Test::new()
.justfile("mod foo")
.write("foo.just", "bar:")
.test_round_trip(false)
.args(["--unstable", "foo::bar::baz"])
.args(["foo::bar::baz"])
.stderr("error: Expected submodule at `foo::bar` but found recipe.\n")
.status(1)
.run();
@ -805,8 +733,74 @@ fn comments_can_follow_modules() {
mod foo # this is foo
",
)
.test_round_trip(false)
.args(["--unstable", "foo", "foo"])
.args(["foo", "foo"])
.stdout("FOO\n")
.run();
}
#[test]
fn doc_comment_on_module() {
Test::new()
.write("foo.just", "")
.justfile(
"
# Comment
mod foo
",
)
.test_round_trip(false)
.arg("--list")
.stdout("Available recipes:\n foo ... # Comment\n")
.run();
}
#[test]
fn doc_attribute_on_module() {
Test::new()
.write("foo.just", "")
.justfile(
r#"
# Suppressed comment
[doc: "Comment"]
mod foo
"#,
)
.test_round_trip(false)
.arg("--list")
.stdout("Available recipes:\n foo ... # Comment\n")
.run();
}
#[test]
fn bad_module_attribute_fails() {
Test::new()
.write("foo.just", "")
.justfile(
r#"
[no-cd]
mod foo
"#,
)
.test_round_trip(false)
.arg("--list")
.stderr("error: Module `foo` has invalid attribute `no-cd`\n ——▶ justfile:2:5\n\n2 │ mod foo\n │ ^^^\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn empty_doc_attribute_on_module() {
Test::new()
.write("foo.just", "")
.justfile(
r#"
# Suppressed comment
[doc]
mod foo
"#,
)
.test_round_trip(false)
.arg("--list")
.stdout("Available recipes:\n foo ...\n")
.run();
}

View File

@ -80,7 +80,7 @@ error: Expected identifier, but found ']'
}
test! {
name: unattached_attribute_before_comment,
name: extraneous_attribute_before_comment,
justfile: r#"
[no-exit-message]
# This is a doc comment
@ -88,25 +88,31 @@ hello:
@exit 100
"#,
stderr: r#"
error: Expected '@', '[', or identifier, but found comment
justfile:2:1
error: Extraneous attribute
justfile:1:1
2 # This is a doc comment
^^^^^^^^^^^^^^^^^^^^^^^
1 [no-exit-message]
^
"#,
status: EXIT_FAILURE,
}
test! {
name: unattached_attribute_before_empty_line,
name: extraneous_attribute_before_empty_line,
justfile: r#"
[no-exit-message]
hello:
@exit 100
"#,
stderr: "error: Expected '@', '[', or identifier, but found end of line\n ——▶ justfile:2:1\n\n2 │ \n │ ^\n",
stderr: "
error: Extraneous attribute
justfile:1:1
1 [no-exit-message]
^
",
status: EXIT_FAILURE,
}

View File

@ -66,7 +66,7 @@ fn shell_expanded_strings_are_dumped_correctly() {
",
)
.env("JUST_TEST_VARIABLE", "FOO")
.args(["--dump", "--unstable"])
.args(["--dump"])
.stdout("x := x'$JUST_TEST_VARIABLE'\n")
.run();
}
@ -114,9 +114,8 @@ fn shell_expanded_strings_can_be_used_in_mod_paths() {
)
.write("mod.just", "@bar:\n echo BAR")
.env("JUST_TEST_VARIABLE", "mod.just")
.args(["--unstable", "foo", "bar"])
.args(["foo", "bar"])
.stdout("BAR\n")
.test_round_trip(false)
.run();
}

View File

@ -110,8 +110,7 @@ fn show_recipe_at_path() {
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--show", "foo::bar"])
.args(["--show", "foo::bar"])
.stdout("bar:\n @echo MODULE\n")
.run();
}
@ -134,8 +133,7 @@ fn show_space_separated_path() {
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--show", "foo bar"])
.args(["--show", "foo bar"])
.stdout("bar:\n @echo MODULE\n")
.run();
}

View File

@ -65,8 +65,6 @@ fn submodule_recipes() {
bar:
",
)
.test_round_trip(false)
.arg("--unstable")
.arg("--summary")
.stdout("bar foo::foo foo::bar::bar foo::bar::baz::baz foo::bar::baz::biz::biz\n")
.run();
@ -81,7 +79,6 @@ fn summary_implies_unstable() {
mod foo
",
)
.test_round_trip(false)
.arg("--summary")
.stdout("foo::foo\n")
.run();

View File

@ -169,6 +169,7 @@ impl Test {
self
}
#[allow(unused)]
pub(crate) fn test_round_trip(mut self, test_round_trip: bool) -> Self {
self.test_round_trip = test_round_trip;
self

View File

@ -2,14 +2,9 @@ use super::*;
#[test]
fn set_unstable_true_with_env_var() {
let justfile = r#"
default:
echo 'foo'
"#;
for val in ["true", "some-arbitrary-string"] {
Test::new()
.justfile(justfile)
.justfile("")
.args(["--fmt"])
.env("JUST_UNSTABLE", val)
.status(EXIT_SUCCESS)
@ -20,13 +15,9 @@ default:
#[test]
fn set_unstable_false_with_env_var() {
let justfile = r#"
default:
echo 'foo'
"#;
for val in ["0", "", "false"] {
Test::new()
.justfile(justfile)
.justfile("")
.args(["--fmt"])
.env("JUST_UNSTABLE", val)
.status(EXIT_FAILURE)
@ -37,12 +28,8 @@ default:
#[test]
fn set_unstable_false_with_env_var_unset() {
let justfile = r#"
default:
echo 'foo'
"#;
Test::new()
.justfile(justfile)
.justfile("")
.args(["--fmt"])
.status(EXIT_FAILURE)
.stderr_regex("error: The `--fmt` command is currently unstable.*")
@ -52,19 +39,16 @@ default:
#[test]
fn set_unstable_with_setting() {
Test::new()
.justfile(
"
set unstable
mod foo
",
)
.write("foo.just", "@bar:\n echo BAR")
.args(["foo", "bar"])
.stdout("BAR\n")
.justfile("set unstable")
.arg("--fmt")
.stderr_regex("Wrote justfile to .*")
.run();
}
// This test should be re-enabled if we get a new unstable feature which is
// encountered in source files. (As opposed to, for example, the unstable
// `--fmt` subcommand, which is encountered on the command line.)
#[cfg(any())]
#[test]
fn unstable_setting_does_not_affect_submodules() {
Test::new()