use async_trait::async_trait;
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};
pub struct Uname;
#[derive(Parser, Debug)]
#[command(name = "uname", about = "Print system identification")]
struct UnameArgs {
#[arg(id = "s", short = 's', long = "s")]
_s: bool,
#[arg(id = "n", short = 'n', long = "n")]
_n: bool,
#[arg(id = "r", short = 'r', long = "r")]
_r: bool,
#[arg(id = "v", short = 'v', long = "v")]
_v: bool,
#[arg(id = "m", short = 'm', long = "m")]
_m: bool,
#[arg(id = "o", short = 'o', long = "o")]
_o: bool,
#[arg(id = "p", short = 'p', long = "p")]
_p: bool,
#[arg(id = "i", short = 'i', long = "i")]
_i: bool,
#[arg(id = "a", short = 'a', long = "a")]
_a: bool,
#[arg(id = "host", long = "host")]
#[cfg_attr(not(feature = "host"), arg(hide = true))]
_host: bool,
#[command(flatten)]
global: GlobalFlags,
}
struct UnameInfo {
sysname: String,
nodename: String,
release: String,
version: String,
machine: String,
os: String,
}
impl UnameInfo {
fn kaish(nodename: String) -> Self {
let version = env!("CARGO_PKG_VERSION");
let git_hash = option_env!("KAISH_GIT_HASH").unwrap_or("unknown");
let build_date = option_env!("KAISH_BUILD_DATE").unwrap_or("unknown");
Self {
sysname: "kaish".to_string(),
nodename,
release: version.to_string(),
version: format!("kaish {version} ({git_hash} {build_date})"),
machine: std::env::consts::ARCH.to_string(),
os: "Kaijutsu".to_string(),
}
}
#[cfg(feature = "host")]
fn host() -> Self {
let read = |name: &str| {
std::fs::read_to_string(format!("/proc/sys/kernel/{name}"))
.map(|s| s.trim().to_string())
.unwrap_or_else(|_| "unknown".to_string())
};
Self {
sysname: read("ostype"),
nodename: read_hostname(),
release: read("osrelease"),
version: read("version"),
machine: std::env::consts::ARCH.to_string(),
os: "GNU/Linux".to_string(),
}
}
}
fn kaish_nodename(ctx: &ExecContext) -> String {
match ctx.scope.get("HOSTNAME") {
Some(Value::String(name)) if !name.is_empty() => name.clone(),
_ => "kaish".to_string(),
}
}
#[cfg(feature = "host")]
pub(super) fn read_hostname() -> String {
std::fs::read_to_string("/proc/sys/kernel/hostname")
.map(|s| s.trim().to_string())
.unwrap_or_else(|_| "unknown".to_string())
}
#[async_trait]
impl Tool for Uname {
fn name(&self) -> &str {
"uname"
}
fn schema(&self) -> ToolSchema {
schema_from_clap(
&UnameArgs::command(),
"uname",
"Print system identification",
[
("System name", "uname"),
("All info", "uname -a"),
("Host OS identity", "uname --host"),
],
)
}
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 parsed = match UnameArgs::try_parse_from(
std::iter::once("uname".to_string()).chain(args.to_argv()),
) {
Ok(p) => p,
Err(e) => return ExecResult::failure(2, format!("uname: {e}")),
};
parsed.global.apply(ctx);
let host_mode = args.has_flag("host");
let info = if host_mode {
#[cfg(feature = "host")]
{
UnameInfo::host()
}
#[cfg(not(feature = "host"))]
{
return ExecResult::failure(
2,
"uname: --host is unavailable in this build (no host capability)".to_string(),
);
}
} else {
UnameInfo::kaish(kaish_nodename(ctx))
};
let all = args.has_flag("a");
let flag_s = all || args.has_flag("s");
let flag_n = all || args.has_flag("n");
let flag_r = all || args.has_flag("r");
let flag_v = all || args.has_flag("v");
let flag_m = all || args.has_flag("m") || args.has_flag("p") || args.has_flag("i");
let flag_o = all || args.has_flag("o");
let any_explicit = flag_s || flag_n || flag_r || flag_v || flag_m || flag_o;
let mut parts: Vec<&str> = Vec::new();
if flag_s || !any_explicit {
parts.push(&info.sysname);
}
if flag_n {
parts.push(&info.nodename);
}
if flag_r {
parts.push(&info.release);
}
if flag_v {
parts.push(&info.version);
}
if flag_m {
parts.push(&info.machine);
}
if flag_o {
parts.push(&info.os);
}
let output = parts.join(" ");
ExecResult::with_output(OutputData::text(output))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vfs::{MemoryFs, VfsRouter};
use std::sync::Arc;
fn make_ctx() -> ExecContext {
let mut vfs = VfsRouter::new();
vfs.mount("/", MemoryFs::new());
ExecContext::new(Arc::new(vfs))
}
#[tokio::test]
async fn test_default_is_sysname() {
let mut ctx = make_ctx();
let result = Uname.execute(ToolArgs::new(), &mut ctx).await;
assert!(result.ok());
assert_eq!(&*result.text_out(), "kaish");
}
#[tokio::test]
async fn test_flag_s() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("s".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(&*result.text_out(), "kaish");
}
#[tokio::test]
async fn test_flag_n_defaults_to_kaish() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("n".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(&*result.text_out(), "kaish");
}
#[tokio::test]
async fn test_flag_n_uses_hostname_var() {
let mut ctx = make_ctx();
ctx.scope.set_exported("HOSTNAME", Value::String("sandbox-01".into()));
let mut args = ToolArgs::new();
args.flags.insert("n".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(&*result.text_out(), "sandbox-01");
}
#[tokio::test]
async fn test_flag_r() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("r".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(&*result.text_out(), env!("CARGO_PKG_VERSION"));
}
#[tokio::test]
async fn test_flag_v() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("v".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.text_out().starts_with("kaish "));
assert!(result.text_out().contains(env!("CARGO_PKG_VERSION")));
}
#[tokio::test]
async fn test_flag_m() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("m".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(&*result.text_out(), std::env::consts::ARCH);
}
#[tokio::test]
async fn test_flag_o() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("o".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(&*result.text_out(), "Kaijutsu");
}
#[tokio::test]
async fn test_processor_aliases_machine() {
let mut ctx = make_ctx();
let mut args_p = ToolArgs::new();
args_p.flags.insert("p".to_string());
let result_p = Uname.execute(args_p, &mut ctx).await;
let mut args_m = ToolArgs::new();
args_m.flags.insert("m".to_string());
let result_m = Uname.execute(args_m, &mut ctx).await;
assert_eq!(&*result_p.text_out(), &*result_m.text_out());
let mut args_i = ToolArgs::new();
args_i.flags.insert("i".to_string());
let result_i = Uname.execute(args_i, &mut ctx).await;
assert_eq!(&*result_i.text_out(), &*result_m.text_out());
}
#[tokio::test]
async fn test_all_flag() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("a".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
let text = result.text_out();
let fields: Vec<&str> = text.split_whitespace().collect();
assert!(fields.len() >= 6, "expected ≥6 fields, got: {:?}", fields);
assert_eq!(fields[0], "kaish");
assert_eq!(*fields.last().unwrap(), "Kaijutsu");
}
#[tokio::test]
async fn test_multiple_flags() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("s".to_string());
args.flags.insert("n".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
let text = result.text_out();
let parts: Vec<&str> = text.split(' ').collect();
assert_eq!(parts.len(), 2);
assert_eq!(parts[0], "kaish");
}
#[cfg(all(target_os = "linux", feature = "host"))]
#[tokio::test]
async fn test_host_mode() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("host".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
assert_ne!(&*result.text_out(), "kaish", "host mode should return real OS");
assert_eq!(&*result.text_out(), "Linux");
}
#[cfg(all(target_os = "linux", feature = "host"))]
#[tokio::test]
async fn test_host_all() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("host".to_string());
args.flags.insert("a".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(result.ok());
let text = result.text_out();
let fields: Vec<&str> = text.split_whitespace().collect();
assert_eq!(fields[0], "Linux");
assert_eq!(*fields.last().unwrap(), "GNU/Linux");
}
#[cfg(not(feature = "host"))]
#[tokio::test]
async fn test_host_unavailable_without_native() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.flags.insert("host".to_string());
let result = Uname.execute(args, &mut ctx).await;
assert!(!result.ok(), "--host must error without the host capability");
}
}