use async_trait::async_trait;
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};
pub struct Stat;
#[derive(Parser, Debug)]
#[command(name = "stat", about = "Display file status")]
struct StatArgs {
#[arg(short = 'c', long)]
format: Option<String>,
#[command(flatten)]
global: GlobalFlags,
paths: Vec<String>,
}
#[async_trait]
impl Tool for Stat {
fn name(&self) -> &str {
"stat"
}
fn schema(&self) -> ToolSchema {
schema_from_clap(
&StatArgs::command(),
"stat",
"Display file status",
[
("Show file info", "stat README.md"),
("Just the size", "stat --format '%s' file.txt"),
],
)
}
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 StatArgs::try_parse_from(
std::iter::once("stat".to_string()).chain(args.to_argv()),
) {
Ok(p) => p,
Err(e) => return ExecResult::failure(2, format!("stat: {e}")),
};
parsed.global.apply(ctx);
if args.positional.is_empty() {
return ExecResult::failure(1, "stat: missing path argument");
}
let format = args
.get_string("format", usize::MAX)
.or_else(|| args.get_string("c", usize::MAX));
let mut nodes: Vec<OutputNode> = Vec::with_capacity(args.positional.len());
let mut format_output = String::new();
let mut last_err: Option<String> = None;
for value in &args.positional {
let path_str = crate::interpreter::value_to_string(value);
let resolved = ctx.resolve_path(&path_str);
match ctx.backend.stat(Path::new(&resolved)).await {
Ok(info) => {
if let Some(fmt) = &format {
format_output.push_str(&format_stat(fmt, &path_str, &info));
} else {
let is_dir = info.is_dir();
let file_type = if is_dir { "directory" } else { "regular file" };
let entry_type = if is_dir { EntryType::Directory } else { EntryType::File };
nodes.push(
OutputNode::new(&path_str)
.with_cells(vec![info.size.to_string(), file_type.to_string()])
.with_entry_type(entry_type),
);
}
}
Err(e) => {
last_err = Some(format!("stat: {}: {}", path_str, e));
}
}
}
let mut result = if format.is_some() {
ExecResult::with_output(OutputData::text(format_output))
} else {
let headers = vec!["FILE".to_string(), "SIZE".to_string(), "TYPE".to_string()];
ExecResult::with_output(OutputData::table(headers, nodes))
};
if let Some(msg) = last_err {
result.err = msg;
result = result.with_code(1);
}
result
}
}
fn format_stat(fmt: &str, name: &str, info: &crate::vfs::DirEntry) -> String {
let mut result = fmt.to_string();
result = result.replace("%n", name);
result = result.replace("%s", &info.size.to_string());
let file_type = if info.is_dir() { "directory" } else { "regular file" };
result = result.replace("%F", file_type);
if !result.ends_with('\n') {
result.push('\n');
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Value;
use crate::vfs::{Filesystem, MemoryFs, VfsRouter};
use std::sync::Arc;
async fn make_ctx() -> ExecContext {
let mut vfs = VfsRouter::new();
let mem = MemoryFs::new();
mem.write(Path::new("file.txt"), b"hello world")
.await
.unwrap();
mem.mkdir(Path::new("mydir")).await.unwrap();
vfs.mount("/", mem);
ExecContext::new(Arc::new(vfs))
}
#[tokio::test]
async fn test_stat_file() {
let mut ctx = make_ctx().await;
let mut args = ToolArgs::new();
args.positional.push(Value::String("/file.txt".into()));
let result = Stat.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.has_output());
let output = result.output().unwrap();
assert!(output.headers.is_some());
assert_eq!(output.root.len(), 1);
assert!(result.text_out().contains("file.txt"));
assert!(result.text_out().contains("11")); }
#[tokio::test]
async fn test_stat_directory() {
let mut ctx = make_ctx().await;
let mut args = ToolArgs::new();
args.positional.push(Value::String("/mydir".into()));
let result = Stat.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.has_output());
let output = result.output().unwrap();
assert_eq!(output.root.len(), 1);
assert!(result.text_out().contains("directory"));
}
#[tokio::test]
async fn test_stat_format() {
let mut ctx = make_ctx().await;
let mut args = ToolArgs::new();
args.positional.push(Value::String("/file.txt".into()));
args.named
.insert("format".to_string(), Value::String("%n: %s bytes".into()));
let result = Stat.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(result.text_out().trim(), "/file.txt: 11 bytes");
}
#[tokio::test]
async fn test_stat_not_found() {
let mut ctx = make_ctx().await;
let mut args = ToolArgs::new();
args.positional.push(Value::String("/nonexistent".into()));
let result = Stat.execute(args, &mut ctx).await;
assert!(!result.ok());
}
#[tokio::test]
async fn test_stat_missing_path() {
let mut ctx = make_ctx().await;
let result = Stat.execute(ToolArgs::new(), &mut ctx).await;
assert!(!result.ok());
}
}