From f48cc24ec7e05b59a67cc73ff86659c58194f5dd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 04:41:10 +0000 Subject: [PATCH] feat(builtins): implement mktemp builtin for temporary file/directory creation Supports -d (directory), -p DIR (prefix), -t (tmpdir), and TEMPLATE with XXXXXX placeholder. Creates files/directories in the virtual filesystem. https://claude.ai/code/session_012rzB3FRw7yoQWCG1mxyW7J --- crates/bashkit/src/builtins/fileops.rs | 88 +++++++++++++++++++ crates/bashkit/src/builtins/mod.rs | 2 +- crates/bashkit/src/interpreter/mod.rs | 1 + .../tests/spec_cases/bash/fileops.test.sh | 65 ++++++++++++++ specs/009-implementation-status.md | 8 +- 5 files changed, 159 insertions(+), 5 deletions(-) diff --git a/crates/bashkit/src/builtins/fileops.rs b/crates/bashkit/src/builtins/fileops.rs index 1df99ef9..511323da 100644 --- a/crates/bashkit/src/builtins/fileops.rs +++ b/crates/bashkit/src/builtins/fileops.rs @@ -642,6 +642,94 @@ impl Builtin for Kill { } } +/// The mktemp builtin - create temporary files or directories. +/// +/// Usage: mktemp [-d] [-p DIR] [-t] [TEMPLATE] +/// +/// Options: +/// -d Create a directory instead of a file +/// -p DIR Use DIR as prefix (default: /tmp) +/// -t Interpret TEMPLATE relative to a temp directory +pub struct Mktemp; + +#[async_trait] +impl Builtin for Mktemp { + async fn execute(&self, ctx: Context<'_>) -> Result { + let mut create_dir = false; + let mut prefix_dir = "/tmp".to_string(); + let mut template: Option = None; + let mut use_tmpdir = false; + + let mut i = 0; + while i < ctx.args.len() { + match ctx.args[i].as_str() { + "-d" => create_dir = true, + "-p" => { + i += 1; + if i < ctx.args.len() { + prefix_dir = ctx.args[i].clone(); + } + } + "-t" => use_tmpdir = true, + arg if !arg.starts_with('-') => { + template = Some(arg.to_string()); + } + _ => {} // ignore unknown flags + } + i += 1; + } + + // Generate random suffix + use std::collections::hash_map::RandomState; + use std::hash::{BuildHasher, Hasher}; + let random = RandomState::new().build_hasher().finish(); + let suffix = format!("{:010x}", random % 0xFF_FFFF_FFFF); + + // Build path + let name = if let Some(tmpl) = &template { + if tmpl.contains("XXXXXX") { + tmpl.replacen("XXXXXX", &suffix[..6], 1) + } else { + format!("{}.{}", tmpl, &suffix[..6]) + } + } else { + format!("tmp.{}", &suffix[..10]) + }; + + let path = if use_tmpdir || template.is_none() || !name.contains('/') { + format!("{}/{}", prefix_dir, name) + } else { + let p = resolve_path(ctx.cwd, &name); + p.to_string_lossy().to_string() + }; + + let full_path = std::path::PathBuf::from(&path); + + // Ensure parent directory exists + if let Some(parent) = full_path.parent() { + if !ctx.fs.exists(parent).await.unwrap_or(false) { + let _ = ctx.fs.mkdir(parent, true).await; + } + } + + if create_dir { + if let Err(e) = ctx.fs.mkdir(&full_path, true).await { + return Ok(ExecResult::err( + format!("mktemp: failed to create directory '{}': {}\n", path, e), + 1, + )); + } + } else if let Err(e) = ctx.fs.write_file(&full_path, &[]).await { + return Ok(ExecResult::err( + format!("mktemp: failed to create file '{}': {}\n", path, e), + 1, + )); + } + + Ok(ExecResult::ok(format!("{}\n", path))) + } +} + #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { diff --git a/crates/bashkit/src/builtins/mod.rs b/crates/bashkit/src/builtins/mod.rs index f03ddab8..2f98bde3 100644 --- a/crates/bashkit/src/builtins/mod.rs +++ b/crates/bashkit/src/builtins/mod.rs @@ -81,7 +81,7 @@ pub use disk::{Df, Du}; pub use echo::Echo; pub use environ::{Env, History, Printenv}; pub use export::Export; -pub use fileops::{Chmod, Chown, Cp, Kill, Ln, Mkdir, Mv, Rm, Touch}; +pub use fileops::{Chmod, Chown, Cp, Kill, Ln, Mkdir, Mktemp, Mv, Rm, Touch}; pub use flow::{Break, Colon, Continue, Exit, False, Return, True}; pub use grep::Grep; pub use headtail::{Head, Tail}; diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 6dd870fc..b97b687d 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -238,6 +238,7 @@ impl Interpreter { builtins.insert("basename".to_string(), Box::new(builtins::Basename)); builtins.insert("dirname".to_string(), Box::new(builtins::Dirname)); builtins.insert("mkdir".to_string(), Box::new(builtins::Mkdir)); + builtins.insert("mktemp".to_string(), Box::new(builtins::Mktemp)); builtins.insert("rm".to_string(), Box::new(builtins::Rm)); builtins.insert("cp".to_string(), Box::new(builtins::Cp)); builtins.insert("mv".to_string(), Box::new(builtins::Mv)); diff --git a/crates/bashkit/tests/spec_cases/bash/fileops.test.sh b/crates/bashkit/tests/spec_cases/bash/fileops.test.sh index 3c4ec6f7..8206ad54 100644 --- a/crates/bashkit/tests/spec_cases/bash/fileops.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/fileops.test.sh @@ -177,3 +177,68 @@ echo $? ### expect 0 ### end + +### mktemp_creates_file +# mktemp creates a temp file and prints its path +f=$(mktemp) +[ -f "$f" ] && echo "ok" +### expect +ok +### end + +### mktemp_in_tmp +# mktemp creates file under /tmp +f=$(mktemp) +echo "$f" | grep -q "^/tmp/" && echo "in_tmp" +### expect +in_tmp +### end + +### mktemp_directory +# mktemp -d creates a directory +d=$(mktemp -d) +[ -d "$d" ] && echo "ok" +### expect +ok +### end + +### mktemp_template +# mktemp with template replaces XXXXXX +f=$(mktemp /tmp/myapp.XXXXXX) +echo "$f" | grep -q "^/tmp/myapp\." && echo "matched" +[ -f "$f" ] && echo "exists" +### expect +matched +exists +### end + +### mktemp_dir_template +# mktemp -d with template +d=$(mktemp -d /tmp/mydir.XXXXXX) +echo "$d" | grep -q "^/tmp/mydir\." && echo "matched" +[ -d "$d" ] && echo "exists" +### expect +matched +exists +### end + +### mktemp_unique +# mktemp creates unique names +f1=$(mktemp) +f2=$(mktemp) +[ "$f1" != "$f2" ] && echo "unique" +### expect +unique +### end + +### mktemp_p_flag +# mktemp -p uses specified directory +### bash_diff +mkdir -p /tmp/custom +f=$(mktemp -p /tmp/custom) +echo "$f" | grep -q "^/tmp/custom/" && echo "in_custom" +[ -f "$f" ] && echo "exists" +### expect +in_custom +exists +### end diff --git a/specs/009-implementation-status.md b/specs/009-implementation-status.md index 2130b41a..51e19364 100644 --- a/specs/009-implementation-status.md +++ b/specs/009-implementation-status.md @@ -103,17 +103,17 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See ## Spec Test Coverage -**Total spec test cases:** 1329 (1324 pass, 5 skip) +**Total spec test cases:** 1336 (1331 pass, 5 skip) | Category | Cases | In CI | Pass | Skip | Notes | |----------|-------|-------|------|------|-------| -| Bash (core) | 911 | Yes | 906 | 5 | `bash_spec_tests` in CI | +| Bash (core) | 918 | Yes | 913 | 5 | `bash_spec_tests` in CI | | AWK | 96 | Yes | 96 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g | | Grep | 76 | Yes | 76 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect | | Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E | | JQ | 114 | Yes | 114 | 0 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env | | Python | 57 | Yes | 57 | 0 | embedded Python (Monty) | -| **Total** | **1329** | **Yes** | **1324** | **5** | | +| **Total** | **1336** | **Yes** | **1331** | **5** | | ### Bash Spec Tests Breakdown @@ -135,7 +135,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | diff.test.sh | 4 | line diffs | | echo.test.sh | 24 | escape sequences | | errexit.test.sh | 8 | set -e tests | -| fileops.test.sh | 21 | | +| fileops.test.sh | 28 | `mktemp`, `-d`, `-p`, template | | find.test.sh | 10 | file search | | functions.test.sh | 22 | local dynamic scoping, nested writes, FUNCNAME call stack | | getopts.test.sh | 9 | POSIX option parsing, combined flags, silent mode |