Skip to main content

kaish_kernel/tools/builtin/
mod.rs

1//! Built-in tools for kaish.
2//!
3//! These tools are always available and provide core functionality.
4
5mod alias;
6mod assert;
7mod awk;
8mod base64_tool;
9mod basename;
10#[cfg(feature = "subprocess")]
11mod bg;
12mod cat;
13mod cd;
14mod checksum;
15mod cmp;
16mod cp;
17mod cut;
18mod date;
19mod dd;
20mod diff;
21mod dirname;
22mod patch;
23mod echo;
24mod env;
25#[cfg(feature = "subprocess")]
26mod exec;
27#[cfg(feature = "subprocess")]
28mod spawn;
29#[cfg(feature = "subprocess")]
30pub use spawn::{resolve_in_path, virtual_cwd_error};
31mod export;
32#[cfg(feature = "subprocess")]
33mod fg;
34mod file;
35mod fromjson;
36mod fromjsonl;
37mod glob;
38mod find;
39pub(crate) mod format_string;
40mod gather;
41mod grep;
42mod grep_engine;
43mod regex_dialect;
44mod head;
45mod ignore;
46mod help;
47// `hostname` is pure host introspection (reads /proc) — gated behind the host
48// capability. `uname` stays available and reports kaish identity instead.
49#[cfg(feature = "host")]
50mod hostname;
51mod introspect;
52mod jobs;
53mod jq_native;
54mod kaish_ast;
55mod kaish_clear;
56mod kaish_last;
57mod kaish_trash;
58mod kaish_status;
59mod kaish_version;
60mod kaish_vfs;
61mod keys;
62mod kill;
63mod ln;
64mod ls;
65mod mkdir;
66mod mktemp;
67mod mv;
68mod output_limit;
69mod plan;
70mod printf;
71mod push;
72mod pwd;
73mod random;
74mod read;
75mod readlink;
76mod realpath;
77mod rm;
78mod scatter;
79mod sed;
80mod seq;
81mod split;
82mod set;
83mod sleep;
84mod sort;
85mod stat;
86mod tac;
87mod tail;
88mod tee;
89mod test;
90mod timeout;
91mod tojson;
92mod tojsonl;
93#[cfg(feature = "tokens")]
94mod tokens;
95mod touch;
96mod tr;
97mod tree;
98mod true_false;
99// Module named `type_of`, not `typeof` — `typeof` is a reserved (but unused)
100// Rust keyword, so `mod typeof;` doesn't compile. The tool name is still the
101// plain string "typeof" (see `Tool::name`).
102mod type_of;
103mod uname;
104mod uniq;
105mod unset;
106mod validate;
107mod values;
108mod vars;
109mod wait;
110mod wc;
111#[cfg(feature = "subprocess")]
112mod which;
113mod write;
114mod xxd;
115
116use super::ToolRegistry;
117use crate::validator::{IssueCode, ValidationIssue};
118
119/// Warn when a name argument is spelled in two scripts.
120///
121/// The validator walker sees an assignment target and nothing else, so the
122/// builtins that take a name as an argument word — `export`, `read`, `unset`,
123/// `push`, and `scatter --as` — report the same rule from their own
124/// `Tool::validate`. `PАTH=/bin` and `export PАTH=/bin` have to agree.
125///
126/// `<dynamic>` is the walker's placeholder for a word it cannot read before
127/// execution (`unset "$name"`). There is no spelling to judge, so it is
128/// skipped rather than reported. The placeholder is single-script Latin and
129/// would not warn on its own; the guard says it is not a name, and holds if
130/// the sentinel ever changes.
131pub(crate) fn mixed_script_issue(name: &str) -> Option<ValidationIssue> {
132    if name == "<dynamic>" {
133        return None;
134    }
135    let mixed = crate::name::mixed_script(name)?;
136    Some(
137        ValidationIssue::warning(IssueCode::MixedScriptName, mixed.to_string())
138            .with_suggestion(mixed.suggestion()),
139    )
140}
141
142/// Read a repeatable string-valued flag off the raw `ToolArgs`.
143///
144/// Repeatable value flags (clap `Append`) are accumulated by the kernel into a
145/// `Value::Json(Array)` under the flag's long name; a single occurrence may
146/// arrive as a bare `Value::String`. `to_argv()` can't round-trip the array, so
147/// the clap field isn't a reliable source — search builtins read the raw args
148/// here. Shared by grep/glob `--ftype`/`--ftype-not`; mirrors sed's
149/// `collect_expressions` (see the repeatable-flag gotcha in `arch_repeatable_flags`).
150///
151/// A non-string occurrence is a loud error, never a silent skip — a dropped
152/// filter leaves the caller running unfiltered while believing it narrowed the
153/// search. Binary never reaches here: `flag_value_to_json` stops it at the
154/// binder (GH #223), which is the only layer that can tell real bytes from a
155/// record that merely looks like the envelope.
156pub(crate) fn read_repeatable_strings(
157    args: &super::ToolArgs,
158    key: &str,
159) -> Result<Vec<String>, String> {
160    use crate::ast::Value;
161    match args.named.get(key) {
162        Some(Value::Json(serde_json::Value::Array(items))) => {
163            let mut out = Vec::with_capacity(items.len());
164            for v in items {
165                match v.as_str() {
166                    Some(s) => out.push(s.to_string()),
167                    None => {
168                        return Err(format!(
169                            "a repeatable flag value must be a string, got `{v}`"
170                        ))
171                    }
172                }
173            }
174            Ok(out)
175        }
176        Some(Value::String(s)) => Ok(vec![s.clone()]),
177        Some(other) => {
178            match crate::interpreter::value_to_text_sink_named(other, "a repeatable flag value") {
179                Ok(s) => Ok(vec![s]),
180                Err(e) => Err(e.to_string()),
181            }
182        }
183        None => Ok(Vec::new()),
184    }
185}
186
187/// Read a path-typed positional/named arg as a string, going LOUD on a
188/// `Value::Bytes` operand rather than `ToolArgs::get_string`'s silent `None`
189/// (that method lives in the `kaish-types` leaf crate, which has no
190/// `EvalError`/rich-error machinery to report *why* it returned nothing, so it
191/// just treats any non-scalar-text value as absent).
192///
193/// A caller reading `get_string` for a *path* generally treats `None` as
194/// "operand missing" and either errors or falls back to some other input
195/// (stdin, `$HOME`, …) — exactly the silent-wrong-source shape GH #93 item 1
196/// is about, just reached through `get_string`'s catch-all instead of
197/// `value_to_string`'s placeholder. `Ok(None)` here means genuinely absent;
198/// `Err` means it was present and binary.
199pub(crate) fn get_path_string(
200    args: &super::ToolArgs,
201    name: &str,
202    positional_index: usize,
203) -> Result<Option<String>, String> {
204    use crate::ast::Value;
205    match args.get(name, positional_index) {
206        Some(v @ Value::Bytes(_)) => match crate::interpreter::value_to_text_sink_named(v, "a path") {
207            Ok(s) => Ok(Some(s)),
208            Err(e) => Err(e.to_string()),
209        },
210        _ => Ok(args.get_string(name, positional_index)),
211    }
212}
213
214/// Refuse to let `ls`/`find`/`glob` report a name containing a newline as
215/// TEXT. `for f in $(cmd)`, a pipe, and `OutputData::to_canonical_string()`
216/// itself all treat one newline as one path boundary; a name that already
217/// contains a newline splits into two lines that name no file that exists
218/// (measured: a 2-file directory with one `we\nird.txt` name counted as 3
219/// items under `for f in $(ls dir)`). This is [`value_to_text_sink_named`]'s
220/// sibling at the output boundary rather than the interpolation boundary —
221/// same shape (name the sink, go loud rather than corrupt silently), a
222/// different one.
223///
224/// Walks `output`'s tree (root and children, so `ls -R`'s nested listing is
225/// covered too) and returns the first offending name, with its newline
226/// rendered as the two characters `\n` so the message stays on one line.
227///
228/// Call this only after confirming `--json` was not requested
229/// (`ctx.output_format.is_none()`): `--json` serializes each name as its own
230/// JSON string and never joins names by newline, so it stays the documented,
231/// lossless way to read a newline-bearing name — named in the error below.
232///
233/// [`value_to_text_sink_named`]: crate::interpreter::value_to_text_sink_named
234pub(crate) fn guard_no_newline_names(
235    builtin: &str,
236    output: &crate::interpreter::OutputData,
237) -> Result<(), String> {
238    fn first_newline_name(nodes: &[crate::interpreter::OutputNode]) -> Option<&str> {
239        for node in nodes {
240            let name = node.display_name();
241            if name.contains('\n') {
242                return Some(name);
243            }
244            if let Some(found) = first_newline_name(&node.children) {
245                return Some(found);
246            }
247        }
248        None
249    }
250
251    if let Some(name) = first_newline_name(&output.root) {
252        let escaped = name.replace('\n', "\\n");
253        return Err(format!(
254            "{builtin}: '{escaped}': a newline in a filename cannot be reported as \
255             text — the line split would report two files that do not exist. \
256             Use `{builtin} --json` to read it losslessly, or rename the file."
257        ));
258    }
259    Ok(())
260}
261
262/// Register all built-in tools with the registry.
263pub fn register_builtins(registry: &mut ToolRegistry) {
264    registry.register(alias::Alias);
265    registry.register(alias::Unalias);
266    registry.register(assert::Assert);
267    registry.register(awk::Awk);
268    registry.register(base64_tool::Base64Tool);
269    registry.register(basename::Basename);
270    #[cfg(feature = "subprocess")]
271    registry.register(bg::Bg);
272    registry.register(cat::Cat);
273    registry.register(cd::Cd);
274    registry.register(checksum::Checksum);
275    registry.register(cmp::Cmp);
276    registry.register(cp::Cp);
277    registry.register(cut::Cut);
278    registry.register(date::Date::new());
279    registry.register(dd::Dd);
280    registry.register(diff::Diff);
281    registry.register(dirname::Dirname);
282    registry.register(echo::Echo);
283    registry.register(env::Env);
284    #[cfg(feature = "subprocess")]
285    registry.register(exec::Exec);
286    #[cfg(feature = "subprocess")]
287    registry.register(spawn::Spawn);
288    registry.register(export::Export);
289    #[cfg(feature = "subprocess")]
290    registry.register(fg::Fg);
291    registry.register(file::File);
292    registry.register(fromjson::FromJson);
293    registry.register(fromjsonl::FromJsonl);
294    registry.register(glob::Glob);
295    registry.register(find::Find);
296    registry.register(gather::Gather);
297    registry.register(grep::Grep);
298    registry.register(head::Head);
299    registry.register(help::Help);
300    registry.register(ignore::KaishIgnore);
301    #[cfg(feature = "host")]
302    registry.register(hostname::Hostname);
303    registry.register(introspect::Mounts);
304    registry.register(introspect::Tools);
305    registry.register(jobs::Jobs);
306    registry.register(jq_native::JqNative);
307    registry.register(kaish_ast::KaishAst);
308    registry.register(kaish_clear::KaishClear);
309    registry.register(kaish_last::KaishLast);
310    registry.register(kaish_trash::KaishTrash);
311    registry.register(kaish_status::KaishStatus);
312    registry.register(kaish_version::KaishVersion);
313    registry.register(kaish_vfs::KaishVfs);
314    registry.register(keys::Keys);
315    registry.register(kill::Kill);
316    registry.register(ln::Ln);
317    registry.register(ls::Ls);
318    registry.register(mkdir::Mkdir);
319    registry.register(mktemp::Mktemp);
320    registry.register(mv::Mv);
321    registry.register(output_limit::KaishOutputLimit);
322    registry.register(patch::Patch);
323    registry.register(plan::PlanTool);
324    registry.register(printf::Printf);
325    registry.register(push::Push);
326    #[cfg(all(target_os = "linux", feature = "host"))]
327    registry.register(kaish_tools_host::Ps);
328    registry.register(pwd::Pwd);
329    registry.register(random::Random);
330    registry.register(read::Read);
331    registry.register(readlink::Readlink);
332    registry.register(realpath::Realpath);
333    registry.register(rm::Rm);
334    registry.register(scatter::Scatter);
335    registry.register(sed::Sed);
336    registry.register(seq::Seq);
337    registry.register(set::Set);
338    registry.register(split::Split);
339    registry.register(sleep::Sleep);
340    registry.register(sort::Sort);
341    registry.register(stat::Stat);
342    registry.register(tac::Tac);
343    registry.register(tail::Tail);
344    registry.register(tee::Tee);
345    registry.register(test::Test);
346    registry.register(timeout::Timeout);
347    #[cfg(feature = "tokens")]
348    registry.register(tokens::Tokens);
349    registry.register(tojson::ToJson);
350    registry.register(tojsonl::ToJsonl);
351    registry.register(touch::Touch);
352    registry.register(tr::Tr);
353    registry.register(tree::Tree);
354    registry.register(true_false::True);
355    registry.register(true_false::False);
356    registry.register(true_false::Colon);
357    registry.register(type_of::TypeOf);
358    registry.register(uname::Uname);
359    registry.register(uniq::Uniq);
360    registry.register(unset::Unset);
361    registry.register(validate::Validate);
362    registry.register(values::Values);
363    registry.register(vars::Vars);
364    registry.register(wait::Wait);
365    registry.register(wc::Wc);
366    #[cfg(feature = "subprocess")]
367    registry.register(which::Which);
368    registry.register(write::Write);
369    registry.register(xxd::Xxd);
370}