1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! keys — the keys of a collection, in insertion order.
//!
//! Part of the native-collections OPS surface (see `docs/arrays-and-hashes.md`,
//! "OPS — keys/values are builtins; length is the param-expansion `${#…}`").
//! `keys $collection` accepts a collection **directly** (never nested through
//! another builtin) so it never falls into the `len $(keys $r)`-style nesting
//! trap that motivated this design.
//!
//! jq semantics for the two collection shapes:
//! - a **record** → its keys, insertion order;
//! - a **list** → its indices as integers, `0..N-1`.
//!
//! This makes `$(keys $c)` the uniform, unambiguous iteration idiom over ANY
//! collection — so bare-var `for x in $list` in a for-head is never needed.
//!
//! Pure data transform — no OS, no VFS — so it belongs in every capability
//! build, same footing as `fromjson`/`tojson`.
//!
//! # Examples
//!
//! ```kaish
//! user=$(fromjson '{"name":"amy","role":"maintainer"}')
//! ks=$(keys $user) # ["name","role"]
//! xs=$(fromjson '["a","b","c"]')
//! for i in $(keys $xs); do echo $i; done # 0 1 2 (list indices)
//! ```
use async_trait::async_trait;
use clap::{CommandFactory, Parser};
use crate::ast::Value;
use crate::interpreter::ExecResult;
use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema};
/// keys tool: the keys of a collection (record keys / list indices).
pub struct Keys;
/// clap-derived argv layer for keys.
#[derive(Parser, Debug)]
#[command(name = "keys", about = "The keys of a collection (record keys / list indices)")]
struct KeysArgs {
#[command(flatten)]
global: GlobalFlags,
/// The collection. Hidden sink — the real value is read off
/// `args.positional` per the Value-typed positional rule.
#[arg(hide = true)]
collection: Vec<String>,
}
/// A human-readable type name for the loud "expected a record or list" error.
/// Mirrors the `type_name` idiom already duplicated per-file in this crate
/// (`interpreter/scope.rs`, `interpreter/eval.rs`, `tools/builtin/vars.rs`) —
/// each site names the distinction it cares about, and this one cares about
/// list-vs-record-vs-scalar.
pub(crate) fn describe_kind(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a bool",
Value::Int(_) | Value::Float(_) => "a number",
Value::String(_) => "a string",
Value::Json(serde_json::Value::Array(_)) => "a list",
Value::Json(serde_json::Value::Object(_)) => "a record",
// JSON scalars normally unwrap to native `Value`s at the access boundary,
// but name them precisely if one reaches here rather than lumping them
// all under "a scalar".
Value::Json(serde_json::Value::Null) => "null",
Value::Json(serde_json::Value::Bool(_)) => "a bool",
Value::Json(serde_json::Value::Number(_)) => "a number",
Value::Json(serde_json::Value::String(_)) => "a string",
Value::Bytes(_) => "binary data",
}
}
#[async_trait]
impl Tool for Keys {
fn name(&self) -> &str {
"keys"
}
fn schema(&self) -> ToolSchema {
schema_from_clap(
&KeysArgs::command(),
"keys",
"The keys of a collection — record keys or list indices (in .data)",
[
("Record keys, insertion order", "ks=$(keys $user)"),
("List indices (0..N-1)", "for i in $(keys $xs); do echo $i; done"),
(
"Uniform key iteration",
"for k in $(keys $user); do echo $k; done",
),
],
)
}
async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let argv = match args.to_argv() {
Ok(v) => v,
Err(e) => return ExecResult::failure(2, format!("keys: {e}")),
};
let parsed = match KeysArgs::try_parse_from(
std::iter::once("keys".to_string()).chain(argv),
) {
Ok(p) => p,
Err(e) => return ExecResult::failure(2, format!("keys: {e}")),
};
parsed.global.apply(ctx);
match args.positional.first() {
// A record → its keys, insertion order.
Some(Value::Json(serde_json::Value::Object(map))) => {
let keys: Vec<serde_json::Value> = map
.keys()
.map(|k| serde_json::Value::String(k.clone()))
.collect();
ExecResult::success_data(Value::Json(serde_json::Value::Array(keys)))
}
// A list → its indices as integers, 0..N-1 (jq semantics). This makes
// `for i in $(keys $xs)` the uniform index-iteration idiom.
Some(Value::Json(serde_json::Value::Array(items))) => {
let indices: Vec<serde_json::Value> = (0..items.len())
.map(|i| serde_json::Value::Number(i.into()))
.collect();
ExecResult::success_data(Value::Json(serde_json::Value::Array(indices)))
}
Some(other) => ExecResult::failure(
1,
format!(
"keys: expected a record or list, got {}",
describe_kind(other)
),
),
None => ExecResult::failure(1, "keys: no argument (expected a record or list)"),
}
}
}