diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d55080..267bc033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,39 @@ breaking entries are marked **BREAKING**. ## [Unreleased] +### Fixed + +- `grep` now reserves exit 1 for "no lines matched". An invalid pattern, an + unreadable file, or a missing pattern argument exits 2, so a caller cannot + read a broken search as a negative answer. `diff` argument errors exit 2 to + match its neighboring operand checks. +- A program kaish refuses — lex, parse, or validation — now exits 2 from + `kaish -c` and from a script file, matching what `kaish --plan` already + documented for the same source. +- `kaish --plan` now runs the validator. A program that parses but the kernel + would reject reports `{"errors": [...]}` and exits 2 instead of printing a + clean plan the caller cannot run. +- An invalid regex names the escape that fixes it (`\[` for a literal `[`, + `[(]` and `[{]` where a backslash would be a BRE operator) instead of + linking kaish's regex crate. A pattern with two faults gets no hint rather + than one that still does not compile. +- A pattern that arrives through a variable (`p='[cast:'; grep "$p" f`) now + exits 2 like a literal one. The validator skips a computed pattern, so the + failure surfaced from the regex builders inside `grep` instead. +- `grep` reading from a pipe no longer discards a read error and reports it as + "no lines matched". A read failure exits 2; a downstream close still keeps + the match-based code. + +### Changed + +- **BREAKING** (`kaish-tool-api`): `ToolCtx` is sealed. Tool authors receive a + `ToolCtx` and never implement one, so this changes no supported use, but an + out-of-tree implementation no longer compiles. +- A kernel builtin dispatched with a context that is not the kernel's now + panics instead of returning exit 1 with an internal message. Sealing + `ToolCtx` is what makes that branch unreachable: `ToolRegistry::get` and + `Tool::execute` are public, so type privacy alone left it open. + ## [0.17.2] - 2026-09-09 ### Fixed diff --git a/crates/kaish-kernel/src/tools/builtin/alias.rs b/crates/kaish-kernel/src/tools/builtin/alias.rs index 67fa6a1b..6e678e64 100644 --- a/crates/kaish-kernel/src/tools/builtin/alias.rs +++ b/crates/kaish-kernel/src/tools/builtin/alias.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Alias tool: define, list, or show command aliases. /// @@ -45,9 +45,7 @@ impl Tool for Alias { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // For alias, args.named carries user-defined name=value pairs that // clap can't know about. Synthesise an argv with just flags so clap // sees only the global --json; we read name=value off args.named below. @@ -172,9 +170,7 @@ impl Tool for Unalias { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("unalias: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/assert.rs b/crates/kaish-kernel/src/tools/builtin/assert.rs index fa441d5d..8a1029ab 100644 --- a/crates/kaish-kernel/src/tools/builtin/assert.rs +++ b/crates/kaish-kernel/src/tools/builtin/assert.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Assert tool: verify conditions in tests. pub struct Assert; @@ -40,9 +40,7 @@ impl Tool for Assert { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("assert: {e}")), @@ -95,6 +93,7 @@ fn is_truthy(value: &Value) -> bool { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/awk.rs b/crates/kaish-kernel/src/tools/builtin/awk.rs index 357bc11c..51a28ef0 100644 --- a/crates/kaish-kernel/src/tools/builtin/awk.rs +++ b/crates/kaish-kernel/src/tools/builtin/awk.rs @@ -17,7 +17,7 @@ use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; use crate::tools::builtin::read_repeatable_strings; use crate::tools::builtin::regex_dialect::{append_dialect_hint, bre_metas_to_ere}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Compile an awk ERE pattern, first rewriting the GNU BRE backslash-metas to /// ERE so `\|`/`\(…\)`/`\{N\}` behave as operators (issue #60). awk is ERE-only @@ -83,9 +83,7 @@ impl Tool for Awk { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("awk: {e}")), @@ -2856,6 +2854,7 @@ impl AwkRuntime { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/base64_tool.rs b/crates/kaish-kernel/src/tools/builtin/base64_tool.rs index 64d56151..d756c093 100644 --- a/crates/kaish-kernel/src/tools/builtin/base64_tool.rs +++ b/crates/kaish-kernel/src/tools/builtin/base64_tool.rs @@ -9,7 +9,7 @@ use base64::Engine; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Base64 tool: encode or decode base64 data. pub struct Base64Tool; @@ -53,9 +53,7 @@ impl Tool for Base64Tool { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Tests poke `args.named.insert("decode", Value::Bool(true))` directly; // to_argv would render that as `--decode=true` which clap won't accept // for a bool field. Promote any Bool-typed named entries to flags so @@ -150,6 +148,7 @@ fn wrap_lines(s: &str, width: usize) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/basename.rs b/crates/kaish-kernel/src/tools/builtin/basename.rs index 4ad19155..b6e40520 100644 --- a/crates/kaish-kernel/src/tools/builtin/basename.rs +++ b/crates/kaish-kernel/src/tools/builtin/basename.rs @@ -8,7 +8,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Basename tool: extract filename from path. pub struct Basename; @@ -43,9 +43,7 @@ impl Tool for Basename { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("basename: {e}")), @@ -91,6 +89,7 @@ impl Tool for Basename { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/bg.rs b/crates/kaish-kernel/src/tools/builtin/bg.rs index 1212494a..9ef41ab9 100644 --- a/crates/kaish-kernel/src/tools/builtin/bg.rs +++ b/crates/kaish-kernel/src/tools/builtin/bg.rs @@ -10,7 +10,7 @@ use crate::interpreter::ExecResult; use crate::interpreter::OutputData; #[cfg(unix)] use crate::scheduler::JobId; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Bg tool: resume a stopped job in the background. pub struct Bg; @@ -55,9 +55,7 @@ impl Tool for Bg { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("bg: {e}")), @@ -189,6 +187,7 @@ impl Tool for Bg { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::scheduler::JobManager; use crate::vfs::{MemoryFs, VfsRouter}; use std::os::unix::process::CommandExt; diff --git a/crates/kaish-kernel/src/tools/builtin/cat.rs b/crates/kaish-kernel/src/tools/builtin/cat.rs index 8ccec91f..da45a4cb 100644 --- a/crates/kaish-kernel/src/tools/builtin/cat.rs +++ b/crates/kaish-kernel/src/tools/builtin/cat.rs @@ -9,7 +9,7 @@ use crate::ast::Value; use crate::backend::ReadRange; use crate::interpreter::{ExecResult, OutputData}; use crate::scheduler::PipeWriter; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Cat tool: read and output file contents. pub struct Cat; @@ -49,9 +49,7 @@ impl Tool for Cat { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("cat: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/cd.rs b/crates/kaish-kernel/src/tools/builtin/cd.rs index 1b3eae7d..2d06c46b 100644 --- a/crates/kaish-kernel/src/tools/builtin/cd.rs +++ b/crates/kaish-kernel/src/tools/builtin/cd.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Cd tool: change current working directory. pub struct Cd; @@ -44,9 +44,7 @@ impl Tool for Cd { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("cd: {e}")), @@ -113,6 +111,7 @@ impl Tool for Cd { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::path::PathBuf; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/checksum.rs b/crates/kaish-kernel/src/tools/builtin/checksum.rs index 28bca739..45dbc068 100644 --- a/crates/kaish-kernel/src/tools/builtin/checksum.rs +++ b/crates/kaish-kernel/src/tools/builtin/checksum.rs @@ -8,7 +8,7 @@ use digest::Digest; use crate::interpreter::{ExecResult, OutputData, OutputNode}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Checksum tool: compute or verify file hashes. pub struct Checksum; @@ -65,9 +65,7 @@ impl Tool for Checksum { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("checksum: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/cmp.rs b/crates/kaish-kernel/src/tools/builtin/cmp.rs index cd142b70..9cfcfccd 100644 --- a/crates/kaish-kernel/src/tools/builtin/cmp.rs +++ b/crates/kaish-kernel/src/tools/builtin/cmp.rs @@ -11,7 +11,7 @@ use std::path::Path; use crate::backend::ReadRange; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// cmp tool. pub struct Cmp; @@ -51,9 +51,7 @@ impl Tool for Cmp { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); args.flagify_bool_named(&self.schema()); let argv = match args.to_argv() { Ok(v) => v, diff --git a/crates/kaish-kernel/src/tools/builtin/cp.rs b/crates/kaish-kernel/src/tools/builtin/cp.rs index 60acfa61..3bf71250 100644 --- a/crates/kaish-kernel/src/tools/builtin/cp.rs +++ b/crates/kaish-kernel/src/tools/builtin/cp.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use crate::backend::{BackendError, KernelBackend, WriteMode}; use crate::interpreter::ExecResult; use crate::operation::KernelOperation; -use crate::tools::{cas_overwrite, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, cas_overwrite, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Cp tool: copy files and directories. pub struct Cp; @@ -62,9 +62,7 @@ impl Tool for Cp { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("cp: {e}")), @@ -389,6 +387,7 @@ fn copy_dir_recursive<'a>( #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/cut.rs b/crates/kaish-kernel/src/tools/builtin/cut.rs index 6b003740..226bcc4e 100644 --- a/crates/kaish-kernel/src/tools/builtin/cut.rs +++ b/crates/kaish-kernel/src/tools/builtin/cut.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Cut tool: select portions of each line. pub struct Cut; @@ -60,9 +60,7 @@ impl Tool for Cut { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("cut: {e}")), @@ -228,6 +226,7 @@ fn select_indices(spec: &str, max_len: usize) -> Vec { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/date.rs b/crates/kaish-kernel/src/tools/builtin/date.rs index bb51f089..598a5743 100644 --- a/crates/kaish-kernel/src/tools/builtin/date.rs +++ b/crates/kaish-kernel/src/tools/builtin/date.rs @@ -37,7 +37,7 @@ use chrono_tz::Tz; use clap::{CommandFactory, Parser}; use crate::interpreter::{value_to_string, ExecResult, OutputData}; -use crate::tools::{ +use crate::tools::{exec_context, schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema, }; @@ -185,9 +185,7 @@ impl Tool for Date { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("date: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/dd.rs b/crates/kaish-kernel/src/tools/builtin/dd.rs index 05fe4452..a3e31fe1 100644 --- a/crates/kaish-kernel/src/tools/builtin/dd.rs +++ b/crates/kaish-kernel/src/tools/builtin/dd.rs @@ -14,7 +14,7 @@ use crate::ast::Value; use crate::backend::ReadRange; use crate::interpreter::{value_to_string, ExecResult}; use crate::operation::KernelOperation; -use crate::tools::{ExecContext, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, Tool, ToolArgs, ToolCtx, ToolSchema}; /// dd tool. pub struct Dd; @@ -56,9 +56,7 @@ impl Tool for Dd { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let mut input: Option = None; let mut output: Option = None; @@ -198,6 +196,7 @@ impl Tool for Dd { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{DevFs, Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/diff.rs b/crates/kaish-kernel/src/tools/builtin/diff.rs index dcc9dbc7..1065139a 100644 --- a/crates/kaish-kernel/src/tools/builtin/diff.rs +++ b/crates/kaish-kernel/src/tools/builtin/diff.rs @@ -17,7 +17,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::{IssueCode, ValidationIssue}; /// Diff tool: compares two files line by line. @@ -114,9 +114,7 @@ impl Tool for Diff { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("diff: {e}")), @@ -148,13 +146,13 @@ impl Tool for Diff { let file1 = match get_path_string(&args, "file1", 0) { Ok(Some(f)) => f, Ok(None) => return ExecResult::failure(2, "diff: missing first file"), - Err(e) => return ExecResult::failure(1, format!("diff: {e}")), + Err(e) => return ExecResult::failure(2, format!("diff: {e}")), }; let file2 = match get_path_string(&args, "file2", 1) { Ok(Some(f)) => f, Ok(None) => return ExecResult::failure(2, "diff: missing second file"), - Err(e) => return ExecResult::failure(1, format!("diff: {e}")), + Err(e) => return ExecResult::failure(2, format!("diff: {e}")), }; let path1 = ctx.resolve_path(&file1); @@ -349,6 +347,7 @@ fn colorize_unified_output(plain: &str) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/dirname.rs b/crates/kaish-kernel/src/tools/builtin/dirname.rs index 6922844f..9e26adf3 100644 --- a/crates/kaish-kernel/src/tools/builtin/dirname.rs +++ b/crates/kaish-kernel/src/tools/builtin/dirname.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use std::path::Path; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Dirname tool: extract directory from path. pub struct Dirname; @@ -40,9 +40,7 @@ impl Tool for Dirname { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("dirname: {e}")), @@ -89,6 +87,7 @@ impl Tool for Dirname { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/echo.rs b/crates/kaish-kernel/src/tools/builtin/echo.rs index aa575925..978dc3c4 100644 --- a/crates/kaish-kernel/src/tools/builtin/echo.rs +++ b/crates/kaish-kernel/src/tools/builtin/echo.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Echo tool: prints arguments to stdout. pub struct Echo; @@ -43,9 +43,7 @@ impl Tool for Echo { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Preserve the original Value rendering (true/false/3.14/etc.) before // collapsing through to_argv()'s string layer — echo formats numerics // specially and we don't want clap to re-stringify them. `echo` is a @@ -95,6 +93,7 @@ impl Tool for Echo { #[allow(clippy::approx_constant)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/env.rs b/crates/kaish-kernel/src/tools/builtin/env.rs index 993fbe3c..27646120 100644 --- a/crates/kaish-kernel/src/tools/builtin/env.rs +++ b/crates/kaish-kernel/src/tools/builtin/env.rs @@ -16,7 +16,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::read_repeatable_strings; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::ValidationIssue; /// Env tool: print environment or run command with modified environment. @@ -122,9 +122,7 @@ impl Tool for Env { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("env: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/exec.rs b/crates/kaish-kernel/src/tools/builtin/exec.rs index 872debf1..22947c53 100644 --- a/crates/kaish-kernel/src/tools/builtin/exec.rs +++ b/crates/kaish-kernel/src/tools/builtin/exec.rs @@ -20,8 +20,8 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; use crate::tools::builtin::get_path_string; -use crate::tools::{ - schema_from_clap, ExecContext, ExternalCommandsUnavailable, GlobalFlags, Tool, ToolArgs, +use crate::tools::{exec_context, + schema_from_clap, ExternalCommandsUnavailable, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema, }; @@ -64,9 +64,7 @@ impl Tool for Exec { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("exec: {e}")), @@ -184,6 +182,7 @@ fn value_to_string(value: &Value) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/export.rs b/crates/kaish-kernel/src/tools/builtin/export.rs index a8bbd9af..82d92f16 100644 --- a/crates/kaish-kernel/src/tools/builtin/export.rs +++ b/crates/kaish-kernel/src/tools/builtin/export.rs @@ -14,7 +14,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::ValidationIssue; /// Export tool: marks variables for export to child processes. @@ -79,9 +79,7 @@ impl Tool for Export { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // For export, args.named carries user-defined VAR=value pairs that // clap can't know about. Synthesise an argv with just flags so clap // parses `-p` / `--json` cleanly; we read VAR=value off args.named diff --git a/crates/kaish-kernel/src/tools/builtin/fg.rs b/crates/kaish-kernel/src/tools/builtin/fg.rs index 5aba4423..5042692c 100644 --- a/crates/kaish-kernel/src/tools/builtin/fg.rs +++ b/crates/kaish-kernel/src/tools/builtin/fg.rs @@ -8,7 +8,7 @@ use crate::ast::Value; use crate::interpreter::ExecResult; #[cfg(unix)] use crate::scheduler::JobId; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Fg tool: resume a stopped job in the foreground. pub struct Fg; @@ -89,9 +89,7 @@ impl Tool for Fg { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("fg: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/file.rs b/crates/kaish-kernel/src/tools/builtin/file.rs index 0bc6a68e..e3e2ef26 100644 --- a/crates/kaish-kernel/src/tools/builtin/file.rs +++ b/crates/kaish-kernel/src/tools/builtin/file.rs @@ -14,7 +14,7 @@ use kaish_glob::{FileType, SNIFF_PREFIX_LEN}; use kaish_types::ReadRange; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// file tool: identify file type from its leading bytes. pub struct File; @@ -59,9 +59,7 @@ impl Tool for File { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("file: {e}")), @@ -196,6 +194,7 @@ fn render_line(name: &str, desc: &str, brief: bool) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::path::Path; diff --git a/crates/kaish-kernel/src/tools/builtin/find.rs b/crates/kaish-kernel/src/tools/builtin/find.rs index 05b3f880..147947ee 100644 --- a/crates/kaish-kernel/src/tools/builtin/find.rs +++ b/crates/kaish-kernel/src/tools/builtin/find.rs @@ -27,7 +27,7 @@ use crate::backend_walker_fs::BackendWalkerFs; use crate::vfs::DirEntry; use crate::ignore_config::IgnoreScope; use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::walker::{EntryTypes, FileWalker, GlobPath, WalkOptions}; /// Find tool: searches for files in directory hierarchy. @@ -105,9 +105,7 @@ impl Tool for Find { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("find: {e}")), @@ -560,6 +558,7 @@ fn parse_size_filter(s: &str) -> Option<(char, u64)> { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/fromjson.rs b/crates/kaish-kernel/src/tools/builtin/fromjson.rs index ceec0bc8..867f82d4 100644 --- a/crates/kaish-kernel/src/tools/builtin/fromjson.rs +++ b/crates/kaish-kernel/src/tools/builtin/fromjson.rs @@ -29,7 +29,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// fromjson tool: parse one JSON document into a structured value. pub struct FromJson; @@ -68,9 +68,7 @@ impl Tool for FromJson { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("fromjson: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/fromjsonl.rs b/crates/kaish-kernel/src/tools/builtin/fromjsonl.rs index d7a650d2..ef977ac7 100644 --- a/crates/kaish-kernel/src/tools/builtin/fromjsonl.rs +++ b/crates/kaish-kernel/src/tools/builtin/fromjsonl.rs @@ -37,7 +37,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// fromjsonl tool: parse JSONL text into a typed list. pub struct FromJsonl; @@ -76,9 +76,7 @@ impl Tool for FromJsonl { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("fromjsonl: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/gather.rs b/crates/kaish-kernel/src/tools/builtin/gather.rs index e6fc6cf4..15107632 100644 --- a/crates/kaish-kernel/src/tools/builtin/gather.rs +++ b/crates/kaish-kernel/src/tools/builtin/gather.rs @@ -19,7 +19,7 @@ use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; use crate::scheduler::parse_gather_options; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Gather tool: collect results from parallel processing. /// @@ -73,9 +73,7 @@ impl Tool for Gather { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("gather: {e}")), @@ -112,6 +110,7 @@ impl Tool for Gather { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; #[tokio::test] async fn test_gather_standalone_passthrough() { diff --git a/crates/kaish-kernel/src/tools/builtin/glob.rs b/crates/kaish-kernel/src/tools/builtin/glob.rs index 972aea42..fcb030e7 100644 --- a/crates/kaish-kernel/src/tools/builtin/glob.rs +++ b/crates/kaish-kernel/src/tools/builtin/glob.rs @@ -7,7 +7,7 @@ use crate::ast::Value; use crate::backend_walker_fs::BackendWalkerFs; use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode}; use crate::tools::builtin::read_repeatable_strings; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::walker::{ build_file_types, list_file_types, EntryTypes, FileWalker, GlobPath, IncludeExclude, WalkOptions, }; @@ -98,9 +98,7 @@ impl Tool for Glob { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Tests poke args.named.insert("no-ignore", Value::Bool(true)); promote // such bool-typed named entries to flag form so clap accepts them. args.flagify_bool_named(&self.schema()); @@ -335,6 +333,7 @@ impl Tool for Glob { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::path::Path; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/grep.rs b/crates/kaish-kernel/src/tools/builtin/grep.rs index 58a18144..2bca6681 100644 --- a/crates/kaish-kernel/src/tools/builtin/grep.rs +++ b/crates/kaish-kernel/src/tools/builtin/grep.rs @@ -16,8 +16,8 @@ use crate::backend_walker_fs::BackendWalkerFs; use crate::interpreter::{ExecResult, OutputData, OutputNode}; use crate::tools::builtin::grep_engine::{AccumulatorSink, ContextKind, SearchEvent}; use crate::tools::builtin::read_repeatable_strings; -use crate::tools::builtin::regex_dialect::{append_dialect_hint, bre_metas_to_ere}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema, validate_against_schema}; +use crate::tools::builtin::regex_dialect::{append_dialect_hint, bre_metas_to_ere, regex_fix_hint}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema, validate_against_schema}; use crate::validator::{IssueCode, ValidationIssue}; use crate::walker::{ build_file_types, list_file_types, FileWalker, GlobPath, IncludeExclude, WalkOptions, @@ -190,7 +190,10 @@ impl Tool for Grep { Some("-E"), ), ) - .with_suggestion("check regex syntax at https://docs.rs/regex") + .with_suggestion( + regex_fix_hint(&rewritten) + .unwrap_or("escape the literal character the engine could not place"), + ) .with_command(self.name())); } } @@ -199,9 +202,7 @@ impl Tool for Grep { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); args.flagify_bool_named(&self.schema()); let argv = match args.to_argv() { @@ -250,7 +251,7 @@ impl Tool for Grep { let pattern = match args.get_string("pattern", 0) { Some(p) => p, - None => return ExecResult::failure(1, "grep: missing pattern argument"), + None => return ExecResult::failure(2, "grep: missing pattern argument"), }; let ignore_case = args.has_flag("ignore-case") || args.has_flag("i"); @@ -338,7 +339,7 @@ impl Tool for Grep { Ok(r) => r, Err(e) => { return ExecResult::failure( - 1, + 2, append_dialect_hint( format!("grep: invalid pattern: {}", e), dialect_rewrote, @@ -357,7 +358,7 @@ impl Tool for Grep { Ok(m) => m, Err(e) => { return ExecResult::failure( - 1, + 2, append_dialect_hint( format!("grep: invalid pattern: {}", e), dialect_rewrote, @@ -401,7 +402,7 @@ impl Tool for Grep { let operands: Vec = match crate::interpreter::values_to_text_sink_named(&args.positional[1..], "a path") { Ok(p) => p, - Err(e) => return ExecResult::failure(1, format!("grep: {e}")), + Err(e) => return ExecResult::failure(2, format!("grep: {e}")), }; // GNU distinguishes a WRITTEN `.` from a defaulted one: // `grep -r p .` reports `./d/a.txt` while a bare `grep -r p` @@ -484,7 +485,7 @@ impl Tool for Grep { match walker.collect().await { Ok(f) => files.extend(f), - Err(e) => return ExecResult::failure(1, format!("grep: {}", e)), + Err(e) => return ExecResult::failure(2, format!("grep: {}", e)), } } @@ -546,7 +547,7 @@ impl Tool for Grep { let file_operands: Vec = match crate::interpreter::values_to_text_sink_named(&args.positional[1..], "a path") { Ok(p) => p, - Err(e) => return ExecResult::failure(1, format!("grep: {e}")), + Err(e) => return ExecResult::failure(2, format!("grep: {e}")), }; if file_operands.len() > 1 { let root = ctx.resolve_path("."); @@ -624,7 +625,7 @@ impl Tool for Grep { // I/O error reading the file. if let Err(e) = scan_result { - return ExecResult::failure(1, format!("grep: {}: {}", path, e)); + return ExecResult::failure(2, format!("grep: {}: {}", path, e)); } // Flush the remaining carry. `saw_invalid_utf8` is set if any @@ -665,7 +666,7 @@ impl Tool for Grep { let resolved = ctx.resolve_path(&path); match ctx.backend.read(Path::new(&resolved), None).await { Ok(data) => (data, Some(path)), - Err(e) => return ExecResult::failure(1, format!("grep: {}: {}", path, e)), + Err(e) => return ExecResult::failure(2, format!("grep: {}: {}", path, e)), } } None => { @@ -693,7 +694,7 @@ impl Tool for Grep { filename.as_deref(), ) { Ok(t) => t, - Err(e) => return ExecResult::failure(1, format!("grep: {e}")), + Err(e) => return ExecResult::failure(2, format!("grep: {e}")), }; // Quiet mode: just return exit code @@ -756,6 +757,10 @@ impl Grep { let mut reader = BufReader::new(pipe_in); let mut match_count = 0usize; let mut line_num = 0usize; + // A read failure is grep's own trouble and exits 2. A *write* failure + // is the downstream stage closing the pipe (`grep x | head -1`), which + // is ordinary and keeps the match-based code. + let mut read_error: Option = None; let mut line_buf = String::new(); loop { @@ -788,13 +793,19 @@ impl Grep { } } } - Err(_) => break, + Err(e) => { + read_error = Some(e); + break; + } } } drop(reader); let _ = pipe_out.shutdown().await; + if let Some(e) = read_error { + return ExecResult::failure(2, format!("grep: {e}")); + } if match_count > 0 { ExecResult::success("") } else { diff --git a/crates/kaish-kernel/src/tools/builtin/head.rs b/crates/kaish-kernel/src/tools/builtin/head.rs index 85ed1288..621eba45 100644 --- a/crates/kaish-kernel/src/tools/builtin/head.rs +++ b/crates/kaish-kernel/src/tools/builtin/head.rs @@ -7,7 +7,7 @@ use std::path::Path; use crate::ast::Value; use crate::backend::ReadRange; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Head tool: output the first part of files or stdin. pub struct Head; @@ -52,9 +52,7 @@ impl Tool for Head { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Pop the POSIX shorthand `-N` Int before we hand off to clap below. // The pop transforms positional[0] = Int(-N) into named lines=N. // Handle POSIX shorthand: head -3 file → head -n 3 file diff --git a/crates/kaish-kernel/src/tools/builtin/help.rs b/crates/kaish-kernel/src/tools/builtin/help.rs index 1f48ed97..6bd98999 100644 --- a/crates/kaish-kernel/src/tools/builtin/help.rs +++ b/crates/kaish-kernel/src/tools/builtin/help.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::help::{get_help, HelpTopic}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Help tool: display help for topics and tools. pub struct Help; @@ -41,9 +41,7 @@ impl Tool for Help { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("help: {e}")), @@ -66,6 +64,7 @@ impl Tool for Help { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::tools::ParamSchema; use crate::vfs::{MemoryFs, VfsRouter}; diff --git a/crates/kaish-kernel/src/tools/builtin/hostname.rs b/crates/kaish-kernel/src/tools/builtin/hostname.rs index 2939f40b..0ecebdb0 100644 --- a/crates/kaish-kernel/src/tools/builtin/hostname.rs +++ b/crates/kaish-kernel/src/tools/builtin/hostname.rs @@ -10,7 +10,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use super::uname::read_hostname; @@ -47,9 +47,7 @@ impl Tool for Hostname { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("hostname: {e}")), @@ -69,6 +67,7 @@ impl Tool for Hostname { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/ignore.rs b/crates/kaish-kernel/src/tools/builtin/ignore.rs index 607f8074..6c44c2e2 100644 --- a/crates/kaish-kernel/src/tools/builtin/ignore.rs +++ b/crates/kaish-kernel/src/tools/builtin/ignore.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ignore_config::IgnoreScope; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Ignore tool: inspect and modify ignore file configuration. pub struct KaishIgnore; @@ -45,9 +45,7 @@ impl Tool for KaishIgnore { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-ignore: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/introspect.rs b/crates/kaish-kernel/src/tools/builtin/introspect.rs index e1d14b80..0a9d351e 100644 --- a/crates/kaish-kernel/src/tools/builtin/introspect.rs +++ b/crates/kaish-kernel/src/tools/builtin/introspect.rs @@ -6,7 +6,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; // ============================================================================ // kaish-tools — List available tools @@ -45,9 +45,7 @@ impl Tool for Tools { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-tools: {e}")), @@ -188,9 +186,7 @@ impl Tool for Mounts { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-mounts: {e}")), @@ -280,6 +276,7 @@ impl Tool for Mounts { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::interpreter::{apply_output_format, OutputFormat}; use crate::tools::{ParamSchema, ToolSchema as TS}; diff --git a/crates/kaish-kernel/src/tools/builtin/jobs.rs b/crates/kaish-kernel/src/tools/builtin/jobs.rs index 5a0c772d..ce57a1d8 100644 --- a/crates/kaish-kernel/src/tools/builtin/jobs.rs +++ b/crates/kaish-kernel/src/tools/builtin/jobs.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; use crate::scheduler::JobInfo; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Build `jobs --json` rows: the full serialized `JobInfo` (GH #241 — id, /// status, command, exit_code, started_at/finished_at, pgids, ... — @@ -71,9 +71,7 @@ impl Tool for Jobs { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("jobs: {e}")), @@ -140,6 +138,7 @@ impl Tool for Jobs { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::scheduler::JobManager; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/jq_native.rs b/crates/kaish-kernel/src/tools/builtin/jq_native.rs index c641b0e7..8d910704 100644 --- a/crates/kaish-kernel/src/tools/builtin/jq_native.rs +++ b/crates/kaish-kernel/src/tools/builtin/jq_native.rs @@ -28,7 +28,7 @@ use jaq_std::ValT as _; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, ParamSchema, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, ParamSchema, Tool, ToolArgs, ToolSchema}; use crate::validator::{IssueCode, ValidationIssue}; /// Native jq tool using jaq (pure Rust jq implementation). @@ -516,9 +516,7 @@ impl Tool for JqNative { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("jq: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_ast.rs b/crates/kaish-kernel/src/tools/builtin/kaish_ast.rs index aa08cb7e..4b657994 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_ast.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_ast.rs @@ -13,7 +13,7 @@ use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; use crate::parser::parse; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// kaish-ast: parse expressions and display their AST. pub struct KaishAst; @@ -57,9 +57,7 @@ impl Tool for KaishAst { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-ast: {e}")), @@ -110,6 +108,7 @@ impl Tool for KaishAst { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_clear.rs b/crates/kaish-kernel/src/tools/builtin/kaish_clear.rs index dd1ab7aa..cbb3dc20 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_clear.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_clear.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, Scope}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// kaish-clear: reset session state (variables, cwd). pub struct KaishClear; @@ -39,9 +39,7 @@ impl Tool for KaishClear { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-clear: {e}")), @@ -77,6 +75,7 @@ impl Tool for KaishClear { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_last.rs b/crates/kaish-kernel/src/tools/builtin/kaish_last.rs index d1a6efeb..c1aa00bb 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_last.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_last.rs @@ -16,7 +16,7 @@ use clap::{CommandFactory, Parser}; use crate::dispatch::PipelinePosition; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use kaish_types::value_to_json; pub struct KaishLast; @@ -57,9 +57,7 @@ impl Tool for KaishLast { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-last: {e}")), @@ -109,6 +107,7 @@ impl Tool for KaishLast { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::interpreter::ExecResult; use crate::vfs::{MemoryFs, VfsRouter}; diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_status.rs b/crates/kaish-kernel/src/tools/builtin/kaish_status.rs index 1a525d8c..bbdc9bb6 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_status.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_status.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// kaish-status: display kernel name, variable count, and job count. pub struct KaishStatus; @@ -39,9 +39,7 @@ impl Tool for KaishStatus { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-status: {e}")), @@ -76,6 +74,7 @@ impl Tool for KaishStatus { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_trash.rs b/crates/kaish-kernel/src/tools/builtin/kaish_trash.rs index 94666cd8..8506788a 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_trash.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_trash.rs @@ -9,7 +9,7 @@ use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; use crate::trash::TrashBackend; use crate::operation::KernelOperation; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// KaishTrash tool: manage the system trash. pub struct KaishTrash; @@ -59,9 +59,7 @@ impl Tool for KaishTrash { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-trash: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_version.rs b/crates/kaish-kernel/src/tools/builtin/kaish_version.rs index c5f1d88c..5dc1249b 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_version.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_version.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// kaish-version: prints the kaish version string. pub struct KaishVersion; @@ -39,9 +39,7 @@ impl Tool for KaishVersion { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-version: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/kaish_vfs.rs b/crates/kaish-kernel/src/tools/builtin/kaish_vfs.rs index 09e83c3d..cd45fd30 100644 --- a/crates/kaish-kernel/src/tools/builtin/kaish_vfs.rs +++ b/crates/kaish-kernel/src/tools/builtin/kaish_vfs.rs @@ -16,7 +16,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// kaish-vfs tool: inspect and manage the overlay VFS transaction. pub struct KaishVfs; @@ -61,9 +61,7 @@ impl Tool for KaishVfs { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-vfs: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/keys.rs b/crates/kaish-kernel/src/tools/builtin/keys.rs index 20484b01..01c88ace 100644 --- a/crates/kaish-kernel/src/tools/builtin/keys.rs +++ b/crates/kaish-kernel/src/tools/builtin/keys.rs @@ -31,7 +31,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// keys tool: the keys of a collection (record keys / list indices). pub struct Keys; @@ -97,9 +97,7 @@ impl Tool for Keys { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("keys: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/kill.rs b/crates/kaish-kernel/src/tools/builtin/kill.rs index e7ddf90d..d6b48969 100644 --- a/crates/kaish-kernel/src/tools/builtin/kill.rs +++ b/crates/kaish-kernel/src/tools/builtin/kill.rs @@ -45,7 +45,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; use crate::scheduler::{JobId, JobManager}; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// Kill tool: send signals to processes or jobs. pub struct Kill; @@ -126,9 +126,7 @@ impl Tool for Kill { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, diff --git a/crates/kaish-kernel/src/tools/builtin/ln.rs b/crates/kaish-kernel/src/tools/builtin/ln.rs index 49bc9b6e..0cb7a506 100644 --- a/crates/kaish-kernel/src/tools/builtin/ln.rs +++ b/crates/kaish-kernel/src/tools/builtin/ln.rs @@ -9,7 +9,7 @@ use std::path::Path; use crate::interpreter::ExecResult; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Ln tool: create symbolic links. pub struct Ln; @@ -52,9 +52,7 @@ impl Tool for Ln { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("ln: {e}")), @@ -107,6 +105,7 @@ impl Tool for Ln { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/ls.rs b/crates/kaish-kernel/src/tools/builtin/ls.rs index b38417d3..8a3781c3 100644 --- a/crates/kaish-kernel/src/tools/builtin/ls.rs +++ b/crates/kaish-kernel/src/tools/builtin/ls.rs @@ -8,7 +8,7 @@ use std::path::Path; use crate::ast::Value; use crate::glob::contains_glob; use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::vfs::DirEntry; /// Ls tool: list directory contents. @@ -82,9 +82,7 @@ impl Tool for Ls { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Tests poke args.named.insert("long", Value::Bool(true)); to_argv would // emit `--long=true` which clap rejects for bool fields. Promote bool // named entries to flag form before clap parsing. diff --git a/crates/kaish-kernel/src/tools/builtin/mkdir.rs b/crates/kaish-kernel/src/tools/builtin/mkdir.rs index f5862075..2e54b389 100644 --- a/crates/kaish-kernel/src/tools/builtin/mkdir.rs +++ b/crates/kaish-kernel/src/tools/builtin/mkdir.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use std::path::Path; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Mkdir tool: create directories. pub struct Mkdir; @@ -45,9 +45,7 @@ impl Tool for Mkdir { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("mkdir: {e}")), @@ -87,6 +85,7 @@ impl Tool for Mkdir { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/mktemp.rs b/crates/kaish-kernel/src/tools/builtin/mktemp.rs index 14f77033..92d17e15 100644 --- a/crates/kaish-kernel/src/tools/builtin/mktemp.rs +++ b/crates/kaish-kernel/src/tools/builtin/mktemp.rs @@ -20,7 +20,7 @@ use std::path::Path; use crate::backend::WriteMode; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Mktemp tool: creates temporary files or directories with unique names. pub struct Mktemp; @@ -78,9 +78,7 @@ impl Tool for Mktemp { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("mktemp: {e}")), @@ -194,6 +192,7 @@ fn random_suffix(len: usize) -> Result { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/mv.rs b/crates/kaish-kernel/src/tools/builtin/mv.rs index 23ef14d3..6a649dc5 100644 --- a/crates/kaish-kernel/src/tools/builtin/mv.rs +++ b/crates/kaish-kernel/src/tools/builtin/mv.rs @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf}; use crate::backend::{BackendError, KernelBackend, WriteMode}; use crate::interpreter::ExecResult; use crate::operation::KernelOperation; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Mv tool: move/rename files and directories. pub struct Mv; @@ -48,9 +48,7 @@ impl Tool for Mv { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("mv: {e}")), @@ -246,6 +244,7 @@ fn move_dir_recursive<'a>( #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/output_limit.rs b/crates/kaish-kernel/src/tools/builtin/output_limit.rs index 8ce8abbc..503cfdb0 100644 --- a/crates/kaish-kernel/src/tools/builtin/output_limit.rs +++ b/crates/kaish-kernel/src/tools/builtin/output_limit.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; use crate::output_limit::{parse_size, OutputLimitConfig}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Output limit tool: inspect and modify output size limit configuration. pub struct KaishOutputLimit; @@ -44,9 +44,7 @@ impl Tool for KaishOutputLimit { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-output-limit: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/patch.rs b/crates/kaish-kernel/src/tools/builtin/patch.rs index 91343541..f7f4745e 100644 --- a/crates/kaish-kernel/src/tools/builtin/patch.rs +++ b/crates/kaish-kernel/src/tools/builtin/patch.rs @@ -19,7 +19,7 @@ use crate::backend::PatchOp; use crate::interpreter::{ExecResult, OutputData}; use crate::operation::KernelOperation; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Patch tool: applies unified diffs to files. pub struct Patch; @@ -76,9 +76,7 @@ impl Tool for Patch { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Tests poke args.flags.insert("dry-run") and args.named.insert("p", Int(1)). // `-R` flag and `--dry-run` flag work directly. The `p=1` form lands as // a single-char named entry which to_argv renders as `-p=1`; clap's @@ -677,6 +675,7 @@ fn apply_hunks( #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/plan.rs b/crates/kaish-kernel/src/tools/builtin/plan.rs index baacfdd1..80e75d9f 100644 --- a/crates/kaish-kernel/src/tools/builtin/plan.rs +++ b/crates/kaish-kernel/src/tools/builtin/plan.rs @@ -12,8 +12,8 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::ExecResult; -use crate::tools::{ - schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema, +use crate::tools::{exec_context, + schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema, }; /// plan: the statement projection an embedder judges a command by. @@ -53,9 +53,7 @@ impl Tool for PlanTool { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("plan: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/printf.rs b/crates/kaish-kernel/src/tools/builtin/printf.rs index a4238e64..0e4489f0 100644 --- a/crates/kaish-kernel/src/tools/builtin/printf.rs +++ b/crates/kaish-kernel/src/tools/builtin/printf.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use super::format_string::{self, FormatArg}; /// Printf tool: formatted output. @@ -262,9 +262,7 @@ impl Tool for Printf { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("printf: {e}")), @@ -329,6 +327,7 @@ impl FormatArg for &Value { #[allow(clippy::approx_constant)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/push.rs b/crates/kaish-kernel/src/tools/builtin/push.rs index 8bd34b01..90abe8a6 100644 --- a/crates/kaish-kernel/src/tools/builtin/push.rs +++ b/crates/kaish-kernel/src/tools/builtin/push.rs @@ -38,7 +38,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; use crate::validator::ValidationIssue; /// push tool: append value(s) to a list variable, in place. @@ -89,9 +89,7 @@ impl Tool for Push { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("push: {e}")), @@ -153,6 +151,7 @@ impl Tool for Push { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/pwd.rs b/crates/kaish-kernel/src/tools/builtin/pwd.rs index bf1d8568..0d5a1886 100644 --- a/crates/kaish-kernel/src/tools/builtin/pwd.rs +++ b/crates/kaish-kernel/src/tools/builtin/pwd.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Pwd tool: print current working directory. pub struct Pwd; @@ -39,9 +39,7 @@ impl Tool for Pwd { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("pwd: {e}")), @@ -61,6 +59,7 @@ impl Tool for Pwd { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::path::PathBuf; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/random.rs b/crates/kaish-kernel/src/tools/builtin/random.rs index bf67b377..d3d14550 100644 --- a/crates/kaish-kernel/src/tools/builtin/random.rs +++ b/crates/kaish-kernel/src/tools/builtin/random.rs @@ -10,7 +10,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{value_to_string, ExecResult}; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// Random tool: print one random integer, uniformly, from a range. pub struct Random; @@ -59,9 +59,7 @@ impl Tool for Random { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // No positional args; curate the error before clap ever sees one. if let Some(v) = args.positional.first() { @@ -157,6 +155,7 @@ fn map_draw_to_range(draw: u64, min: i64, max: i64) -> Option { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/read.rs b/crates/kaish-kernel/src/tools/builtin/read.rs index 7ffa4319..8ec110fd 100644 --- a/crates/kaish-kernel/src/tools/builtin/read.rs +++ b/crates/kaish-kernel/src/tools/builtin/read.rs @@ -13,7 +13,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::ValidationIssue; /// Read tool: reads a line from stdin into variable(s). @@ -72,9 +72,7 @@ impl Tool for Read { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("read: {e}")), @@ -209,6 +207,7 @@ fn process_escapes(s: &str) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/readlink.rs b/crates/kaish-kernel/src/tools/builtin/readlink.rs index dcf813ec..7b9505df 100644 --- a/crates/kaish-kernel/src/tools/builtin/readlink.rs +++ b/crates/kaish-kernel/src/tools/builtin/readlink.rs @@ -8,7 +8,7 @@ use clap::{CommandFactory, Parser}; use std::path::Path; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Readlink tool: read symlink target or canonicalize a path. pub struct Readlink; @@ -47,9 +47,7 @@ impl Tool for Readlink { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("readlink: {e}")), @@ -146,6 +144,7 @@ impl Tool for Readlink { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/realpath.rs b/crates/kaish-kernel/src/tools/builtin/realpath.rs index ee03dad7..2eba2423 100644 --- a/crates/kaish-kernel/src/tools/builtin/realpath.rs +++ b/crates/kaish-kernel/src/tools/builtin/realpath.rs @@ -7,7 +7,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Realpath tool: resolve path to absolute, canonical form. pub struct Realpath; @@ -42,9 +42,7 @@ impl Tool for Realpath { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("realpath: {e}")), @@ -101,6 +99,7 @@ impl Tool for Realpath { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs b/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs index 1c0f860c..f02dc001 100644 --- a/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs +++ b/crates/kaish-kernel/src/tools/builtin/regex_dialect.rs @@ -77,11 +77,177 @@ pub(crate) fn bre_metas_to_ere(pattern: &str) -> String { out } + +/// The opener a pattern never closed, if there is exactly one to name. +/// +/// Scans outside-in, honoring backslash escapes and the rule that `]` is a +/// literal when it opens a class body (`[]a]`). Returns the innermost opener +/// still waiting at end of input. +fn unbalanced_opener(pattern: &str) -> Option<(usize, char)> { + let mut chars = pattern.char_indices().peekable(); + let mut open: Vec<(usize, char)> = Vec::new(); + let mut class_body_start: Option = None; + let mut class_open_index = 0usize; + + while let Some((index, c)) = chars.next() { + // A backslash escapes whatever follows, in or out of a class. + if c == '\\' { + chars.next(); + continue; + } + match class_body_start { + // Inside `[...]`: only `]` closes, and not in first position, + // where it is a literal. + Some(start) => { + if c == ']' && index != start { + class_body_start = None; + } + } + None => match c { + // `[^...]` keeps the caret out of the first-position rule. + '[' => { + let body = match chars.peek() { + Some(&(next_index, '^')) => next_index + 1, + Some(&(next_index, _)) => next_index, + None => index + 1, + }; + class_body_start = Some(body); + class_open_index = index; + } + '(' | '{' => open.push((index, c)), + ')' if open.last().map(|&(_, o)| o) == Some('(') => { + open.pop(); + } + '}' if open.last().map(|&(_, o)| o) == Some('{') => { + open.pop(); + } + _ => {} + }, + } + } + + if class_body_start.is_some() { + return Some((class_open_index, '[')); + } + open.pop() +} + +/// Name the spelling that fixes a pattern the regex engine refused. +/// +/// The engine reports what is wrong ("unclosed character class"); an agent +/// needs to know what to write instead. An unbalanced opener is nearly always +/// a literal the author did not escape, so name the escape for the character +/// left open — minding the dialect: `\(` and `\{` are BRE operators +/// [`bre_metas_to_ere`] rewrites, so only a bracket class spells those +/// literally. `[` is not a BRE meta, so `\[` is its literal form. +/// +/// Returns `None` when no single opener explains the failure, leaving the +/// engine's own message to stand alone. +pub(crate) fn regex_fix_hint(pattern: &str) -> Option<&'static str> { + let (index, opener) = unbalanced_opener(pattern)?; + let (spelling, hint) = match opener { + '[' => (r"\[", r"write `\[` to match a literal `[`"), + '(' => ("[(]", "write `[(]` to match a literal `(`"), + '{' => ("[{]", "write `[{]` to match a literal `{`"), + _ => return None, + }; + + // An unbalanced opener explains the failure only when escaping it is the + // whole fix. `[)` opens a class AND leaves a group unopened, and naming + // `\[` there would send the reader back with a pattern that still does not + // compile. Apply the spelling at the site the scan found and keep the hint + // only if the result compiles. + let mut fixed = String::with_capacity(pattern.len() + spelling.len()); + fixed.push_str(&pattern[..index]); + fixed.push_str(spelling); + fixed.push_str(&pattern[index + opener.len_utf8()..]); + regex::Regex::new(&bre_metas_to_ere(&fixed)).ok().map(|_| hint) +} + + #[cfg(test)] mod tests { use super::*; use rstest::rstest; + #[rstest] + // `[` is not a BRE meta, so a backslash is its literal form. + #[case("[cast:", Some(r"write `\[` to match a literal `[`"))] + #[case("[^abc", Some(r"write `\[` to match a literal `[`"))] + // A `]` in first position is a literal, so the class is still open. + #[case("[]", Some(r"write `\[` to match a literal `[`"))] + // `(` and `{` ARE BRE metas: `\(` is rewritten to a group, so only a + // bracket class spells the literal. + #[case("(unclosed", Some("write `[(]` to match a literal `(`"))] + #[case("a{2", Some("write `[{]` to match a literal `{`"))] + // Balanced patterns have nothing to name. + #[case("[cast:]", None)] + #[case("[]]", None)] + #[case("(a|b)", None)] + #[case("a{2,5}", None)] + // An escaped opener is a literal and never opens anything. + #[case(r"\[cast:", None)] + // Brackets inside a class are literal, not nested openers. + #[case("[([{]", None)] + fn fix_hint_names_the_dialect_correct_escape( + #[case] pattern: &str, + #[case] expected: Option<&str>, + ) { + assert_eq!(regex_fix_hint(pattern), expected); + } + + #[rstest] + // A second unescaped meta after the opener: escaping `[` leaves `)` + // unopened, so there is no single spelling to name. + #[case("[)")] + #[case("[a(b")] + // The scan reports the class and never reaches the unclosed group. + #[case("(a[")] + fn a_pattern_with_two_faults_gets_no_hint(#[case] pattern: &str) { + assert!( + regex::Regex::new(&bre_metas_to_ere(pattern)).is_err(), + "fixture must actually be a broken pattern", + ); + assert_eq!( + regex_fix_hint(pattern), + None, + "naming one fix for a two-fault pattern sends the reader back with \ + a pattern that still does not compile", + ); + } + + #[rstest] + #[case("[cast:")] + #[case("[^abc")] + #[case("[]")] + #[case("(unclosed")] + #[case("a{2")] + #[case("x[0-9]+(")] + #[case("日本[")] + fn every_hint_it_gives_actually_compiles(#[case] pattern: &str) { + // The hint is applied at the site the scan found, which is what + // `regex_fix_hint` itself does — a test that searched for the first + // occurrence of the character could pass while the real fix landed + // somewhere else. + let hint = regex_fix_hint(pattern).expect("an open pattern gets a hint"); + let spelling = hint + .split('`') + .nth(1) + .expect("the hint quotes the spelling it recommends"); + let (index, opener) = unbalanced_opener(pattern).expect("an opener"); + let fixed = format!( + "{}{}{}", + &pattern[..index], + spelling, + &pattern[index + opener.len_utf8()..], + ); + let rewritten = bre_metas_to_ere(&fixed); + assert!( + regex::Regex::new(&rewritten).is_ok(), + "hint {hint:?} produced {rewritten:?}, which still does not compile", + ); + } + #[rstest] // Alternation — the issue's headline case. #[case(r"foo\|bar", "foo|bar")] @@ -109,4 +275,4 @@ mod tests { fn rewrites_gnu_bre_metas(#[case] input: &str, #[case] expected: &str) { assert_eq!(bre_metas_to_ere(input), expected); } -} +} \ No newline at end of file diff --git a/crates/kaish-kernel/src/tools/builtin/rm.rs b/crates/kaish-kernel/src/tools/builtin/rm.rs index c6bf7915..57bfee28 100644 --- a/crates/kaish-kernel/src/tools/builtin/rm.rs +++ b/crates/kaish-kernel/src/tools/builtin/rm.rs @@ -11,7 +11,7 @@ use std::path::{Path, PathBuf}; use crate::backend::BackendError; use crate::interpreter::ExecResult; use crate::operation::KernelOperation; -use crate::tools::{is_trash_excluded, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, is_trash_excluded, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// clap-derived argv layer for rm. #[derive(Parser, Debug)] @@ -112,9 +112,7 @@ impl Tool for Rm { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); args.flagify_bool_named(&self.schema()); let argv = match args.to_argv() { @@ -233,6 +231,7 @@ impl Tool for Rm { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/scatter.rs b/crates/kaish-kernel/src/tools/builtin/scatter.rs index 4403cfc7..38d86ab1 100644 --- a/crates/kaish-kernel/src/tools/builtin/scatter.rs +++ b/crates/kaish-kernel/src/tools/builtin/scatter.rs @@ -15,7 +15,7 @@ use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; use crate::scheduler::{extract_items, parse_scatter_options}; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::ValidationIssue; /// Scatter tool: fan out items for parallel processing. @@ -85,9 +85,7 @@ impl Tool for Scatter { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("scatter: {e}")), @@ -139,6 +137,7 @@ impl Tool for Scatter { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; #[tokio::test] diff --git a/crates/kaish-kernel/src/tools/builtin/sed.rs b/crates/kaish-kernel/src/tools/builtin/sed.rs index 8f617615..b576c415 100644 --- a/crates/kaish-kernel/src/tools/builtin/sed.rs +++ b/crates/kaish-kernel/src/tools/builtin/sed.rs @@ -17,7 +17,7 @@ use crate::operation::KernelOperation; use crate::tools::builtin::get_path_string; use crate::tools::builtin::regex_dialect::{append_dialect_hint, bre_metas_to_ere}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::{IssueCode, ValidationIssue}; /// Sed tool: stream editor for text transformations. @@ -140,9 +140,7 @@ impl Tool for Sed { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // A structured call (`{"in-place": true}`) binds a bool into args.named, // which to_argv renders as `--in-place=true` — and clap's SetTrue rejects // a value. Move bool-schema named entries into flags so they render bare. @@ -1058,6 +1056,7 @@ fn expand_replacement(replacement: &str, captures: ®ex::Captures) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/seq.rs b/crates/kaish-kernel/src/tools/builtin/seq.rs index 518c567a..f98c0924 100644 --- a/crates/kaish-kernel/src/tools/builtin/seq.rs +++ b/crates/kaish-kernel/src/tools/builtin/seq.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::{IssueCode, ValidationIssue}; /// Seq tool: print a sequence of numbers. @@ -81,9 +81,7 @@ impl Tool for Seq { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("seq: {e}")), @@ -245,6 +243,7 @@ fn value_to_f64(v: &Value) -> f64 { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/set.rs b/crates/kaish-kernel/src/tools/builtin/set.rs index 22fa227f..2eafb8c1 100644 --- a/crates/kaish-kernel/src/tools/builtin/set.rs +++ b/crates/kaish-kernel/src/tools/builtin/set.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Set tool: configure shell options. /// @@ -104,9 +104,7 @@ impl Tool for Set { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // set has bespoke argv handling — strip the user-provided -e / -o etc. // tokens from the argv before handing to clap, otherwise clap would // reject unknown flags. Only `--json` (global) needs to clap-parse. diff --git a/crates/kaish-kernel/src/tools/builtin/sleep.rs b/crates/kaish-kernel/src/tools/builtin/sleep.rs index a330fc48..fe84281c 100644 --- a/crates/kaish-kernel/src/tools/builtin/sleep.rs +++ b/crates/kaish-kernel/src/tools/builtin/sleep.rs @@ -6,7 +6,7 @@ use std::time::Duration; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Sleep tool: pause execution for a specified duration. pub struct Sleep; @@ -41,9 +41,7 @@ impl Tool for Sleep { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("sleep: {e}")), @@ -110,6 +108,7 @@ fn parse_duration(s: &str) -> Result { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; use kaish_types::clock::Instant; diff --git a/crates/kaish-kernel/src/tools/builtin/sort.rs b/crates/kaish-kernel/src/tools/builtin/sort.rs index f6c95f5a..6fdb86ec 100644 --- a/crates/kaish-kernel/src/tools/builtin/sort.rs +++ b/crates/kaish-kernel/src/tools/builtin/sort.rs @@ -6,7 +6,7 @@ use std::cmp::Ordering; use std::path::Path; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; // `Value` is used only in the test module (positional fixtures); import it // there to avoid a dead-code warning in non-test builds. @@ -77,9 +77,7 @@ impl Tool for Sort { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("sort: {e}")), @@ -449,6 +447,7 @@ fn extract_leading_number(s: &str) -> f64 { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/spawn.rs b/crates/kaish-kernel/src/tools/builtin/spawn.rs index e703808e..6282a940 100644 --- a/crates/kaish-kernel/src/tools/builtin/spawn.rs +++ b/crates/kaish-kernel/src/tools/builtin/spawn.rs @@ -22,8 +22,8 @@ use tokio::process::Command; use crate::ast::Value; use crate::interpreter::ExecResult; use crate::tools::builtin::get_path_string; -use crate::tools::{ - schema_from_clap, ExecContext, ExternalCommandsUnavailable, GlobalFlags, Tool, ToolArgs, +use crate::tools::{exec_context, + schema_from_clap, ExternalCommandsUnavailable, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema, }; @@ -84,9 +84,7 @@ impl Tool for Spawn { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); args.flagify_bool_named(&self.schema()); let argv = match args.to_argv() { @@ -431,6 +429,7 @@ fn extract_string_object(value: &Value) -> Vec<(String, String)> { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/split.rs b/crates/kaish-kernel/src/tools/builtin/split.rs index 3c9f92c5..ec1781f7 100644 --- a/crates/kaish-kernel/src/tools/builtin/split.rs +++ b/crates/kaish-kernel/src/tools/builtin/split.rs @@ -34,7 +34,7 @@ use regex::Regex; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Split tool: split a string into an array. pub struct Split; @@ -82,9 +82,7 @@ impl Tool for Split { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("split: {e}")), @@ -209,6 +207,7 @@ impl Tool for Split { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/stat.rs b/crates/kaish-kernel/src/tools/builtin/stat.rs index 0aa23e3c..92d86326 100644 --- a/crates/kaish-kernel/src/tools/builtin/stat.rs +++ b/crates/kaish-kernel/src/tools/builtin/stat.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use std::path::Path; use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Stat tool: display file or filesystem status. pub struct Stat; @@ -49,9 +49,7 @@ impl Tool for Stat { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("stat: {e}")), @@ -190,6 +188,7 @@ fn format_stat(fmt: &str, name: &str, info: &crate::vfs::DirEntry) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/tac.rs b/crates/kaish-kernel/src/tools/builtin/tac.rs index 2c8d68ae..20f9e99e 100644 --- a/crates/kaish-kernel/src/tools/builtin/tac.rs +++ b/crates/kaish-kernel/src/tools/builtin/tac.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use std::path::Path; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tac tool: output lines in reverse order. pub struct Tac; @@ -40,9 +40,7 @@ impl Tool for Tac { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tac: {e}")), @@ -132,6 +130,7 @@ impl Tool for Tac { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/tail.rs b/crates/kaish-kernel/src/tools/builtin/tail.rs index cb420fe5..62065658 100644 --- a/crates/kaish-kernel/src/tools/builtin/tail.rs +++ b/crates/kaish-kernel/src/tools/builtin/tail.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tail tool: output the last part of files or stdin. pub struct Tail; @@ -51,9 +51,7 @@ impl Tool for Tail { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Handle POSIX shorthand: tail -3 file → tail -n 3 file // Lexer tokenizes "-3" as Int(-3), which lands in positional[0]. if let Some(Value::Int(n)) = args.positional.first() { diff --git a/crates/kaish-kernel/src/tools/builtin/tee.rs b/crates/kaish-kernel/src/tools/builtin/tee.rs index effe4440..cb4799bd 100644 --- a/crates/kaish-kernel/src/tools/builtin/tee.rs +++ b/crates/kaish-kernel/src/tools/builtin/tee.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::interpreter::ExecResult; use crate::operation::KernelOperation; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tee tool: duplicate stdin to stdout and files. pub struct Tee; @@ -47,9 +47,7 @@ impl Tool for Tee { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tee: {e}")), @@ -152,6 +150,7 @@ impl Tool for Tee { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::backend::WriteMode; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; diff --git a/crates/kaish-kernel/src/tools/builtin/test.rs b/crates/kaish-kernel/src/tools/builtin/test.rs index 7f37a2af..f33873a2 100644 --- a/crates/kaish-kernel/src/tools/builtin/test.rs +++ b/crates/kaish-kernel/src/tools/builtin/test.rs @@ -30,7 +30,7 @@ use crate::interpreter::{ value_to_text_sink_named, values_equal, ExecResult, }; use kaish_tool_api::{IssueCode, ValidationIssue}; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; pub struct Test; @@ -141,9 +141,7 @@ impl Tool for Test { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("test: {e}")).into_fault(), diff --git a/crates/kaish-kernel/src/tools/builtin/timeout.rs b/crates/kaish-kernel/src/tools/builtin/timeout.rs index a139353a..e1568996 100644 --- a/crates/kaish-kernel/src/tools/builtin/timeout.rs +++ b/crates/kaish-kernel/src/tools/builtin/timeout.rs @@ -20,7 +20,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use crate::ast::{Arg, Command, Expr, Value}; use crate::duration::parse_duration; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Timeout tool: run a command with a deadline. pub struct Timeout; @@ -60,9 +60,7 @@ impl Tool for Timeout { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("timeout: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/tojson.rs b/crates/kaish-kernel/src/tools/builtin/tojson.rs index 2ed281aa..2657290c 100644 --- a/crates/kaish-kernel/src/tools/builtin/tojson.rs +++ b/crates/kaish-kernel/src/tools/builtin/tojson.rs @@ -26,7 +26,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// tojson tool: serialize a value to a JSON document. pub struct ToJson; @@ -69,9 +69,7 @@ impl Tool for ToJson { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tojson: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/tojsonl.rs b/crates/kaish-kernel/src/tools/builtin/tojsonl.rs index 06c03995..174a3eac 100644 --- a/crates/kaish-kernel/src/tools/builtin/tojsonl.rs +++ b/crates/kaish-kernel/src/tools/builtin/tojsonl.rs @@ -34,7 +34,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; use super::keys::describe_kind; @@ -74,9 +74,7 @@ impl Tool for ToJsonl { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tojsonl: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/tokens.rs b/crates/kaish-kernel/src/tools/builtin/tokens.rs index 49ed388c..f7ea6348 100644 --- a/crates/kaish-kernel/src/tools/builtin/tokens.rs +++ b/crates/kaish-kernel/src/tools/builtin/tokens.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use tiktoken_rs::{cl100k_base, o200k_base, p50k_base}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tokens tool: count BPE tokens in text. pub struct Tokens; @@ -51,9 +51,7 @@ impl Tool for Tokens { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tokens: {e}")), @@ -140,6 +138,7 @@ impl Tool for Tokens { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/touch.rs b/crates/kaish-kernel/src/tools/builtin/touch.rs index 85ede595..e81fff5c 100644 --- a/crates/kaish-kernel/src/tools/builtin/touch.rs +++ b/crates/kaish-kernel/src/tools/builtin/touch.rs @@ -7,7 +7,7 @@ use kaish_types::clock::system_now; use crate::backend::WriteMode; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Touch tool: change file timestamps or create files. pub struct Touch; @@ -42,9 +42,7 @@ impl Tool for Touch { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("touch: {e}")), @@ -95,6 +93,7 @@ impl Tool for Touch { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/tr.rs b/crates/kaish-kernel/src/tools/builtin/tr.rs index 596c5e2d..bbeb7702 100644 --- a/crates/kaish-kernel/src/tools/builtin/tr.rs +++ b/crates/kaish-kernel/src/tools/builtin/tr.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tr tool: translate, squeeze, or delete characters. pub struct Tr; @@ -52,9 +52,7 @@ impl Tool for Tr { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("tr: {e}")), @@ -331,6 +329,7 @@ fn squeeze_set_pred(input: &str, in_set: impl Fn(&char) -> bool) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/tree.rs b/crates/kaish-kernel/src/tools/builtin/tree.rs index e78e1378..5a9b0bc5 100644 --- a/crates/kaish-kernel/src/tools/builtin/tree.rs +++ b/crates/kaish-kernel/src/tools/builtin/tree.rs @@ -7,7 +7,7 @@ use std::path::Path; use crate::interpreter::{EntryType, ExecResult, OutputData, OutputNode}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Tree tool: display directory structure. pub struct Tree; @@ -204,9 +204,7 @@ impl Tool for Tree { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); args.flagify_bool_named(&self.schema()); let argv = match args.to_argv() { @@ -407,6 +405,7 @@ fn apply_walk_errors(mut result: ExecResult, errors: &[String]) -> ExecResult { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/true_false.rs b/crates/kaish-kernel/src/tools/builtin/true_false.rs index 7fd2f516..61c98602 100644 --- a/crates/kaish-kernel/src/tools/builtin/true_false.rs +++ b/crates/kaish-kernel/src/tools/builtin/true_false.rs @@ -25,7 +25,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// True builtin: always succeeds (exit code 0). pub struct True; @@ -60,9 +60,7 @@ impl Tool for True { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("true: {e}")), @@ -118,9 +116,7 @@ impl Tool for Colon { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!(": {e}")), @@ -168,9 +164,7 @@ impl Tool for False { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("false: {e}")), @@ -190,6 +184,7 @@ impl Tool for False { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/type_of.rs b/crates/kaish-kernel/src/tools/builtin/type_of.rs index aaba1544..4b00b6ba 100644 --- a/crates/kaish-kernel/src/tools/builtin/type_of.rs +++ b/crates/kaish-kernel/src/tools/builtin/type_of.rs @@ -32,7 +32,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; /// typeof tool: a value's type as a plain type name — the shape guard. pub struct TypeOf; @@ -99,9 +99,7 @@ impl Tool for TypeOf { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("typeof: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/uname.rs b/crates/kaish-kernel/src/tools/builtin/uname.rs index 751f6be4..2e99233b 100644 --- a/crates/kaish-kernel/src/tools/builtin/uname.rs +++ b/crates/kaish-kernel/src/tools/builtin/uname.rs @@ -29,7 +29,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Uname tool: print system identification. pub struct Uname; @@ -180,9 +180,7 @@ impl Tool for Uname { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("uname: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/uniq.rs b/crates/kaish-kernel/src/tools/builtin/uniq.rs index 89d518ad..9a5447f1 100644 --- a/crates/kaish-kernel/src/tools/builtin/uniq.rs +++ b/crates/kaish-kernel/src/tools/builtin/uniq.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::interpreter::{ExecResult, OutputData}; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Uniq tool: report or filter out repeated adjacent lines. pub struct Uniq; @@ -57,9 +57,7 @@ impl Tool for Uniq { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("uniq: {e}")), @@ -164,6 +162,7 @@ impl Tool for Uniq { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::vfs::{Filesystem, MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/unset.rs b/crates/kaish-kernel/src/tools/builtin/unset.rs index fc5af8ac..551ee027 100644 --- a/crates/kaish-kernel/src/tools/builtin/unset.rs +++ b/crates/kaish-kernel/src/tools/builtin/unset.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, validate_against_schema, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, validate_against_schema, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::ValidationIssue; /// Unset tool: removes variables from the current scope. @@ -57,9 +57,7 @@ impl Tool for Unset { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("unset: {e}")), @@ -108,6 +106,7 @@ impl Tool for Unset { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/validate.rs b/crates/kaish-kernel/src/tools/builtin/validate.rs index b8e5478d..0998d91e 100644 --- a/crates/kaish-kernel/src/tools/builtin/validate.rs +++ b/crates/kaish-kernel/src/tools/builtin/validate.rs @@ -16,7 +16,7 @@ use crate::ast::ToolDef; use crate::interpreter::{ExecResult, OutputData}; use crate::parser::parse; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; use crate::validator::{Severity, Validator}; /// Validate tool: check kaish scripts for errors before execution. @@ -65,9 +65,7 @@ impl Tool for Validate { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-validate: {e}")), @@ -184,6 +182,7 @@ impl Tool for Validate { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::ast::Value; use crate::tools::{register_builtins, ToolRegistry}; use crate::vfs::{MemoryFs, VfsRouter}; diff --git a/crates/kaish-kernel/src/tools/builtin/values.rs b/crates/kaish-kernel/src/tools/builtin/values.rs index e4f1ea65..c4ad1480 100644 --- a/crates/kaish-kernel/src/tools/builtin/values.rs +++ b/crates/kaish-kernel/src/tools/builtin/values.rs @@ -28,7 +28,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::ExecResult; -use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema}; use super::keys::describe_kind; @@ -72,9 +72,7 @@ impl Tool for Values { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("values: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/vars.rs b/crates/kaish-kernel/src/tools/builtin/vars.rs index 8926d936..919cef28 100644 --- a/crates/kaish-kernel/src/tools/builtin/vars.rs +++ b/crates/kaish-kernel/src/tools/builtin/vars.rs @@ -5,7 +5,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Vars tool: lists all variables in the current scope. pub struct Vars; @@ -40,9 +40,7 @@ impl Tool for Vars { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("kaish-vars: {e}")), @@ -114,6 +112,7 @@ fn value_type_name(value: &Value) -> &'static str { #[allow(clippy::approx_constant)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::interpreter::apply_output_format; use crate::interpreter::OutputFormat; use crate::vfs::{MemoryFs, VfsRouter}; diff --git a/crates/kaish-kernel/src/tools/builtin/wait.rs b/crates/kaish-kernel/src/tools/builtin/wait.rs index 77b6d30d..dd5ce2b2 100644 --- a/crates/kaish-kernel/src/tools/builtin/wait.rs +++ b/crates/kaish-kernel/src/tools/builtin/wait.rs @@ -7,7 +7,7 @@ use clap::{CommandFactory, Parser}; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::scheduler::JobId; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Wait tool: wait for background jobs. pub struct Wait; @@ -45,9 +45,7 @@ impl Tool for Wait { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("wait: {e}")), @@ -169,6 +167,7 @@ fn finish(output: String, any_failed: bool) -> ExecResult { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::scheduler::JobManager; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/wc.rs b/crates/kaish-kernel/src/tools/builtin/wc.rs index 5f3350c8..0899fcdc 100644 --- a/crates/kaish-kernel/src/tools/builtin/wc.rs +++ b/crates/kaish-kernel/src/tools/builtin/wc.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use clap::{CommandFactory, Parser}; use crate::interpreter::{ExecResult, OutputData, OutputNode}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Wc tool: count lines, words, characters, and bytes. pub struct Wc; @@ -56,9 +56,7 @@ impl Tool for Wc { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("wc: {e}")), diff --git a/crates/kaish-kernel/src/tools/builtin/which.rs b/crates/kaish-kernel/src/tools/builtin/which.rs index ce12ac44..5978bcf6 100644 --- a/crates/kaish-kernel/src/tools/builtin/which.rs +++ b/crates/kaish-kernel/src/tools/builtin/which.rs @@ -14,7 +14,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Which tool: locates commands in PATH. pub struct Which; @@ -53,9 +53,7 @@ impl Tool for Which { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); let argv = match args.to_argv() { Ok(v) => v, Err(e) => return ExecResult::failure(2, format!("which: {e}")), @@ -196,6 +194,7 @@ fn value_to_string(value: &Value) -> String { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/write.rs b/crates/kaish-kernel/src/tools/builtin/write.rs index 316ca7a0..c88944a0 100644 --- a/crates/kaish-kernel/src/tools/builtin/write.rs +++ b/crates/kaish-kernel/src/tools/builtin/write.rs @@ -8,7 +8,7 @@ use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; use crate::operation::KernelOperation; use crate::tools::builtin::get_path_string; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Write tool: write content to a file. pub struct Write; @@ -53,9 +53,7 @@ impl Tool for Write { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // `--content` is never read off `parsed.content` — see below, it's // always read as a raw typed `Value` off `args.named`/`args.positional` // specifically so a `Value::Bytes` payload survives untouched. That @@ -165,6 +163,7 @@ fn value_to_bytes(value: &Value) -> Vec { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/builtin/xxd.rs b/crates/kaish-kernel/src/tools/builtin/xxd.rs index d815dce8..a19bf5e8 100644 --- a/crates/kaish-kernel/src/tools/builtin/xxd.rs +++ b/crates/kaish-kernel/src/tools/builtin/xxd.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::ast::Value; use crate::interpreter::{ExecResult, OutputData}; -use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; +use crate::tools::{exec_context, schema_from_clap, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema}; /// Xxd tool: hex dump or reverse. pub struct Xxd; @@ -58,9 +58,7 @@ impl Tool for Xxd { } async fn execute(&self, mut args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); // Tests poke args.named.insert("plain", Value::Bool(true)); to_argv would // produce `--plain=true` which clap rejects for a bool field. Promote // bool-typed named entries into flag form. @@ -284,6 +282,7 @@ fn reverse_hex(input: &str, plain: bool) -> ExecResult { #[cfg(test)] mod tests { use super::*; + use crate::tools::ExecContext; use crate::vfs::{MemoryFs, VfsRouter}; use std::sync::Arc; diff --git a/crates/kaish-kernel/src/tools/context.rs b/crates/kaish-kernel/src/tools/context.rs index 03727021..c007d31d 100644 --- a/crates/kaish-kernel/src/tools/context.rs +++ b/crates/kaish-kernel/src/tools/context.rs @@ -1257,6 +1257,8 @@ impl ExecContext { /// pipes, dispatcher) through /// [`ToolCtx::as_any_mut`](kaish_tool_api::ToolCtx::as_any_mut). #[async_trait] +impl kaish_tool_api::sealed::Sealed for ExecContext {} + impl kaish_tool_api::ToolCtx for ExecContext { fn backend(&self) -> &Arc { &self.backend @@ -1336,11 +1338,99 @@ fn normalize_path(path: &std::path::Path) -> PathBuf { } } +/// Narrow a [`ToolCtx`](crate::tools::ToolCtx) to the kernel's own +/// [`ExecContext`]. +/// +/// [`ToolCtx`](crate::tools::ToolCtx) is sealed, so `ExecContext` is its only +/// implementor and this downcast cannot fail. Type privacy alone would not be +/// enough: `ToolRegistry::get` hands out an `Arc` and `Tool::execute` +/// is public, so without the seal an embedder could dispatch a builtin with a +/// context of its own and reach this branch. +/// +/// It is still checked, and a failure panics. Returning an exit code here +/// would hand a script a number it could only read as an ordinary command +/// failure, hiding a kernel that was built wrong behind a value that looks +/// like data. +pub(crate) fn exec_context(ctx: &mut dyn crate::tools::ToolCtx) -> &mut ExecContext { + match ctx.as_any_mut().downcast_mut::() { + Some(ctx) => ctx, + None => panic!( + "kernel builtin dispatched with a foreign ToolCtx; \ + builtins must be registered through register_builtins" + ), + } +} + #[cfg(test)] mod tests { use super::{decide_mutation_action, MutationAction}; use std::path::Path; + /// A `ToolCtx` that is not the kernel's `ExecContext`. + /// + /// Only `as_any_mut` is ever reached: `exec_context` downcasts and gives + /// up. The rest of the trait is here to satisfy the compiler, and calling + /// any of it in a test would be the test itself being wrong. + struct ForeignCtx; + + // `ToolCtx` is sealed, so this line is what a crate outside kaish cannot + // write — it is the seal, stated as code. The test opts in deliberately to + // reach a branch that is otherwise unreachable, and its existence here is + // the reason `exec_context` may assert instead of returning a code. + impl kaish_tool_api::sealed::Sealed for ForeignCtx {} + + impl kaish_tool_api::ToolCtx for ForeignCtx { + fn backend(&self) -> &std::sync::Arc { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn cwd(&self) -> &Path { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn resolve_path(&self, _path: &str) -> std::path::PathBuf { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn var(&self, _name: &str) -> Option { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn set_var(&mut self, _name: &str, _value: crate::ast::Value) { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn set_output_format(&mut self, _format: kaish_types::OutputFormat) { + unimplemented!("ForeignCtx exists only to fail the downcast") + } + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + } + + #[test] + #[should_panic(expected = "foreign ToolCtx")] + fn a_foreign_tool_ctx_panics_rather_than_returning_an_exit_code() { + // The downcast is unreachable through the kernel's registry, which is + // the only way a builtin is dispatched. If it ever does fail, the + // kernel was built wrong, and a script must not be able to read that + // as an ordinary non-zero exit. + let mut ctx = ForeignCtx; + let _ = super::exec_context(&mut ctx); + } + + #[test] + fn the_kernel_s_own_context_downcasts() { + // The control: without this, the panic test above would pass even if + // `exec_context` panicked unconditionally. + use crate::vfs::{MemoryFs, VfsRouter}; + + let mut vfs = VfsRouter::new(); + vfs.mount("/", MemoryFs::new()); + let mut ctx = super::ExecContext::new(std::sync::Arc::new(vfs)); + let expected = ctx.cwd.clone(); + let narrowed = super::exec_context(&mut ctx); + assert_eq!(narrowed.cwd, expected); + } + fn decide( trash: bool, real: Option<&str>, diff --git a/crates/kaish-kernel/src/tools/mod.rs b/crates/kaish-kernel/src/tools/mod.rs index 7b21f048..e8835a13 100644 --- a/crates/kaish-kernel/src/tools/mod.rs +++ b/crates/kaish-kernel/src/tools/mod.rs @@ -31,7 +31,7 @@ pub use context::{ external_commands_unavailable_error, ExecContext, ExternalCommandsUnavailable, GateExpectations, OutputContext, OverwriteExpectation, DEFAULT_KILL_GRACE, }; -pub(crate) use context::{cas_overwrite, is_trash_excluded, ExternalCommandOutcome}; +pub(crate) use context::{cas_overwrite, exec_context, is_trash_excluded, ExternalCommandOutcome}; pub use global_flags::GlobalFlags; pub use registry::ToolRegistry; pub use traits::{ArgBinding, global_flag_value_is_truthy, is_global_output_flag, validate_against_schema, Tool, ToolArgs, ToolCtx, ToolSchema, ParamSchema}; diff --git a/crates/kaish-kernel/src/tools/wrapped.rs b/crates/kaish-kernel/src/tools/wrapped.rs index e2f9392b..eb74ad03 100644 --- a/crates/kaish-kernel/src/tools/wrapped.rs +++ b/crates/kaish-kernel/src/tools/wrapped.rs @@ -52,7 +52,7 @@ use kaish_tool_api::{IssueCode, ValidationIssue}; use crate::spawn::{ hermetic_env, spawn_process, OutputPolicy, SpawnContext, SpawnRequest, StdinPolicy, }; -use crate::tools::{virtual_cwd_error, ExecContext, Tool, ToolCtx}; +use crate::tools::{exec_context, virtual_cwd_error, ExecContext, Tool, ToolCtx}; pub use declaration::{find_executable, Flag, Positional, Stdin, Style, Tail, Verb, WrappedCommand}; pub use error::WrappedError; @@ -340,9 +340,7 @@ impl Tool for WrappedTool { } async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult { - let Some(ctx) = ctx.as_any_mut().downcast_mut::() else { - return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext"); - }; + let ctx = exec_context(ctx); self.run(args, ctx).await } } diff --git a/crates/kaish-kernel/src/validator/mod.rs b/crates/kaish-kernel/src/validator/mod.rs index efeb86a9..e7c8f980 100644 --- a/crates/kaish-kernel/src/validator/mod.rs +++ b/crates/kaish-kernel/src/validator/mod.rs @@ -33,3 +33,26 @@ pub use walker::{build_tool_args_for_validation, Validator}; pub(crate) use walker::{ classify_command_name, is_runtime_special_form, is_static_command_name, SpecialForm, }; + +/// Validate `source` against the builtin catalog, without a kernel. +/// +/// This is the mechanism behind `kaish --plan`: a dry run should refuse the +/// same programs the kernel would, and it has no kernel to ask. The registry +/// is the compiled-in builtin set, so an embedder's own tools are absent — +/// a command this cannot resolve is left to the kernel rather than reported +/// as unknown. +/// +/// Returns `Err` with the parse errors when `source` does not parse, so a +/// caller reports a parse failure as a parse failure and never as an empty +/// issue list. +pub fn validate_program( + source: &str, +) -> Result, Vec> { + use std::collections::HashMap; + + let program = crate::parser::parse(source)?; + let mut registry = crate::tools::ToolRegistry::new(); + crate::tools::register_builtins(&mut registry); + let user_tools: HashMap = HashMap::new(); + Ok(Validator::new(®istry, &user_tools, &[]).validate(&program)) +} diff --git a/crates/kaish-repl/src/main.rs b/crates/kaish-repl/src/main.rs index d3317be7..0aa9a73a 100644 --- a/crates/kaish-repl/src/main.rs +++ b/crates/kaish-repl/src/main.rs @@ -234,9 +234,11 @@ fn run() -> Result { /// Print `source`'s statement plans as JSON and exit — command analysis for /// a consumer that is not written in Rust. /// -/// Nothing executes and no kernel is built: `plan_program` is a pure function -/// of the source text, so this touches no filesystem and needs no capability. -/// `--overlay` is therefore irrelevant here and is ignored. +/// Nothing executes and no kernel is built. `plan_program` is a pure function +/// of the source text, and the validation pass builds only a registry of the +/// compiled-in builtins to read their schemas, so this still touches no +/// filesystem and needs no capability. `--overlay` is therefore irrelevant +/// here and is ignored. /// /// The output is always a JSON object, so a caller parses one shape whatever /// happened: `{"statements": [...]}` and exit 0, or `{"errors": [...]}` and @@ -254,6 +256,13 @@ fn print_plan(source: Option) -> ExitCode { }; match kaish_kernel::plan_program(&source) { Ok(statements) => { + // A plan that parses can still be a program the kernel refuses. + // Reporting it as a clean plan makes the dry run worse than + // useless — the caller commits to a command that cannot run. + let refusals = plan_validation_errors(&source); + if !refusals.is_empty() { + return print_plan_errors(refusals); + } let doc = serde_json::json!({ "statements": statements, "kaish_version": kaish_kernel::KAISH_VERSION, @@ -263,8 +272,8 @@ fn print_plan(source: Option) -> ExitCode { println!("{doc}"); ExitCode::SUCCESS } - Err(errors) => { - let errors: Vec<_> = errors + Err(errors) => print_plan_errors( + errors .iter() .map(|e| { serde_json::json!({ @@ -273,20 +282,56 @@ fn print_plan(source: Option) -> ExitCode { "end": e.span.end, }) }) - .collect(); - let doc = serde_json::json!({ - "errors": errors, - "kaish_version": kaish_kernel::KAISH_VERSION, - "kaish_git_hash": kaish_kernel::KAISH_GIT_HASH, - "kaish_build_date": kaish_kernel::KAISH_BUILD_DATE, - }); - println!("{doc}"); - // 2 is the usage/parse code, matching a builtin's argv rejection. - ExitCode::from(2) - } + .collect(), + ), } } +/// The validator's errors for `source`, as plan-error JSON objects. +/// +/// Warnings are left out: the kernel filters validation to `Error` before it +/// refuses a program, so anything else would report a plan as unrunnable that +/// the kernel would have run. A source that does not parse returns nothing — +/// the caller is already reporting the parse failure. +fn plan_validation_errors(source: &str) -> Vec { + use kaish_kernel::validator::Severity; + + let Ok(issues) = kaish_kernel::validator::validate_program(source) else { + return Vec::new(); + }; + issues + .iter() + .filter(|issue| issue.severity == Severity::Error) + .map(|issue| { + let mut object = serde_json::Map::new(); + object.insert("message".into(), issue.message.clone().into()); + if let Some(span) = &issue.span { + object.insert("start".into(), span.start.into()); + object.insert("end".into(), span.end.into()); + } + // The suggestion is the fix the caller acts on; dropping it here + // would hand back a refusal with no way forward. + if let Some(suggestion) = &issue.suggestion { + object.insert("suggestion".into(), suggestion.clone().into()); + } + serde_json::Value::Object(object) + }) + .collect() +} + +/// Emit the `{"errors": [...]}` document and the rejection exit code. +fn print_plan_errors(errors: Vec) -> ExitCode { + let doc = serde_json::json!({ + "errors": errors, + "kaish_version": kaish_kernel::KAISH_VERSION, + "kaish_git_hash": kaish_kernel::KAISH_GIT_HASH, + "kaish_build_date": kaish_kernel::KAISH_BUILD_DATE, + }); + println!("{doc}"); + // 2 is the usage/parse code, matching a builtin's argv rejection. + ExitCode::from(REJECTED) +} + /// Report a plan failure that has no position in a source — a missing /// argument, or a file that could not be read. Same shape and same exit code /// as a parse failure, because a caller branches on the shape, not on which @@ -299,7 +344,7 @@ fn print_plan_error(message: &str) -> ExitCode { "kaish_build_date": kaish_kernel::KAISH_BUILD_DATE, }); println!("{doc}"); - ExitCode::from(2) + ExitCode::from(REJECTED) } /// Read plan source from `path`, or from stdin when `path` is `-`. @@ -331,7 +376,9 @@ Options: and each heredoc body with its byte offset. Executes nothing and touches no filesystem. Prints {{"statements": [...]}} and exits 0, or - {{"errors": [...]}} and exits 2. Both carry + {{"errors": [...]}} and exits 2 for a program + kaish would refuse to run, whether it failed to + parse or failed validation. Both carry kaish_version, kaish_git_hash, and kaish_build_date at the top level, so a caller can window results by build without @@ -379,7 +426,7 @@ fn run_script(path: &str, overlay: bool) -> Result { // execution-error wrapper. if let Some(diagnostic) = kaish_repl::format_parse_error(&source) { eprintln!("{diagnostic}"); - return Ok(ExitCode::FAILURE); + return Ok(ExitCode::from(REJECTED)); } // Non-interactive: pipe stdout so command substitution captures output. @@ -403,7 +450,11 @@ fn run_script(path: &str, overlay: bool) -> Result { // `main` would prefix `Error:` and split the chain under // `Caused by:`, which is the noise this path exists to avoid. eprintln!("{e:#}"); - return Ok(ExitCode::FAILURE); + return Ok(if is_rejection(&e) { + ExitCode::from(REJECTED) + } else { + ExitCode::FAILURE + }); } }; @@ -414,6 +465,28 @@ fn run_script(path: &str, overlay: bool) -> Result { } } +/// The exit code for a program kaish refused to run. +/// +/// A lex, parse, or validation failure means no statement executed. That is +/// the same class of mistake a builtin reports with 2 for bad argv, and +/// `kaish --plan` already exits 2 for it, so `-c` and a script file use 2 as +/// well. The alternative, 1, is a *result* in `grep`, `test`, `cmp`, and +/// `diff` — a caller branching on it cannot tell "found nothing" from "never +/// ran". +const REJECTED: u8 = 2; + +/// True when the kernel refused the program outright rather than failing +/// partway through running it. +fn is_rejection(error: &anyhow::Error) -> bool { + use kaish_client::ClientError; + match error.downcast_ref::() { + Some(ClientError::Kernel(kernel_error)) => kernel_error.is_rejected(), + // Any other client error reached us after dispatch began, or never + // reached the kernel at all; neither is a rejection. + _ => false, + } +} + /// Execute a command string and exit. fn run_command(cmd: &str, overlay: bool) -> Result { use kaish_client::EmbeddedClient; @@ -425,7 +498,7 @@ fn run_command(cmd: &str, overlay: bool) -> Result { // execution-error wrapper. if let Some(diagnostic) = kaish_repl::format_parse_error(cmd) { eprintln!("{diagnostic}"); - return Ok(ExitCode::FAILURE); + return Ok(ExitCode::from(REJECTED)); } // Non-interactive: pipe stdout so command substitution captures output. @@ -445,7 +518,11 @@ fn run_command(cmd: &str, overlay: bool) -> Result { Err(e) => { // See `run_script`: the diagnostic is the message, printed as-is. eprintln!("{e:#}"); - return Ok(ExitCode::FAILURE); + return Ok(if is_rejection(&e) { + ExitCode::from(REJECTED) + } else { + ExitCode::FAILURE + }); } }; diff --git a/crates/kaish-repl/tests/error_presentation_tests.rs b/crates/kaish-repl/tests/error_presentation_tests.rs index 7f10bc8e..595a3580 100644 --- a/crates/kaish-repl/tests/error_presentation_tests.rs +++ b/crates/kaish-repl/tests/error_presentation_tests.rs @@ -49,7 +49,7 @@ fn assert_no_wrapper_noise(text: &str) { #[test] fn cli_parse_error_prints_diagnostic_directly() { let (stdout, stderr, code) = run_kaish(&["-c", "echo $GREET/world.txt"]); - assert_eq!(code, 1, "a parse failure must still exit 1: stderr={stderr:?}"); + assert_eq!(code, 2, "a refused program exits 2: stderr={stderr:?}"); assert_eq!(stdout, "", "a parse failure must not execute anything"); assert!( stderr.starts_with("1:6 [parse]:"), @@ -65,7 +65,7 @@ fn cli_parse_error_prints_diagnostic_directly() { #[test] fn cli_lexer_error_prints_diagnostic_directly() { let (stdout, stderr, code) = run_kaish(&["-c", "echo `ls`"]); - assert_eq!(code, 1, "a lexer failure must still exit 1: stderr={stderr:?}"); + assert_eq!(code, 2, "a refused program exits 2: stderr={stderr:?}"); assert_eq!(stdout, ""); // `[parse]` here is inherited from `ParseError::format`, which labels // every diagnostic `[parse]` regardless of whether the lexer or the @@ -121,7 +121,7 @@ fn cli_validation_error_leads_with_its_diagnostic() { // wrapper named the phase and nothing else, so it displaced the one line // that identified the problem. let (stdout, stderr, code) = run_kaish(&["-c", "v=1; for x in $v; do echo $x; done"]); - assert_eq!(code, 1); + assert_eq!(code, 2, "a validator rejection is a refused program"); assert_eq!(stdout, ""); assert_no_wrapper_noise(&stderr); assert!( @@ -172,7 +172,7 @@ fn cli_script_parse_error_prints_diagnostic_with_correct_line() { let script_path = dir.path().join("bad.kai"); std::fs::write(&script_path, "echo ok\necho $GREET/world.txt\n").expect("write script"); let (stdout, stderr, code) = run_kaish(&[script_path.to_str().expect("utf8 path")]); - assert_eq!(code, 1); + assert_eq!(code, 2, "a refused script exits 2, like a refused -c program"); assert_eq!( stdout, "", "the whole script is parsed up front — a later parse failure must run nothing, including line 1" @@ -195,7 +195,7 @@ fn cli_shebang_script_parse_error_reports_the_source_line() { std::fs::write(&script_path, "#!/usr/bin/env kaish\necho ok\necho $GREET/world.txt\n") .expect("write script"); let (stdout, stderr, code) = run_kaish(&[script_path.to_str().expect("utf8 path")]); - assert_eq!(code, 1); + assert_eq!(code, 2, "a refused script exits 2, like a refused -c program"); assert_eq!(stdout, "", "a parse failure runs nothing"); assert!( stderr.starts_with("3:6 [parse]:"), diff --git a/crates/kaish-repl/tests/exit_code_convention_tests.rs b/crates/kaish-repl/tests/exit_code_convention_tests.rs new file mode 100644 index 00000000..0938fb5d --- /dev/null +++ b/crates/kaish-repl/tests/exit_code_convention_tests.rs @@ -0,0 +1,193 @@ +//! Exit-code convention: what a caller may conclude from `$?`. +//! +//! kaish reserves exit 1 for a *result* wherever a builtin already uses it as +//! one — `grep` found nothing, `test` was false, `cmp`/`diff` saw a +//! difference. In those builtins every error reports 2 instead, so a caller +//! branching on 1 never mistakes a broken command for a negative answer. +//! `kaish -c` and `kaish --plan` apply the same rule to a whole program: a +//! rejection (lex, parse, or validation) exits 2, matching the usage-error +//! code a builtin returns for bad argv. +//! +//! These drive the real binary because the contract is the process one. + +// Test-fixture code: unwrap/expect on known-good setup is the idiom here. +#![allow(clippy::unwrap_used, clippy::expect_used)] + +use std::path::Path; +use std::process::Command; + +use tempfile::TempDir; + +/// A directory holding `lines.txt`, whose middle line contains `[cast:`. +fn fixture() -> TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("lines.txt"), "alpha\n[cast:deepseek]\nbeta\n") + .expect("write fixture"); + dir +} + +/// Run `kaish -c ` in `cwd`; return (exit code, stdout, stderr). +fn run_c(cwd: &Path, source: &str) -> (i32, String, String) { + let out = Command::new(env!("CARGO_BIN_EXE_kaish")) + .current_dir(cwd) + .arg("-c") + .arg(source) + .output() + .expect("run kaish -c"); + ( + out.status.code().expect("exit code"), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) +} + +/// Run `kaish --plan `; return (exit code, parsed stdout). +fn plan(source: &str) -> (i32, serde_json::Value) { + let out = Command::new(env!("CARGO_BIN_EXE_kaish")) + .arg("--plan") + .arg(source) + .output() + .expect("run kaish --plan"); + let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); + let json = serde_json::from_str(&stdout) + .unwrap_or_else(|e| panic!("stdout was not JSON ({e}): {stdout:?}")); + (out.status.code().expect("exit code"), json) +} + +// --- grep: 1 means "no match" and nothing else ------------------------------ + +#[test] +fn grep_exits_1_only_when_it_searched_and_found_nothing() { + let dir = fixture(); + let (code, out, _) = run_c(dir.path(), "grep zzz lines.txt"); + assert_eq!(code, 1, "no-match is the one thing grep spends exit 1 on"); + assert_eq!(out, ""); +} + +#[test] +fn grep_exits_2_for_an_unclosed_character_class() { + // The regression: `grep -v '[cast:'` is correctly quoted, so the pattern + // reaches the regex engine intact and fails to compile there. Reporting + // that as 1 is indistinguishable from "no lines matched". + let dir = fixture(); + let (code, _, err) = run_c(dir.path(), "grep -v '[cast:' lines.txt"); + assert_eq!(code, 2, "an uncompilable pattern is a usage error, not a result"); + assert!(err.contains("unclosed character class"), "stderr was: {err}"); +} + +#[test] +fn grep_exits_2_for_a_bad_pattern_that_arrives_through_a_variable() { + // The validator compiles a *literal* pattern and skips anything holding a + // `` marker, so a pattern that arrives through `$p` reaches the + // regex builders inside execute() instead. That is the same failure, and + // it must not come back as 1 just because the validator could not see it. + let dir = fixture(); + let (code, _, err) = run_c(dir.path(), "p='[cast:'; grep -v \"$p\" lines.txt"); + assert_eq!(code, 2, "a pattern is no less broken for being computed"); + assert!(err.contains("invalid pattern"), "stderr was: {err}"); +} + +#[test] +fn grep_exits_2_when_a_file_cannot_be_read() { + let dir = fixture(); + let (code, _, _) = run_c(dir.path(), "grep alpha no_such_file.txt"); + assert_eq!(code, 2, "an unreadable file is not 'found no matches'"); +} + +#[test] +fn grep_exits_2_when_the_pattern_argument_is_missing() { + let dir = fixture(); + let (code, _, _) = run_c(dir.path(), "grep"); + assert_eq!(code, 2); +} + +#[test] +fn escaping_the_bracket_is_the_fix_the_error_names() { + let dir = fixture(); + let (code, out, _) = run_c(dir.path(), r"grep -v '\[cast:' lines.txt"); + assert_eq!(code, 0); + assert_eq!(out.trim(), "alpha\nbeta"); +} + +#[test] +fn the_invalid_pattern_error_names_the_fix_and_not_our_regex_crate() { + let dir = fixture(); + let (_, _, err) = run_c(dir.path(), "grep -v '[cast:' lines.txt"); + assert!( + err.contains(r"\["), + "the error must name the escape that fixes it; stderr was: {err}" + ); + assert!( + !err.contains("docs.rs"), + "an implementation crate is not an affordance; stderr was: {err}" + ); +} + +// --- diff/cmp/test: 1 is already a result there too ------------------------- + +#[test] +fn diff_exits_1_for_a_difference_and_2_for_a_missing_operand() { + let dir = fixture(); + std::fs::write(dir.path().join("other.txt"), "alpha\nchanged\nbeta\n").unwrap(); + + let (differ, _, _) = run_c(dir.path(), "diff lines.txt other.txt"); + assert_eq!(differ, 1, "files differ is diff's result, not an error"); + + let (usage, _, _) = run_c(dir.path(), "diff lines.txt"); + assert_eq!(usage, 2, "a missing operand is a usage error"); +} + +#[test] +fn cmp_keeps_1_for_a_difference() { + let dir = fixture(); + std::fs::write(dir.path().join("other.txt"), "alpha\nchanged\nbeta\n").unwrap(); + let (code, _, _) = run_c(dir.path(), "cmp lines.txt other.txt"); + assert_eq!(code, 1); +} + +#[test] +fn test_keeps_1_for_a_false_condition() { + let dir = fixture(); + let (code, _, _) = run_c(dir.path(), "test -f no_such_file.txt"); + assert_eq!(code, 1, "false is test's result"); +} + +// --- whole-program rejections ----------------------------------------------- + +#[test] +fn a_parse_rejection_exits_2_like_the_same_source_under_plan() { + let dir = fixture(); + let (via_c, _, _) = run_c(dir.path(), "if"); + let (via_plan, _) = plan("if"); + assert_eq!(via_plan, 2, "--plan already documents 2 for a rejection"); + assert_eq!(via_c, via_plan, "-c and --plan must agree on a rejection"); +} + +#[test] +fn a_validation_rejection_exits_2() { + let dir = fixture(); + let (code, _, err) = run_c(dir.path(), "grep '[cast:' lines.txt"); + assert_eq!(code, 2); + assert!(err.contains("validation failed"), "stderr was: {err}"); +} + +// --- --plan sees what the kernel would reject ------------------------------- + +#[test] +fn plan_reports_a_validation_failure_instead_of_a_clean_plan() { + // `--plan` is the dry run an agent reaches for before committing to a + // command. Printing a clean plan for a program the kernel then rejects + // makes the dry run worse than useless. + let (code, json) = plan("grep '[cast:' lines.txt"); + assert_eq!(code, 2, "a program that cannot run is not a plan"); + let errors = json["errors"].as_array().expect("errors array"); + let text = errors.iter().map(|e| e["message"].as_str().unwrap_or("")).collect::(); + assert!(text.contains("unclosed character class"), "errors were: {errors:?}"); +} + +#[test] +fn plan_still_exits_0_for_a_program_that_would_run() { + let (code, json) = plan(r"grep -v '\[cast:' lines.txt"); + assert_eq!(code, 0); + assert!(json["statements"].is_array()); +} diff --git a/crates/kaish-tool-api/src/ctx.rs b/crates/kaish-tool-api/src/ctx.rs index eb268999..59d6c20e 100644 --- a/crates/kaish-tool-api/src/ctx.rs +++ b/crates/kaish-tool-api/src/ctx.rs @@ -55,8 +55,25 @@ impl PatientGuard { /// /// `#[async_trait]` desugars the `async fn`s below into boxed futures; every /// already-synchronous method is untouched. +/// Restricts `ToolCtx` to the kernel's own execution context. +/// +/// A tool author *receives* a `ToolCtx`; nobody outside the kernel implements +/// one. Sealing says so in the type system, and the kernel's builtins depend +/// on it: each one narrows the `&mut dyn ToolCtx` it is handed back to the +/// concrete `ExecContext` it needs for pipes, jobs, and the dispatcher. With +/// exactly one implementor that narrowing cannot fail, which is what lets it +/// assert instead of inventing an exit code for a case that cannot happen. +/// +/// `#[doc(hidden)]`: reachable for the kernel's own `impl`, kept off the +/// documented surface so it never reads as an extension point. +#[doc(hidden)] +pub mod sealed { + /// Implemented only by the kernel's `ExecContext`. + pub trait Sealed {} +} + #[async_trait] -pub trait ToolCtx: Send + Sync { +pub trait ToolCtx: sealed::Sealed + Send + Sync { /// The backend for file I/O and tool dispatch. /// /// Tools reach the VFS (and re-dispatch other tools) through this handle. diff --git a/crates/kaish-tool-api/src/lib.rs b/crates/kaish-tool-api/src/lib.rs index 969cb68e..d64ccede 100644 --- a/crates/kaish-tool-api/src/lib.rs +++ b/crates/kaish-tool-api/src/lib.rs @@ -34,7 +34,7 @@ mod tool; pub use backend::KernelBackend; pub use clap_schema::{params_from_clap, schema_from_clap, schema_tree_from_clap}; -pub use ctx::{PatientGuard, ToolCtx}; +pub use ctx::{sealed, PatientGuard, ToolCtx}; pub use global_flags::GlobalFlags; pub use issue::{IssueCode, Severity, Span, ValidationIssue}; pub use tool::{is_global_output_flag, validate_against_schema, Tool}; diff --git a/docs/LANGUAGE.md b/docs/LANGUAGE.md index edfe6864..fc37c626 100644 --- a/docs/LANGUAGE.md +++ b/docs/LANGUAGE.md @@ -1304,6 +1304,37 @@ source utils.kai # load utilities . config.kai # dot notation also works ``` +## Exit Codes + +```sh +grep pat file.txt # 0 matched · 1 no match · 2 could not search +kaish -c 'grep "[cast:" f' # 2 — the program was refused, nothing ran +``` + +`0` is success. Past that, kaish keeps one rule: **exit 1 is a result, exit 2 is +a mistake.** + +A builtin that answers a question spends `1` on the negative answer and nothing +else. `grep` exits 1 only when it searched and matched nothing; `test` exits 1 +only when the condition was false; `cmp` and `diff` exit 1 only when the inputs +differ. In those builtins every error — an unreadable file, a missing operand, a +pattern that does not compile — exits `2`, so a caller branching on 1 never +reads a broken command as a negative answer. + +A builtin where `1` is free keeps the familiar split: `2` for a usage error, +`1` for an operational failure. `cat missing.txt` exits 1. + +A whole program kaish refuses exits `2`. A lex, parse, or validation failure +means no statement ran, which is the same class of mistake as bad argv: + +```sh +kaish -c 'if' # 2 — parse error +kaish --plan 'if' # 2 — the same source, the same code +``` + +`124` (timeout) and `123` (a scatter worker failed) are the documented +exceptions; see "Cancellation and Timeouts" and "散・集 (San/Shū)". + ## Background Jobs ```sh