Support Cargo scaffolding and human output, and find boolean expressions
This commit is contained in:
@@ -7,7 +7,11 @@ The response contains newline-delimited JSON events.
|
||||
The final `exit` event contains a code and component locations.
|
||||
Each location contains an executable path and working directory.
|
||||
|
||||
The CLI forwards the bytes immediately.
|
||||
The CLI forwards human output immediately.
|
||||
The builder captures compiler JSON for artifact discovery without forwarding it to stdout.
|
||||
Compiler diagnostics remain on stderr. `cargo run` reserves stdout for the program.
|
||||
`cargo new` and `cargo init` use native Cargo, including `--lib`, `--name`, and `--edition`.
|
||||
They need no existing manifest. Subsequent builds use the world's default WASI target.
|
||||
For `test` and `run`, it executes the artifacts through the process interface.
|
||||
Cargo selects run binaries through a capture runner, so `--bin` and `default-run` retain their normal behavior.
|
||||
The test runner uses each package's root directory, even when the caller runs Cargo from a subdirectory.
|
||||
|
||||
+21
-9
@@ -66,7 +66,7 @@ impl Builder {
|
||||
ensure!(
|
||||
[
|
||||
"build", "check", "clippy", "test", "run", "fmt", "fetch", "add", "remove",
|
||||
"update", "tree", "metadata", "clean"
|
||||
"update", "tree", "metadata", "clean", "new", "init"
|
||||
]
|
||||
.contains(&action),
|
||||
"unsupported Cargo command"
|
||||
@@ -96,7 +96,14 @@ impl Builder {
|
||||
.checked_add(Duration::from_secs(seconds))
|
||||
.context("timeout too large")?;
|
||||
let world_cwd = sausage_vfs::resolve("/", &request.cwd);
|
||||
let workspace = files::workspace(self.root.as_ref(), &world_cwd).await?;
|
||||
let scaffold = matches!(action, "new" | "init");
|
||||
let workspace = if scaffold {
|
||||
files::workspace(self.root.as_ref(), &world_cwd)
|
||||
.await
|
||||
.unwrap_or_else(|_| world_cwd.clone())
|
||||
} else {
|
||||
files::workspace(self.root.as_ref(), &world_cwd).await?
|
||||
};
|
||||
let world_target = sausage_vfs::resolve(&workspace, "target");
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
@@ -140,10 +147,10 @@ impl Builder {
|
||||
fetch.arg("fetch");
|
||||
}
|
||||
fetch.current_dir(&cwd).env("CARGO_HOME", ®istry);
|
||||
let mut code = if matches!(action, "clean" | "fmt" | "remove") {
|
||||
let mut code = if scaffold || matches!(action, "clean" | "fmt" | "remove") {
|
||||
0
|
||||
} else {
|
||||
run(fetch, tx, deadline).await?.0
|
||||
run(fetch, tx, deadline, false).await?.0
|
||||
};
|
||||
if code == 0 && action == "add" {
|
||||
let mut fetch = Command::new("cargo");
|
||||
@@ -151,7 +158,7 @@ impl Builder {
|
||||
.arg("fetch")
|
||||
.current_dir(&cwd)
|
||||
.env("CARGO_HOME", ®istry);
|
||||
code = run(fetch, tx, deadline).await?.0;
|
||||
code = run(fetch, tx, deadline, false).await?.0;
|
||||
}
|
||||
let mut executables = Vec::new();
|
||||
if code == 0 {
|
||||
@@ -191,7 +198,7 @@ impl Builder {
|
||||
)?;
|
||||
let mut startup = Command::new("nu");
|
||||
startup.arg(tools.join("build.nu"));
|
||||
let (startup_code, _) = run(startup, tx, deadline).await?;
|
||||
let (startup_code, _) = run(startup, tx, deadline, false).await?;
|
||||
if startup_code != 0 {
|
||||
files::copy_back(self.root.as_ref(), &source, &workspace, Some(&before)).await?;
|
||||
self.evict(20 * 1024 * 1024 * 1024)?;
|
||||
@@ -303,7 +310,7 @@ impl Builder {
|
||||
]);
|
||||
}
|
||||
cmd.args(&args[split..]);
|
||||
let (status, stdout) = run(cmd, tx, deadline).await?;
|
||||
let (status, stdout) = run(cmd, tx, deadline, compiles).await?;
|
||||
code = status;
|
||||
if action == "clean" && code == 0 {
|
||||
files::clean_artifacts(self.root.as_ref(), &target, &world_target).await?;
|
||||
@@ -439,6 +446,7 @@ async fn pump(
|
||||
mut reader: impl tokio::io::AsyncRead + Unpin,
|
||||
tx: &mpsc::Sender<Event>,
|
||||
stderr: bool,
|
||||
silent_stdout: bool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let mut all = Vec::new();
|
||||
let mut bytes = vec![0; 8192];
|
||||
@@ -450,6 +458,9 @@ async fn pump(
|
||||
if !stderr {
|
||||
all.extend_from_slice(&bytes[..n]);
|
||||
}
|
||||
if !stderr && silent_stdout {
|
||||
continue;
|
||||
}
|
||||
tx.send(if stderr {
|
||||
Event::Stderr {
|
||||
bytes: bytes[..n].to_vec(),
|
||||
@@ -466,6 +477,7 @@ async fn run(
|
||||
mut cmd: Command,
|
||||
tx: &mpsc::Sender<Event>,
|
||||
deadline: Instant,
|
||||
silent_stdout: bool,
|
||||
) -> Result<(i32, Vec<u8>)> {
|
||||
cmd.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
@@ -479,8 +491,8 @@ async fn run(
|
||||
let collect = async {
|
||||
let (status, out, _) = tokio::try_join!(
|
||||
async { Ok::<_, anyhow::Error>(child.wait().await?) },
|
||||
pump(out, tx, false),
|
||||
pump(err, tx, true)
|
||||
pump(out, tx, false, silent_stdout),
|
||||
pump(err, tx, true, false)
|
||||
)?;
|
||||
Ok::<_, anyhow::Error>((status.code().unwrap_or(1), out))
|
||||
};
|
||||
|
||||
@@ -191,10 +191,9 @@ async fn timeout_kills_before_output_can_drain() -> Result<()> {
|
||||
let output = tx.clone();
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.args(["-c", "echo $$; exec sleep 1000"]);
|
||||
let task =
|
||||
tokio::spawn(
|
||||
async move { run(cmd, &output, Instant::now() + Duration::from_secs(1)).await },
|
||||
);
|
||||
let task = tokio::spawn(async move {
|
||||
run(cmd, &output, Instant::now() + Duration::from_secs(1), false).await
|
||||
});
|
||||
let Some(Event::Stdout { bytes }) =
|
||||
tokio::time::timeout(Duration::from_secs(2), rx.recv()).await?
|
||||
else {
|
||||
@@ -219,8 +218,9 @@ async fn closed_receiver_cancels_command() -> Result<()> {
|
||||
let (tx, mut rx) = mpsc::channel(1);
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.args(["-c", "printf ready; exec sleep 1000"]);
|
||||
let task =
|
||||
tokio::spawn(async move { run(cmd, &tx, Instant::now() + Duration::from_secs(60)).await });
|
||||
let task = tokio::spawn(async move {
|
||||
run(cmd, &tx, Instant::now() + Duration::from_secs(60), false).await
|
||||
});
|
||||
// Output must arrive before the command exits, not after collection completes.
|
||||
let event = tokio::time::timeout(Duration::from_secs(2), rx.recv())
|
||||
.await?
|
||||
|
||||
@@ -54,6 +54,81 @@ async fn cargo_at(
|
||||
.await?;
|
||||
Ok((code, String::from_utf8_lossy(&bytes).into_owned()))
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn cargo_scaffold_and_human_output() -> anyhow::Result<()> {
|
||||
let binaries = programs();
|
||||
let temp = tempfile::tempdir()?;
|
||||
let world = World::open(temp.path())?;
|
||||
let bin = temp.path().join("world/bin");
|
||||
std::fs::create_dir_all(&bin)?;
|
||||
std::fs::copy(binaries.join("cargo.wasm"), bin.join("cargo.wasm"))?;
|
||||
std::fs::copy(binaries.join("brush.wasm"), bin.join("sh.wasm"))?;
|
||||
let runtime = Runtime::new(world)?;
|
||||
for (script, cwd, expected, success) in [
|
||||
(
|
||||
"cargo new hello && cd hello && cargo run",
|
||||
"/",
|
||||
"Hello, world!\n",
|
||||
true,
|
||||
),
|
||||
("cargo build", "/hello", "", true),
|
||||
(
|
||||
"cargo new library --lib --name named_lib --edition 2021",
|
||||
"/",
|
||||
"",
|
||||
true,
|
||||
),
|
||||
("cargo check", "/library", "", true),
|
||||
(
|
||||
"cargo init --lib --name initialized --edition 2024",
|
||||
"/empty",
|
||||
"",
|
||||
true,
|
||||
),
|
||||
("cargo check", "/empty", "", true),
|
||||
("cargo new broken --edition 1900", "/", "", false),
|
||||
] {
|
||||
std::fs::create_dir_all(temp.path().join("world/empty"))?;
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let code = runtime
|
||||
.exec(
|
||||
Command::Argv(vec!["/bin/sh.wasm".into(), "-c".into(), script.into()]),
|
||||
Options {
|
||||
cwd: cwd.into(),
|
||||
timeout: Some(Duration::from_secs(120)),
|
||||
..Default::default()
|
||||
},
|
||||
&mut |stream, bytes| match stream {
|
||||
sausage::exec::Stream::Stdout => stdout.extend_from_slice(bytes),
|
||||
sausage::exec::Stream::Stderr => stderr.extend_from_slice(bytes),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let stderr = String::from_utf8_lossy(&stderr);
|
||||
assert_eq!(code == 0, success, "{script}: {stderr}");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&stdout),
|
||||
expected,
|
||||
"{script}: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("\"reason\":\"compiler-artifact\""),
|
||||
"{stderr}"
|
||||
);
|
||||
if script == "cargo build" {
|
||||
assert!(stderr.contains("Finished"), "{stderr}");
|
||||
}
|
||||
}
|
||||
let root = temp.path().join("world");
|
||||
let manifest: toml::Value =
|
||||
toml::from_str(&std::fs::read_to_string(root.join("library/Cargo.toml"))?)?;
|
||||
assert_eq!(manifest["package"]["name"].as_str(), Some("named_lib"));
|
||||
assert_eq!(manifest["package"]["edition"].as_str(), Some("2021"));
|
||||
assert!(root.join("library/src/lib.rs").is_file());
|
||||
assert!(root.join("empty/src/lib.rs").is_file());
|
||||
Ok(())
|
||||
}
|
||||
fn cache_hits(base: &Path) -> anyhow::Result<u64> {
|
||||
let stats = std::process::Command::new("kache")
|
||||
.arg("stats")
|
||||
|
||||
@@ -25,7 +25,7 @@ fn error(value: impl std::fmt::Debug) -> io::Error {
|
||||
}
|
||||
fn local_help(argv: &[String]) -> Option<&'static str> {
|
||||
matches!(argv.first().map(String::as_str), Some("--help" | "-h" | "help")).then_some(
|
||||
"Usage: cargo <COMMAND> [OPTIONS]\n\nWorld commands: build, check, clippy, test, run, fmt, fetch, add, remove\n",
|
||||
"Usage: cargo <COMMAND> [OPTIONS]\n\nWorld commands: build, check, clippy, test, run, fmt, fetch, add, remove, new, init\n",
|
||||
)
|
||||
}
|
||||
pub fn run() -> io::Result<i32> {
|
||||
@@ -123,7 +123,9 @@ fn help_is_local_and_lists_world_commands() {
|
||||
for flag in ["--help", "-h", "help"] {
|
||||
let help = local_help(&[flag.into()]).expect("local help");
|
||||
assert!(help.starts_with("Usage: cargo <COMMAND> [OPTIONS]\n"));
|
||||
assert!(help.contains("build, check, clippy, test, run, fmt, fetch, add, remove"));
|
||||
assert!(
|
||||
help.contains("build, check, clippy, test, run, fmt, fetch, add, remove, new, init")
|
||||
);
|
||||
}
|
||||
assert!(local_help(&["build".into()]).is_none());
|
||||
assert!(local_help(&[]).is_none());
|
||||
|
||||
+153
-97
@@ -1,11 +1,15 @@
|
||||
//! Find world paths with short-circuit AND/OR predicates and directory pruning.
|
||||
//! Find world paths with precedence-aware, short-circuit predicates and directory pruning.
|
||||
use sausage_spawn::Command;
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
enum Predicate {
|
||||
True,
|
||||
Not(Box<Self>),
|
||||
And(Box<Self>, Box<Self>),
|
||||
Or(Box<Self>, Box<Self>),
|
||||
Name(globset::GlobMatcher),
|
||||
Path(globset::GlobMatcher),
|
||||
Type(String),
|
||||
@@ -15,75 +19,181 @@ enum Predicate {
|
||||
Print(bool),
|
||||
Exec(Vec<String>),
|
||||
}
|
||||
fn run() -> Result<i32, Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
let mut i = 0;
|
||||
let mut roots = Vec::new();
|
||||
while i < args.len() && !args[i].starts_with('-') {
|
||||
roots.push(PathBuf::from(&args[i]));
|
||||
i += 1;
|
||||
struct Parser<'a> {
|
||||
args: &'a [String],
|
||||
i: usize,
|
||||
maxdepth: usize,
|
||||
mindepth: usize,
|
||||
explicit_action: bool,
|
||||
}
|
||||
impl Parser<'_> {
|
||||
fn peek(&self) -> Option<&str> {
|
||||
self.args.get(self.i).map(String::as_str)
|
||||
}
|
||||
if roots.is_empty() {
|
||||
roots.push(".".into());
|
||||
fn or(&mut self) -> Result<Predicate> {
|
||||
let mut value = self.and()?;
|
||||
while matches!(self.peek(), Some("-o" | "-or")) {
|
||||
self.i += 1;
|
||||
value = Predicate::Or(Box::new(value), Box::new(self.and()?));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
let mut groups = vec![Vec::new()];
|
||||
let mut maxdepth = usize::MAX;
|
||||
let mut mindepth = 0;
|
||||
let mut explicit_action = false;
|
||||
while i < args.len() {
|
||||
let flag = &args[i];
|
||||
i += 1;
|
||||
let predicate = match flag.as_str() {
|
||||
"-o" | "-or" => {
|
||||
groups.push(Vec::new());
|
||||
continue;
|
||||
fn and(&mut self) -> Result<Predicate> {
|
||||
let mut value = self.unary()?;
|
||||
while !matches!(self.peek(), None | Some("-o" | "-or" | ")")) {
|
||||
if matches!(self.peek(), Some("-a" | "-and")) {
|
||||
self.i += 1;
|
||||
}
|
||||
value = Predicate::And(Box::new(value), Box::new(self.unary()?));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
fn unary(&mut self) -> Result<Predicate> {
|
||||
let flag = self.args.get(self.i).ok_or("missing expression")?;
|
||||
self.i += 1;
|
||||
Ok(match flag.as_str() {
|
||||
"!" | "-not" => Predicate::Not(Box::new(self.unary()?)),
|
||||
"(" => {
|
||||
let value = self.or()?;
|
||||
if self.peek() != Some(")") {
|
||||
return Err("missing closing parenthesis".into());
|
||||
}
|
||||
self.i += 1;
|
||||
value
|
||||
}
|
||||
"-a" | "-and" => continue,
|
||||
"-prune" => Predicate::Prune,
|
||||
"-empty" => Predicate::Empty,
|
||||
"-print" | "-print0" => {
|
||||
explicit_action = true;
|
||||
self.explicit_action = true;
|
||||
Predicate::Print(flag == "-print0")
|
||||
}
|
||||
"-name" | "-path" | "-type" | "-maxdepth" | "-mindepth" | "-newer" => {
|
||||
let value = args.get(i).ok_or("missing predicate argument")?;
|
||||
i += 1;
|
||||
let value = self.args.get(self.i).ok_or("missing predicate argument")?;
|
||||
self.i += 1;
|
||||
match flag.as_str() {
|
||||
"-name" => Predicate::Name(globset::Glob::new(value)?.compile_matcher()),
|
||||
"-path" => Predicate::Path(globset::Glob::new(value)?.compile_matcher()),
|
||||
"-type" => Predicate::Type(value.clone()),
|
||||
"-newer" => Predicate::Newer(std::fs::metadata(value)?.modified()?),
|
||||
"-maxdepth" => {
|
||||
maxdepth = value.parse()?;
|
||||
continue;
|
||||
self.maxdepth = value.parse()?;
|
||||
Predicate::True
|
||||
}
|
||||
_ => {
|
||||
mindepth = value.parse()?;
|
||||
continue;
|
||||
self.mindepth = value.parse()?;
|
||||
Predicate::True
|
||||
}
|
||||
}
|
||||
}
|
||||
"-exec" => {
|
||||
let start = i;
|
||||
while i < args.len() && args[i] != ";" {
|
||||
i += 1;
|
||||
let start = self.i;
|
||||
while self.i < self.args.len() && self.args[self.i] != ";" {
|
||||
self.i += 1;
|
||||
}
|
||||
if i == args.len() || i == start {
|
||||
if self.i == self.args.len() || self.i == start {
|
||||
return Err("-exec requires a command and ;".into());
|
||||
}
|
||||
explicit_action = true;
|
||||
let argv = args[start..i].to_vec();
|
||||
i += 1;
|
||||
self.explicit_action = true;
|
||||
let argv = self.args[start..self.i].to_vec();
|
||||
self.i += 1;
|
||||
Predicate::Exec(argv)
|
||||
}
|
||||
_ => return Err(format!("unsupported predicate: {flag}").into()),
|
||||
};
|
||||
groups.last_mut().unwrap().push(predicate);
|
||||
})
|
||||
}
|
||||
}
|
||||
fn matches(
|
||||
predicate: &Predicate,
|
||||
entry: &walkdir::DirEntry,
|
||||
prune: &mut bool,
|
||||
status: &mut i32,
|
||||
out: &mut impl Write,
|
||||
) -> Result<bool> {
|
||||
Ok(match predicate {
|
||||
Predicate::True => true,
|
||||
Predicate::Not(value) => !matches(value, entry, prune, status, out)?,
|
||||
Predicate::And(left, right) => {
|
||||
matches(left, entry, prune, status, out)? && matches(right, entry, prune, status, out)?
|
||||
}
|
||||
Predicate::Or(left, right) => {
|
||||
matches(left, entry, prune, status, out)? || matches(right, entry, prune, status, out)?
|
||||
}
|
||||
Predicate::Name(g) => g.is_match(entry.file_name()),
|
||||
Predicate::Path(g) => g.is_match(entry.path()),
|
||||
Predicate::Type(t) => match t.as_str() {
|
||||
"f" => entry.file_type().is_file(),
|
||||
"d" => entry.file_type().is_dir(),
|
||||
"l" => entry.file_type().is_symlink(),
|
||||
_ => return Err("unsupported file type".into()),
|
||||
},
|
||||
Predicate::Newer(time) => entry.metadata()?.modified()? > *time,
|
||||
Predicate::Empty => {
|
||||
if entry.file_type().is_dir() {
|
||||
std::fs::read_dir(entry.path())?.next().is_none()
|
||||
} else {
|
||||
entry.file_type().is_file() && entry.metadata()?.len() == 0
|
||||
}
|
||||
}
|
||||
Predicate::Prune => {
|
||||
*prune = true;
|
||||
true
|
||||
}
|
||||
Predicate::Print(null) => {
|
||||
out.write_all(entry.path().as_os_str().as_encoded_bytes())?;
|
||||
out.write_all(if *null { b"\0" } else { b"\n" })?;
|
||||
true
|
||||
}
|
||||
Predicate::Exec(argv) => {
|
||||
out.flush()?;
|
||||
let argv: Vec<_> = argv
|
||||
.iter()
|
||||
.map(|arg| arg.replace("{}", &entry.path().to_string_lossy()))
|
||||
.collect();
|
||||
match Command::new(&argv[0]).args(&argv[1..]).status() {
|
||||
Ok(code) => code == 0,
|
||||
Err(e) => {
|
||||
eprintln!("find: {e}");
|
||||
*status = 1;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
fn run() -> Result<i32> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
let mut parser = Parser {
|
||||
args: &args,
|
||||
i: 0,
|
||||
maxdepth: usize::MAX,
|
||||
mindepth: 0,
|
||||
explicit_action: false,
|
||||
};
|
||||
let mut roots = Vec::new();
|
||||
while parser
|
||||
.peek()
|
||||
.is_some_and(|s| !s.starts_with('-') && !matches!(s, "!" | "(" | ")"))
|
||||
{
|
||||
roots.push(PathBuf::from(&args[parser.i]));
|
||||
parser.i += 1;
|
||||
}
|
||||
if roots.is_empty() {
|
||||
roots.push(".".into());
|
||||
}
|
||||
let predicate = if parser.peek().is_none() {
|
||||
Predicate::True
|
||||
} else {
|
||||
parser.or()?
|
||||
};
|
||||
if parser.peek().is_some() {
|
||||
return Err("unexpected closing parenthesis".into());
|
||||
}
|
||||
let mut status = 0;
|
||||
let mut out = io::BufWriter::new(io::stdout().lock());
|
||||
for root in roots {
|
||||
let mut walk = walkdir::WalkDir::new(root).max_depth(maxdepth).into_iter();
|
||||
let mut walk = walkdir::WalkDir::new(root)
|
||||
.max_depth(parser.maxdepth)
|
||||
.into_iter();
|
||||
while let Some(entry) = walk.next() {
|
||||
let entry = match entry {
|
||||
Ok(e) => e,
|
||||
@@ -93,69 +203,15 @@ fn run() -> Result<i32, Box<dyn std::error::Error>> {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if entry.depth() < mindepth {
|
||||
if entry.depth() < parser.mindepth {
|
||||
continue;
|
||||
}
|
||||
let mut prune = false;
|
||||
let mut matches = false;
|
||||
for group in &groups {
|
||||
let mut matched = true;
|
||||
for predicate in group {
|
||||
matched = match predicate {
|
||||
Predicate::Name(g) => g.is_match(entry.file_name()),
|
||||
Predicate::Path(g) => g.is_match(entry.path()),
|
||||
Predicate::Type(t) => match t.as_str() {
|
||||
"f" => entry.file_type().is_file(),
|
||||
"d" => entry.file_type().is_dir(),
|
||||
"l" => entry.file_type().is_symlink(),
|
||||
_ => return Err("unsupported file type".into()),
|
||||
},
|
||||
Predicate::Newer(time) => entry.metadata()?.modified()? > *time,
|
||||
Predicate::Empty => {
|
||||
if entry.file_type().is_dir() {
|
||||
std::fs::read_dir(entry.path())?.next().is_none()
|
||||
} else {
|
||||
entry.file_type().is_file() && entry.metadata()?.len() == 0
|
||||
}
|
||||
}
|
||||
Predicate::Prune => {
|
||||
prune = true;
|
||||
true
|
||||
}
|
||||
Predicate::Print(null) => {
|
||||
out.write_all(entry.path().as_os_str().as_encoded_bytes())?;
|
||||
out.write_all(if *null { b"\0" } else { b"\n" })?;
|
||||
true
|
||||
}
|
||||
Predicate::Exec(argv) => {
|
||||
out.flush()?;
|
||||
let argv: Vec<_> = argv
|
||||
.iter()
|
||||
.map(|arg| arg.replace("{}", &entry.path().to_string_lossy()))
|
||||
.collect();
|
||||
match Command::new(&argv[0]).args(&argv[1..]).status() {
|
||||
Ok(code) => code == 0,
|
||||
Err(e) => {
|
||||
eprintln!("find: {e}");
|
||||
status = 1;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if !matched {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let matched = matches(&predicate, &entry, &mut prune, &mut status, &mut out)?;
|
||||
if prune && entry.file_type().is_dir() {
|
||||
walk.skip_current_dir();
|
||||
}
|
||||
if matches && !explicit_action {
|
||||
if matched && !parser.explicit_action {
|
||||
out.write_all(entry.path().as_os_str().as_encoded_bytes())?;
|
||||
out.write_all(b"\n")?;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ def main [host: path, bundle: path] {
|
||||
mkdir ($fixture | path join skip/deep) ($fixture | path join keep/deep) ($fixture | path join empty)
|
||||
'hidden' | save ($fixture | path join skip/deep/file)
|
||||
'shown' | save ($fixture | path join keep/deep/file)
|
||||
mkdir ($fixture | path join target)
|
||||
'artifact' | save ($fixture | path join target output)
|
||||
'' | save ($fixture | path join zero)
|
||||
'' | save ($fixture | path join reference)
|
||||
^touch -d '2001-01-01 UTC' ($fixture | path join reference)
|
||||
for args in [[-name skip -prune -o -type f -print] [-path './skip' -prune -o -print] [-name skip -prune -o -type f] [-maxdepth 1] [-mindepth 1 -maxdepth 2] [-empty] [-newer reference] [-path './keep/*']] {
|
||||
for args in [[-name skip -prune -o -type f -print] [-path './skip' -prune -o -print] [-name skip -prune -o -type f] [-maxdepth 1] [-mindepth 1 -maxdepth 2] [-empty] [-newer reference] [-path './keep/*'] [-type f -not -path './target/*'] ['!' -type d] [-not -not -type f] ['(' -name zero -o -name reference ')' -a -type f] [-name zero -o -name reference -a -type d] [-not '(' -name skip -o -name keep ')' -a -type d] [-name skip -prune -o '(' -type f -a '!' -name reference ')' -print] [-name zero -o -name reference -print] ['(' -name zero -o -name reference ')' -print] [-not] ['(' -type f] [-type f -a]] {
|
||||
let native = (do { cd $fixture; ^find . ...$args | complete })
|
||||
let command = ($args | each {|arg| $arg | into string | to json -r} | str join ' ')
|
||||
let guest = (^$host sh $world -c $"cd /fixture; find . ($command)" | complete)
|
||||
|
||||
Reference in New Issue
Block a user