1
1
mirror of https://github.com/casey/just.git synced 2024-11-21 14:52:13 +03:00

Compare commits

...

13 Commits

Author SHA1 Message Date
Ben Heidemann
104e8446ce
Merge e3c9a0c199 into 17350a603e 2024-11-19 21:31:50 +00:00
Ben Heidemann
e3c9a0c199
Merge branch 'master' into feat/working-directory-attribute 2024-11-19 21:31:47 +00:00
dependabot[bot]
17350a603e
Update softprops/action-gh-release (#2471) 2024-11-18 11:04:52 -08:00
Casey Rodarmor
084a2d2de3
Add style() function (#2462) 2024-11-17 02:26:11 +00:00
Casey Rodarmor
eb6e3741b8
Make recipe doc attribute override comment (#2470) 2024-11-17 02:02:27 +00:00
Naveen Prashanth
c7b2b78dcc
Add -g to rust-just install instructions (#2459) 2024-11-17 01:34:04 +00:00
Casey Rodarmor
520cf91423
Change doc backtick color to cyan (#2469) 2024-11-17 00:21:14 +00:00
Michael Bianco
a73c0976a1
Note that set shell is not used for [script] recipes (#2468) 2024-11-16 23:58:35 +00:00
laniakea64
5db910f400
Replace derivative with derive-where (#2465) 2024-11-16 00:04:10 +00:00
Ben Heidemann
18ff6b5705 review comments 2024-10-31 19:36:42 +00:00
Ben Heidemann
d034fd1510
Merge branch 'master' into feat/working-directory-attribute 2024-10-31 14:01:56 +00:00
Ben Heidemann
d97d77ab0c
Merge branch 'master' into feat/working-directory-attribute 2024-10-29 10:08:43 +00:00
Ben Heidemann
d16727b9c4 feat: add working-directory attribute 2024-10-22 18:22:16 +01:00
18 changed files with 398 additions and 56 deletions

View File

@ -110,7 +110,7 @@ jobs:
shell: bash
- name: Publish Archive
uses: softprops/action-gh-release@v2.0.9
uses: softprops/action-gh-release@v2.1.0
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
draft: false
@ -120,7 +120,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish Changelog
uses: softprops/action-gh-release@v2.0.9
uses: softprops/action-gh-release@v2.1.0
if: >-
${{
startsWith(github.ref, 'refs/tags/')
@ -157,7 +157,7 @@ jobs:
shasum -a 256 * > ../SHA256SUMS
- name: Publish Checksums
uses: softprops/action-gh-release@v2.0.9
uses: softprops/action-gh-release@v2.1.0
with:
draft: false
files: SHA256SUMS

37
Cargo.lock generated
View File

@ -237,7 +237,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn",
]
[[package]]
@ -329,14 +329,14 @@ dependencies = [
]
[[package]]
name = "derivative"
version = "2.2.0"
name = "derive-where"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@ -531,7 +531,7 @@ dependencies = [
"clap_complete",
"clap_mangen",
"ctrlc",
"derivative",
"derive-where",
"dirs",
"dotenvy",
"edit-distance",
@ -861,7 +861,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn",
]
[[package]]
@ -930,7 +930,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn",
]
[[package]]
@ -958,18 +958,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.79",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
"syn",
]
[[package]]
@ -1038,7 +1027,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn",
]
[[package]]
@ -1142,7 +1131,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn",
"wasm-bindgen-shared",
]
@ -1164,7 +1153,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -1396,5 +1385,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn",
]

View File

@ -26,7 +26,7 @@ clap = { version = "4.0.0", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.0.0"
clap_mangen = "0.2.20"
ctrlc = { version = "3.1.1", features = ["termination"] }
derivative = "2.0.0"
derive-where = "1.2.7"
dirs = "5.0.1"
dotenvy = "0.15"
edit-distance = "2.0.0"

View File

@ -167,7 +167,7 @@ most Windows users.)
<tr>
<td><a href=https://www.npmjs.com/>npm</a></td>
<td><a href=https://www.npmjs.com/package/rust-just>rust-just</a></td>
<td><code>npm install rust-just</code></td>
<td><code>npm install -g rust-just</code></td>
</tr>
<tr>
<td><a href=https://pypi.org/>PyPI</a></td>
@ -1851,6 +1851,24 @@ for details.
`requirement`, e.g., `">=0.1.0"`, returning `"true"` if so and `"false"`
otherwise.
#### Style
- `style(name)`<sup>master</sup> - Return a named terminal display attribute
escape sequence used by `just`. Unlike terminal display attribute escape
sequence constants, which contain standard colors and styles, `style(name)`
returns an escape sequence used by `just` itself, and can be used to make
recipe output match `just`'s own output.
Recognized values for `name` are `'command'`, for echoed recipe lines,
`error`, and `warning`.
For example, to style an error message:
```just
scary:
@echo '{{ style("error") }}OH NO{{ NORMAL }}'
```
##### XDG Directories<sup>1.23.0</sup>
These functions return paths to user-specific directories for things like
@ -1954,6 +1972,7 @@ change their behavior.
| `[script(COMMAND)]`<sup>1.32.0</sup> | recipe | Execute recipe as a script interpreted by `COMMAND`. See [script recipes](#script-recipes) for more details. |
| `[unix]`<sup>1.8.0</sup> | recipe | Enable recipe on Unixes. (Includes MacOS). |
| `[windows]`<sup>1.8.0</sup> | recipe | Enable recipe on Windows. |
| `[working-directory('bar')]`<sup>1.37.0</sup> | recipe | Set the working directory for the recipe, relative to the default working directory. |
A recipe can have multiple attributes, either on multiple lines:
@ -2017,6 +2036,23 @@ Can be used with paths that are relative to the current directory, because
`[no-cd]` prevents `just` from changing the current directory when executing
`commit`.
#### Changing Working Directory<sup>1.37.0</sup>
`just` normally executes recipes with the current directory set to the directory
that contains the `justfile`. The execution directory can be changed with the
`[working-directory('dir')]` attribute. This can be used to create recipes which
are executed in a directory relative to the default directory.
For example, this `example` recipe:
```just
[working-directory('dir')]
example:
echo "$(pwd)"
```
Which will run in the `dir` directory.
#### Requiring Confirmation for Recipes<sup>1.17.0</sup>
`just` normally executes all recipes unless there is an error. The `[confirm]`
@ -2755,8 +2791,9 @@ scripts interpreted by `COMMAND`. This avoids some of the issues with shebang
recipes, such as the use of `cygpath` on Windows, the need to use
`/usr/bin/env`, and inconsistences in shebang line splitting across Unix OSs.
Recipes with an empty `[script]` attribute are executed with the value of
`set script-interpreter := […]`<sup>1.33.0</sup>, defaulting to `sh -eu`.
Recipes with an empty `[script]` attribute are executed with the value of `set
script-interpreter := […]`<sup>1.33.0</sup>, defaulting to `sh -eu`, and *not*
the value of `set shell`.
The body of the recipe is evaluated, written to disk in the temporary
directory, and run by passing its path as an argument to `COMMAND`.

View File

@ -220,6 +220,22 @@ list:
<code>asdf install just &lt;version&gt;</code>
</td>
</tr>
<tr>
<td><a href="https://packaging.python.org/tutorials/installing-packages">Various</a></td>
<td><a href="https://pypi.org">PyPI</a></td>
<td><a href="https://pypi.org/project/rust-just">rust-just</a></td>
<td>
<code>pipx install rust-just</code><br>
</td>
</tr>
<tr>
<td><a href="https://docs.npmjs.com/packages-and-modules/getting-packages-from-the-registry">Various</a></td>
<td><a href="https://www.npmjs.com">npm</a></td>
<td><a href="https://www.npmjs.com/package/rust-just">rust-just</a></td>
<td>
<code>npm install -g rust-just</code><br>
</td>
</tr>
<tr>
<td><a href="https://debian.org">Debian</a> and <a href="https://ubuntu.com">Ubuntu</a> derivatives</td>
<td><a href="https://mpr.makedeb.org">MPR</a></td>

View File

@ -23,13 +23,14 @@ pub(crate) enum Attribute<'src> {
Script(Option<Interpreter<'src>>),
Unix,
Windows,
WorkingDirectory(StringLiteral<'src>),
}
impl AttributeDiscriminant {
fn argument_range(self) -> RangeInclusive<usize> {
match self {
Self::Confirm | Self::Doc => 0..=1,
Self::Group | Self::Extension => 1..=1,
Self::Group | Self::Extension | Self::WorkingDirectory => 1..=1,
Self::Linux
| Self::Macos
| Self::NoCd
@ -93,6 +94,9 @@ impl<'src> Attribute<'src> {
}),
AttributeDiscriminant::Unix => Self::Unix,
AttributeDiscriminant::Windows => Self::Windows,
AttributeDiscriminant::WorkingDirectory => {
Self::WorkingDirectory(arguments.into_iter().next().unwrap())
}
})
}
@ -109,7 +113,8 @@ impl<'src> Display for Attribute<'src> {
Self::Confirm(Some(argument))
| Self::Doc(Some(argument))
| Self::Extension(argument)
| Self::Group(argument) => write!(f, "({argument})")?,
| Self::Group(argument)
| Self::WorkingDirectory(argument) => write!(f, "({argument})")?,
Self::Script(Some(shell)) => write!(f, "({shell})")?,
Self::Confirm(None)
| Self::Doc(None)

View File

@ -35,7 +35,6 @@ impl Color {
Self::default()
}
#[cfg(test)]
pub(crate) fn always() -> Self {
Self {
use_color: UseColor::Always,
@ -67,7 +66,7 @@ impl Color {
}
pub(crate) fn doc_backtick(self) -> Self {
self.restyle(Style::new().fg(White).on(Black))
self.restyle(Style::new().fg(Cyan))
}
pub(crate) fn error(self) -> Self {

View File

@ -98,6 +98,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
"snakecase" => Unary(snakecase),
"source_directory" => Nullary(source_directory),
"source_file" => Nullary(source_file),
"style" => Unary(style),
"titlecase" => Unary(titlecase),
"trim" => Unary(trim),
"trim_end" => Unary(trim_end),
@ -623,6 +624,20 @@ fn source_file(context: Context) -> FunctionResult {
})
}
fn style(context: Context, s: &str) -> FunctionResult {
match s {
"command" => Ok(
Color::always()
.command(context.evaluator.context.config.command_color)
.prefix()
.to_string(),
),
"error" => Ok(Color::always().error().prefix().to_string()),
"warning" => Ok(Color::always().warning().prefix().to_string()),
_ => Err(format!("unknown style: `{s}`")),
}
}
fn titlecase(_context: Context, s: &str) -> FunctionResult {
Ok(s.to_title_case())
}

View File

@ -34,7 +34,7 @@ pub(crate) use {
},
camino::Utf8Path,
clap::ValueEnum,
derivative::Derivative,
derive_where::derive_where,
edit_distance::edit_distance,
lexiclean::Lexiclean,
libc::EXIT_FAILURE,

View File

@ -197,7 +197,7 @@ impl<'src> Node<'src> for UnresolvedRecipe<'src> {
t.push_mut("quiet");
}
if let Some(doc) = self.doc {
if let Some(doc) = &self.doc {
t.push_mut(Tree::string(doc));
}

View File

@ -936,6 +936,14 @@ impl<'run, 'src> Parser<'run, 'src> {
let private = name.lexeme().starts_with('_') || attributes.contains(&Attribute::Private);
let mut doc = doc.map(ToOwned::to_owned);
for attribute in &attributes {
if let Attribute::Doc(attribute_doc) = attribute {
doc = attribute_doc.as_ref().map(|doc| doc.cooked.clone());
}
}
Ok(Recipe {
shebang: shebang || script,
attributes,
@ -1118,6 +1126,7 @@ impl<'run, 'src> Parser<'run, 'src> {
&mut self,
) -> CompileResult<'src, Option<(Token<'src>, BTreeSet<Attribute<'src>>)>> {
let mut attributes = BTreeMap::new();
let mut working_directory_attribute_line = None;
let mut token = None;
@ -1144,6 +1153,17 @@ impl<'run, 'src> Parser<'run, 'src> {
let attribute = Attribute::new(name, arguments)?;
if let Attribute::WorkingDirectory(_) = &attribute {
if let Some(line) = working_directory_attribute_line {
return Err(name.error(CompileErrorKind::DuplicateAttribute {
attribute: name.lexeme(),
first: line,
}));
}
working_directory_attribute_line = Some(name.line);
}
if let Some(line) = attributes.get(&attribute) {
return Err(name.error(CompileErrorKind::DuplicateAttribute {
attribute: name.lexeme(),

View File

@ -22,7 +22,7 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> {
pub(crate) attributes: BTreeSet<Attribute<'src>>,
pub(crate) body: Vec<Line<'src>>,
pub(crate) dependencies: Vec<D>,
pub(crate) doc: Option<&'src str>,
pub(crate) doc: Option<String>,
#[serde(skip)]
pub(crate) file_depth: u32,
#[serde(skip)]
@ -131,11 +131,19 @@ impl<'src, D> Recipe<'src, D> {
}
fn working_directory<'a>(&'a self, context: &'a ExecutionContext) -> Option<PathBuf> {
if self.change_directory() {
Some(context.working_directory())
} else {
None
if !self.change_directory() {
return None;
}
let working_directory = context.working_directory();
for attribute in &self.attributes {
if let Attribute::WorkingDirectory(dir) = attribute {
return Some(working_directory.join(&dir.cooked));
}
}
Some(working_directory)
}
fn no_quiet(&self) -> bool {
@ -465,7 +473,8 @@ impl<'src, D> Recipe<'src, D> {
return doc.as_ref().map(|s| s.cooked.as_ref());
}
}
self.doc
self.doc.as_deref()
}
pub(crate) fn subsequents(&self) -> impl Iterator<Item = &D> {
@ -475,8 +484,14 @@ impl<'src, D> Recipe<'src, D> {
impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
if let Some(doc) = self.doc {
writeln!(f, "# {doc}")?;
if !self
.attributes
.iter()
.any(|attribute| matches!(attribute, Attribute::Doc(_)))
{
if let Some(doc) = &self.doc {
writeln!(f, "# {doc}")?;
}
}
for attribute in &self.attributes {

View File

@ -1,46 +1,46 @@
use super::*;
#[derive(Derivative)]
#[derivative(Debug, Clone, PartialEq = "feature_allow_slow_enum")]
#[derive_where(Debug, PartialEq)]
#[derive(Clone)]
pub(crate) enum Thunk<'src> {
Nullary {
name: Name<'src>,
#[derivative(Debug = "ignore", PartialEq = "ignore")]
#[derive_where(skip(Debug, EqHashOrd))]
function: fn(function::Context) -> FunctionResult,
},
Unary {
name: Name<'src>,
#[derivative(Debug = "ignore", PartialEq = "ignore")]
#[derive_where(skip(Debug, EqHashOrd))]
function: fn(function::Context, &str) -> FunctionResult,
arg: Box<Expression<'src>>,
},
UnaryOpt {
name: Name<'src>,
#[derivative(Debug = "ignore", PartialEq = "ignore")]
#[derive_where(skip(Debug, EqHashOrd))]
function: fn(function::Context, &str, Option<&str>) -> FunctionResult,
args: (Box<Expression<'src>>, Box<Option<Expression<'src>>>),
},
UnaryPlus {
name: Name<'src>,
#[derivative(Debug = "ignore", PartialEq = "ignore")]
#[derive_where(skip(Debug, EqHashOrd))]
function: fn(function::Context, &str, &[String]) -> FunctionResult,
args: (Box<Expression<'src>>, Vec<Expression<'src>>),
},
Binary {
name: Name<'src>,
#[derivative(Debug = "ignore", PartialEq = "ignore")]
#[derive_where(skip(Debug, EqHashOrd))]
function: fn(function::Context, &str, &str) -> FunctionResult,
args: [Box<Expression<'src>>; 2],
},
BinaryPlus {
name: Name<'src>,
#[derivative(Debug = "ignore", PartialEq = "ignore")]
#[derive_where(skip(Debug, EqHashOrd))]
function: fn(function::Context, &str, &str, &[String]) -> FunctionResult,
args: ([Box<Expression<'src>>; 2], Vec<Expression<'src>>),
},
Ternary {
name: Name<'src>,
#[derivative(Debug = "ignore", PartialEq = "ignore")]
#[derive_where(skip(Debug, EqHashOrd))]
function: fn(function::Context, &str, &str, &str) -> FunctionResult,
args: [Box<Expression<'src>>; 3],
},

View File

@ -1096,3 +1096,27 @@ fn multi_argument_attribute() {
)
.run();
}
#[test]
fn doc_attribute_suppresses_comment() {
Test::new()
.justfile(
"
set unstable
# COMMENT
[doc('ATTRIBUTE')]
foo:
",
)
.arg("--dump")
.stdout(
"
set unstable := true
[doc('ATTRIBUTE')]
foo:
",
)
.run();
}

View File

@ -1183,3 +1183,78 @@ bar:
.args(["foo", "bar"])
.run();
}
#[test]
fn style_command_default() {
Test::new()
.justfile(
r#"
foo:
@echo '{{ style("command") }}foo{{NORMAL}}'
"#,
)
.stdout("\x1b[1mfoo\x1b[0m\n")
.run();
}
#[test]
fn style_command_non_default() {
Test::new()
.justfile(
r#"
foo:
@echo '{{ style("command") }}foo{{NORMAL}}'
"#,
)
.args(["--command-color", "red"])
.stdout("\x1b[1;31mfoo\x1b[0m\n")
.run();
}
#[test]
fn style_error() {
Test::new()
.justfile(
r#"
foo:
@echo '{{ style("error") }}foo{{NORMAL}}'
"#,
)
.stdout("\x1b[1;31mfoo\x1b[0m\n")
.run();
}
#[test]
fn style_warning() {
Test::new()
.justfile(
r#"
foo:
@echo '{{ style("warning") }}foo{{NORMAL}}'
"#,
)
.stdout("\x1b[1;33mfoo\x1b[0m\n")
.run();
}
#[test]
fn style_unknown() {
Test::new()
.justfile(
r#"
foo:
@echo '{{ style("hippo") }}foo{{NORMAL}}'
"#,
)
.stderr(
r#"
error: Call to function `style` failed: unknown style: `hippo`
justfile:2:13
2 @echo '{{ style("hippo") }}foo{{NORMAL}}'
^^^^^
"#,
)
.status(EXIT_FAILURE)
.run();
}

View File

@ -1429,3 +1429,58 @@ fn recipes_with_private_attribute_are_private() {
}),
);
}
#[test]
fn doc_attribute_overrides_comment() {
case(
"
# COMMENT
[doc('ATTRIBUTE')]
foo:
",
json!({
"aliases": {},
"assignments": {},
"first": "foo",
"doc": null,
"groups": [],
"modules": {},
"recipes": {
"foo": {
"attributes": [{"doc": "ATTRIBUTE"}],
"body": [],
"dependencies": [],
"doc": "ATTRIBUTE",
"name": "foo",
"namepath": "foo",
"parameters": [],
"priors": 0,
"private": false,
"quiet": false,
"shebang": false,
}
},
"settings": {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
"positional_arguments": false,
"quiet": false,
"shell": null,
"tempdir" : null,
"unstable": false,
"windows_powershell": false,
"windows_shell": null,
"working_directory" : null,
},
"unexports": [],
"warnings": [],
}),
);
}

View File

@ -452,7 +452,7 @@ fn backticks_highlighted() {
.stdout(
"
Available recipes:
recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[40;37m``\u{1b}[0m\u{1b}[34m \u{1b}[0m\u{1b}[40;37m`with backticks`\u{1b}[0m\u{1b}[34m and trailing text\u{1b}[0m
recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[36m``\u{1b}[0m\u{1b}[34m \u{1b}[0m\u{1b}[36m`with backticks`\u{1b}[0m\u{1b}[34m and trailing text\u{1b}[0m
")
.run();
}
@ -470,7 +470,7 @@ fn unclosed_backticks() {
.stdout(
"
Available recipes:
recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[40;37m`with unclosed backick\u{1b}[0m
recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[36m`with unclosed backick\u{1b}[0m
")
.run();
}

View File

@ -331,3 +331,95 @@ file := shell('cat file.txt')
.stdout("FILE\n")
.run();
}
#[test]
fn attribute_duplicate() {
Test::new()
.justfile(
r#"
[working-directory('bar')]
[working-directory('baz')]
print:
echo "$(basename "$PWD")"
"#,
)
.current_dir("foo")
.tree(tree! {
foo: {},
bar: {},
baz: {},
})
.args(["print"])
.stderr(
r#"error: Recipe attribute `working-directory` first used on line 1 is duplicated on line 2
justfile:2:2
2 [working-directory('baz')]
^^^^^^^^^^^^^^^^^
"#,
)
.stdout("")
.status(1)
.run();
}
#[test]
fn attribute() {
Test::new()
.justfile(
r#"
[working-directory('bar')]
print1:
echo "$(basename "$PWD")"
[working-directory('baz')]
[no-cd]
print2:
echo "$(basename "$PWD")"
"#,
)
.current_dir("foo")
.tree(tree! {
foo: {},
bar: {},
baz: {},
})
.args(["print1", "print2"])
.stderr(
r#"echo "$(basename "$PWD")"
echo "$(basename "$PWD")"
"#,
)
.stdout("bar\nfoo\n")
.run();
}
#[test]
fn setting_and_attribute() {
Test::new()
.justfile(
r#"
set working-directory := 'bar'
[working-directory('baz')]
print1:
echo "$(basename "$PWD")"
echo "$(basename "$(dirname "$PWD")")"
"#,
)
.current_dir("foo")
.tree(tree! {
foo: {},
bar: {
baz: {},
},
})
.args(["print1"])
.stderr(
r#"echo "$(basename "$PWD")"
echo "$(basename "$(dirname "$PWD")")"
"#,
)
.stdout("baz\nbar\n")
.run();
}