use async_trait::async_trait;
use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, FileSystem, InMemoryFs};
use bashkit::{
BashkitContext, ClapBuiltin,
clap::{Parser, Subcommand},
};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
struct PrefixEcho {
prefix: String,
}
#[async_trait]
impl Builtin for PrefixEcho {
async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
let msg = ctx.args.join(" ");
Ok(ExecResult::ok(format!("{}{}\n", self.prefix, msg)))
}
}
struct Transform {
transform_fn: fn(&str) -> String,
}
#[async_trait]
impl Builtin for Transform {
async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
let input = ctx.stdin.unwrap_or("");
Ok(ExecResult::ok((self.transform_fn)(input)))
}
}
struct FileReader;
#[async_trait]
impl Builtin for FileReader {
async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
let path = match ctx.args.first() {
Some(p) => std::path::Path::new(p),
None => return Ok(ExecResult::err("Usage: readfile <path>\n".to_string(), 1)),
};
match ctx.fs.read_file(path).await {
Ok(content) => Ok(ExecResult::ok(
String::from_utf8_lossy(&content).to_string(),
)),
Err(e) => Ok(ExecResult::err(format!("Error: {}\n", e), 1)),
}
}
}
struct Counter {
count: Arc<AtomicU64>,
}
#[async_trait]
impl Builtin for Counter {
async fn execute(&self, _ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
let value = self.count.fetch_add(1, Ordering::SeqCst) + 1;
Ok(ExecResult::ok(format!("{}\n", value)))
}
}
struct Fail {
message: String,
code: i32,
}
#[async_trait]
impl Builtin for Fail {
async fn execute(&self, _ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
Ok(ExecResult::err(format!("{}\n", self.message), self.code))
}
}
struct EnvDumper;
#[async_trait]
impl Builtin for EnvDumper {
async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
let mut output = String::new();
let mut keys: Vec<_> = ctx.env.keys().collect();
keys.sort();
for key in keys {
if let Some(value) = ctx.env.get(key) {
output.push_str(&format!("{}={}\n", key, value));
}
}
Ok(ExecResult::ok(output))
}
}
#[derive(Parser)]
#[command(name = "hello-clap", about = "greet someone")]
struct HelloClapArgs {
#[arg(short, long, default_value = "World")]
name: String,
#[arg(short, long)]
shout: bool,
}
struct HelloClap;
#[async_trait]
impl ClapBuiltin for HelloClap {
type Args = HelloClapArgs;
async fn execute_clap(
&self,
args: Self::Args,
ctx: &mut BashkitContext<'_>,
) -> bashkit::Result<()> {
let greeting = format!("Hello, {}!", args.name);
let greeting = if args.shout {
greeting.to_uppercase()
} else {
greeting
};
ctx.write_stdout(format!("{greeting}\n"));
Ok(())
}
}
#[derive(Parser)]
#[command(name = "math-clap")]
struct MathClapArgs {
#[command(subcommand)]
command: MathClapCommand,
}
#[derive(Subcommand)]
enum MathClapCommand {
Add { left: i64, right: i64 },
Fail { message: String },
StdinLen,
}
struct MathClap;
#[async_trait]
impl ClapBuiltin for MathClap {
type Args = MathClapArgs;
async fn execute_clap(
&self,
args: Self::Args,
ctx: &mut BashkitContext<'_>,
) -> bashkit::Result<()> {
let value = match args.command {
MathClapCommand::Add { left, right } => left + right,
MathClapCommand::Fail { message } => {
ctx.fail(format!("math-clap: {message}\n"), 7);
return Ok(());
}
MathClapCommand::StdinLen => ctx.stdin().unwrap_or("").len() as i64,
};
ctx.write_stdout(format!("{value}\n"));
Ok(())
}
}
#[tokio::test]
async fn test_custom_builtin_simple() {
let mut bash = Bash::builder()
.builtin(
"prefix",
Box::new(PrefixEcho {
prefix: "[LOG] ".to_string(),
}),
)
.build();
let result = bash.exec("prefix hello world").await.unwrap();
assert_eq!(result.stdout, "[LOG] hello world\n");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn test_custom_builtin_no_args() {
let mut bash = Bash::builder()
.builtin(
"prefix",
Box::new(PrefixEcho {
prefix: ">>> ".to_string(),
}),
)
.build();
let result = bash.exec("prefix").await.unwrap();
assert_eq!(result.stdout, ">>> \n");
}
#[tokio::test]
async fn test_custom_builtin_clap_parser() {
let mut bash = Bash::builder()
.builtin("hello-clap", Box::new(HelloClap))
.build();
let result = bash.exec("hello-clap --name Alice --shout").await.unwrap();
assert_eq!(result.stdout, "HELLO, ALICE!\n");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn test_custom_builtin_clap_help_and_errors() {
let mut bash = Bash::builder()
.builtin("hello-clap", Box::new(HelloClap))
.build();
let help = bash.exec("hello-clap --help").await.unwrap();
assert_eq!(help.exit_code, 0);
assert!(help.stdout.contains("Usage: hello-clap"));
assert!(help.stderr.is_empty());
let error = bash.exec("hello-clap --unknown").await.unwrap();
assert_eq!(error.exit_code, 2);
assert!(error.stderr.contains("unexpected argument"));
assert!(error.stdout.is_empty());
}
#[tokio::test]
async fn test_custom_builtin_clap_subcommands_and_stdin() {
let mut bash = Bash::builder()
.builtin("math-clap", Box::new(MathClap))
.build();
let add = bash.exec("math-clap add 20 22").await.unwrap();
assert_eq!(add.stdout, "42\n");
assert_eq!(add.exit_code, 0);
let stdin_len = bash.exec("printf abc | math-clap stdin-len").await.unwrap();
assert_eq!(stdin_len.stdout, "3\n");
assert_eq!(stdin_len.exit_code, 0);
let fail = bash.exec("math-clap fail nope").await.unwrap();
assert_eq!(fail.stdout, "");
assert_eq!(fail.stderr, "math-clap: nope\n");
assert_eq!(fail.exit_code, 7);
}
#[tokio::test]
async fn test_custom_builtin_in_pipeline() {
fn to_upper(s: &str) -> String {
s.to_uppercase()
}
let mut bash = Bash::builder()
.builtin(
"upper",
Box::new(Transform {
transform_fn: to_upper,
}),
)
.build();
let result = bash.exec("echo hello | upper").await.unwrap();
assert_eq!(result.stdout, "HELLO\n");
}
#[tokio::test]
async fn test_custom_builtin_pipeline_chain() {
fn to_upper(s: &str) -> String {
s.to_uppercase()
}
fn reverse(s: &str) -> String {
s.chars().rev().collect()
}
let mut bash = Bash::builder()
.builtin(
"upper",
Box::new(Transform {
transform_fn: to_upper,
}),
)
.builtin(
"reverse",
Box::new(Transform {
transform_fn: reverse,
}),
)
.build();
let result = bash.exec("echo abc | upper | reverse").await.unwrap();
assert_eq!(result.stdout, "\nCBA");
}
#[tokio::test]
async fn test_custom_builtin_filesystem_access() {
let fs = Arc::new(InMemoryFs::new());
fs.mkdir(std::path::Path::new("/data"), false)
.await
.unwrap();
fs.write_file(
std::path::Path::new("/data/test.txt"),
b"custom content here",
)
.await
.unwrap();
let mut bash = Bash::builder()
.fs(fs)
.builtin("readfile", Box::new(FileReader))
.build();
let result = bash.exec("readfile /data/test.txt").await.unwrap();
assert_eq!(result.stdout, "custom content here");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn test_custom_builtin_filesystem_error() {
let mut bash = Bash::builder()
.builtin("readfile", Box::new(FileReader))
.build();
let result = bash.exec("readfile /nonexistent").await.unwrap();
assert!(result.stderr.contains("Error:"));
assert_eq!(result.exit_code, 1);
}
#[tokio::test]
async fn test_custom_builtin_shared_state() {
let counter = Arc::new(AtomicU64::new(0));
let mut bash = Bash::builder()
.builtin(
"counter",
Box::new(Counter {
count: counter.clone(),
}),
)
.build();
let result = bash.exec("counter").await.unwrap();
assert_eq!(result.stdout, "1\n");
let result = bash.exec("counter").await.unwrap();
assert_eq!(result.stdout, "2\n");
let result = bash.exec("counter").await.unwrap();
assert_eq!(result.stdout, "3\n");
assert_eq!(counter.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn test_custom_builtin_returns_error() {
let mut bash = Bash::builder()
.builtin(
"fail",
Box::new(Fail {
message: "Something went wrong".to_string(),
code: 42,
}),
)
.build();
let result = bash.exec("fail").await.unwrap();
assert_eq!(result.stderr, "Something went wrong\n");
assert_eq!(result.exit_code, 42);
}
#[tokio::test]
async fn test_custom_builtin_error_in_conditional() {
let mut bash = Bash::builder()
.builtin(
"fail",
Box::new(Fail {
message: "error".to_string(),
code: 1,
}),
)
.builtin(
"prefix",
Box::new(PrefixEcho {
prefix: "".to_string(),
}),
)
.build();
let result = bash.exec("fail || prefix success").await.unwrap();
assert_eq!(result.stdout, "success\n");
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn test_custom_builtin_override_echo() {
let mut bash = Bash::builder()
.builtin(
"echo",
Box::new(PrefixEcho {
prefix: "[CUSTOM] ".to_string(),
}),
)
.build();
let result = bash.exec("echo hello").await.unwrap();
assert_eq!(result.stdout, "[CUSTOM] hello\n");
}
#[tokio::test]
async fn test_custom_builtin_environment_access() {
let mut bash = Bash::builder()
.env("FOO", "bar")
.env("BAZ", "qux")
.builtin("dumpenv", Box::new(EnvDumper))
.build();
let result = bash.exec("dumpenv").await.unwrap();
assert!(result.stdout.contains("FOO=bar"));
assert!(result.stdout.contains("BAZ=qux"));
}
#[tokio::test]
async fn test_custom_builtin_in_for_loop() {
let mut bash = Bash::builder()
.builtin(
"prefix",
Box::new(PrefixEcho {
prefix: "- ".to_string(),
}),
)
.build();
let script = r#"
for item in a b c; do
prefix $item
done
"#;
let result = bash.exec(script).await.unwrap();
assert_eq!(result.stdout, "- a\n- b\n- c\n");
}
#[tokio::test]
async fn test_custom_builtin_in_if_condition() {
let mut bash = Bash::builder()
.builtin(
"fail",
Box::new(Fail {
message: "".to_string(),
code: 1,
}),
)
.build();
let script = r#"
if fail; then
echo "should not reach"
else
echo "correctly handled"
fi
"#;
let result = bash.exec(script).await.unwrap();
assert_eq!(result.stdout, "correctly handled\n");
}
#[tokio::test]
async fn test_custom_builtin_with_variable_expansion() {
let mut bash = Bash::builder()
.builtin(
"prefix",
Box::new(PrefixEcho {
prefix: "".to_string(),
}),
)
.build();
let result = bash.exec("NAME=Alice; prefix Hello $NAME").await.unwrap();
assert_eq!(result.stdout, "Hello Alice\n");
}
#[tokio::test]
async fn test_multiple_custom_builtins() {
fn to_upper(s: &str) -> String {
s.to_uppercase()
}
let counter = Arc::new(AtomicU64::new(0));
let mut bash = Bash::builder()
.builtin(
"prefix",
Box::new(PrefixEcho {
prefix: "[LOG] ".to_string(),
}),
)
.builtin(
"upper",
Box::new(Transform {
transform_fn: to_upper,
}),
)
.builtin("counter", Box::new(Counter { count: counter }))
.builtin(
"fail",
Box::new(Fail {
message: "error".to_string(),
code: 1,
}),
)
.build();
let result = bash.exec("prefix test").await.unwrap();
assert_eq!(result.stdout, "[LOG] test\n");
let result = bash.exec("echo hello | upper").await.unwrap();
assert_eq!(result.stdout, "HELLO\n");
let result = bash.exec("counter").await.unwrap();
assert_eq!(result.stdout, "1\n");
let result = bash.exec("fail").await.unwrap();
assert_eq!(result.exit_code, 1);
}
#[tokio::test]
async fn test_custom_builtin_empty_name() {
struct Empty;
#[async_trait]
impl Builtin for Empty {
async fn execute(&self, _ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
Ok(ExecResult::ok("empty\n".to_string()))
}
}
let mut bash = Bash::builder().builtin("_", Box::new(Empty)).build();
let result = bash.exec("_").await.unwrap();
assert_eq!(result.stdout, "empty\n");
}
#[tokio::test]
async fn test_custom_builtin_special_characters_in_output() {
struct SpecialOutput;
#[async_trait]
impl Builtin for SpecialOutput {
async fn execute(&self, _ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
Ok(ExecResult::ok("line1\nline2\ttab\n".to_string()))
}
}
let mut bash = Bash::builder()
.builtin("special", Box::new(SpecialOutput))
.build();
let result = bash.exec("special").await.unwrap();
assert_eq!(result.stdout, "line1\nline2\ttab\n");
}