Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions crates/bashkit/src/builtins/fileops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExecResult> {
let mut create_dir = false;
let mut prefix_dir = "/tmp".to_string();
let mut template: Option<String> = 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 {
Expand Down
2 changes: 1 addition & 1 deletion crates/bashkit/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
1 change: 1 addition & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
65 changes: 65 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/fileops.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 4 additions & 4 deletions specs/009-implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 |
Expand Down
Loading