///|
priv suberror MktempError {
MktempError(String)
}
///|
priv struct Template {
prefix : String
width : Int32
suffix : String
}
///|
fn parse_template(
template : String,
suffix : String?,
) -> Template raise MktempError {
let end = if suffix is Some(_) {
if !template.has_suffix("X") {
raise MktempError("with --suffix, template '\{template}' must end in X")
}
template.length()
} else {
let mut last = template.length()
while last > 0 && template[last - 1] != 'X' {
last -= 1
}
if last == 0 {
template.length()
} else {
last
}
}
let tail = suffix.unwrap_or(template[end:].to_owned())
if tail.contains("/") || (@platform.is_windows() && tail.contains("\\")) {
raise MktempError("invalid suffix '\{tail}', contains directory separator")
}
let mut first = end
while first > 0 && template[first - 1] == 'X' {
first -= 1
}
if end - first < 3 {
raise MktempError("too few X's in template '\{template}'")
}
{ prefix: template[:first].to_owned(), width: end - first, suffix: tail, }
}
///|
fn random_letters(length : Int32) -> String raise MktempError {
let alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let output = StringBuilder()
let mut count = 0
while count < length {
guard @env.rand(length - count) is Some(bytes) else {
raise MktempError("secure random source is unavailable")
}
for byte in bytes {
if byte.to_int() < 248 {
output.write_char(alphabet[byte.to_int() % 62].unsafe_to_char())
count += 1
}
}
}
output.to_string()
}
///|
fn Template::fill(self : Template) -> String raise MktempError {
self.prefix + random_letters(self.width) + self.suffix
}
///|
fn mktemp_args(args : ArrayView[String]) -> Array[String] {
let result : Array[String] = []
let mut literal = false
let mut value = false
for arg in args {
if literal || value {
result.push(arg)
value = false
continue
}
if arg == "--" {
literal = true
}
result.push(if arg == "--tmpdir" { "--tmpdir=" } else { arg })
value = arg == "--suffix" ||
arg == "-p" ||
(
arg.has_prefix("-") &&
!arg.has_prefix("--") &&
arg.find("p") == Some(arg.length() - 1)
)
}
result
}
///|
fn creation_error(error : Error) -> String {
match error {
MktempError(message) => message
@os_error.OSError(_) as error if error.is_ENOENT() =>
"No such file or directory"
@os_error.OSError(_) as error if error.is_ENOTDIR() => "Not a directory"
@os_error.OSError(_) as error if error.is_EACCES() => "Permission denied"
_ => error.to_string()
}
}
///|
async fn create_unique(template : Template, directory : Bool) -> String {
for _ in 0..<238328 {
let candidate = template.fill()
try {
if directory {
@fs.mkdir(candidate, permission=0o700)
} else {
let file = @fs.open(
candidate,
mode=WriteOnly,
create_mode=CreateNew,
permission=0o600,
)
file.close()
}
} catch {
@os_error.OSError(_) as error if error.is_EEXIST() => continue
error => raise error
}
return candidate
}
raise MktempError("File exists")
}
///|
async fn main {
let parsed = @cli.parse(mktemp_args(@env.args()[1:]), [
@cli.flag("directory", short='d'),
@cli.flag("quiet", short='q'),
@cli.option("tmpdir", short='p'),
@cli.flag("tmpdir", short='t'),
@cli.option("suffix"),
@cli.flag("help"),
@cli.flag("version"),
]) catch {
@cli.CliError(kind=MissingOptionValue, option~, ..) => {
let description = if option == "tmpdir" {
"option requires an argument -- 'p'"
} else {
"option '--suffix' requires an argument"
}
@stdio.stderr.write(
"mktemp: \{description}\nTry 'mktemp --help' for more information.\n",
)
@sys.exit(1)
return
}
@cli.CliError(option~, message~, ..) => {
@stdio.stderr.write("mktemp: \{message}: '\{option}'\n")
@sys.exit(1)
return
}
}
if parsed.contains("help") {
@stdio.stdout.write(
"Usage: mktemp [OPTION]... [TEMPLATE]\n" +
"Create an exclusive temporary file (0600) or directory (0700).\n" +
" -d, --directory create a directory\n" +
" -p DIR, --tmpdir[=DIR] use DIR, then TMPDIR, then /tmp\n" +
" -t prefer TMPDIR; reject template directories\n" +
" --suffix=SUFFIX append SUFFIX to a template ending in X\n" +
" -q, --quiet suppress creation and output errors\n" +
" --help display this help and exit\n" +
" --version display version and exit\n" +
"Default template: tmp.XXXXXXXXXX. The final X run requires at least three Xs.\n",
)
return
}
if parsed.contains("version") {
@stdio.stdout.write("mktemp 0.1.3\n")
return
}
if parsed.operands.length() > 1 {
@stdio.stderr.write(
"mktemp: too many templates\nTry 'mktemp --help' for more information.\n",
)
@sys.exit(1)
return
}
let template = parsed.operands.get(0).unwrap_or("tmp.XXXXXXXXXX")
let spec = parse_template(template, parsed.last_value("suffix")) catch {
MktempError(message) => {
@stdio.stderr.write("mktemp: \{message}\n")
@sys.exit(1)
return
}
}
let destination = parsed.contains("tmpdir") || parsed.operands.is_empty()
// -t shares the canonical destination option but has no value occurrence.
let legacy = parsed.occurrences
.get("tmpdir")
.unwrap_or([])
.any(value => value is None)
let with_suffix = spec.prefix + "X".repeat(spec.width) + spec.suffix
if legacy &&
(
with_suffix.contains("/") ||
(@platform.is_windows() && with_suffix.contains("\\"))
) {
@stdio.stderr.write(
"mktemp: invalid template, '\{with_suffix}', contains directory separator\n",
)
@sys.exit(1)
return
}
if destination && !legacy && @path.Path(template).is_absolute() {
@stdio.stderr.write(
"mktemp: invalid template, '\{with_suffix}'; with --tmpdir, it may not be absolute\n",
)
@sys.exit(1)
return
}
let environment = @env.get_env_var("TMPDIR").filter(value => !value.is_empty())
let directory_arg = parsed
.last_value("tmpdir")
.filter(value => !value.is_empty())
let directory = if legacy {
if environment is Some(_) {
environment
} else {
directory_arg
}
} else if directory_arg is Some(_) {
directory_arg
} else {
environment
}
let directory = directory.unwrap_or("/tmp")
let spec = if destination {
{
..spec,
prefix: directory +
(if directory.has_suffix("/") || directory.has_suffix("\\") {
""
} else {
"/"
}) +
spec.prefix,
}
} else {
spec
}
let display_template = spec.prefix + "X".repeat(spec.width) + spec.suffix
let make_directory = parsed.contains("directory")
let quiet = parsed.contains("quiet")
let result = create_unique(spec, make_directory) catch {
error => {
if !quiet {
let kind = if make_directory { "directory" } else { "file" }
@stdio.stderr.write(
"mktemp: failed to create \{kind} via template '\{display_template}': \{creation_error(error)}\n",
)
}
@sys.exit(1)
return
}
}
@stdio.stdout.write(result + "\n") catch {
error => {
if make_directory {
@fs.rmdir(result) catch {
_ => ()
}
} else {
@fs.remove(result) catch {
_ => ()
}
}
if !quiet {
@stdio.stderr.write("mktemp: write error: \{creation_error(error)}\n")
}
@sys.exit(1)
}
}
}