tur/fs
extended file-system operations (stat, readdir, fnmatch, walk).
Higher-level wrappers around sys/stat.h, dirent.h, and fnmatch.h.
Since: Phase B1
StatInfo
(defopaque StatInfo :int)
TmpFile
(defopaque TmpFile :int :linear)
fs/stat
(fs/stat [path :cstr])
return file metadata for a path.
| path | NUL-terminated path cstr |
(Ok info) -- a StatInfo; inspect it with fs/stat-size, fs/stat-mtime, fs/stat-atime, fs/stat-mode, fs/stat-perms and fs/stat-kind, and release it with fs/stat-free -- or (Err e) when the path is inaccessible. A symlink is followed; fs/lstat describes the link itself.
(match (fs/stat "/etc/hosts") (Ok s) (do (println (fs/stat-size s)) (fs/stat-free s)) (Err e) (println (io-error/message e)))
Since: Phase B2
fs/stat-free
(fs/stat-free [s :StatInfo] :void)
free the stat result returned by fs/stat.
| s | a StatInfo from a successful fs/stat |
(fs/stat-free s)
Since: Phase B2
fs/stat-size
(fs/stat-size [s :StatInfo] :int)
extract file size from a stat result.
| s | stat pointer from fs/stat |
File size in bytes as :int.
(fs/stat-size info) ; => 221
Since: Phase B2
fs/stat-mtime
(fs/stat-mtime [s :StatInfo] :int)
extract modification time from a stat result.
| s | stat pointer from fs/stat |
Modification time as a Unix timestamp (:int).
(fs/stat-mtime info) ; => 1747123456
Since: Phase B2
fs/stat-mode
(fs/stat-mode [s :StatInfo] :int)
extract file mode bits from a stat result.
| s | stat pointer from fs/stat |
File mode bits as :int (same as st_mode from stat(2)).
(fs/stat-mode info) ; => 33188
Since: Phase B2
fs/stat-atime
(fs/stat-atime [s :StatInfo] :int)
extract the last-access time from a stat result.
| s | a StatInfo from fs/stat or fs/lstat |
Last access time as a Unix timestamp in seconds (:int). Many systems mount with relatime or noatime, so it can lag behind the last read.
(fs/stat-atime info) ; => 1747123456
Since: stdlib-os-surface P3
Mode
(defopaque Mode :int)
the permission bits of a file (rwx for user / group / other, plus setuid, setgid and sticky).
A Mode, not a raw octal int, so fs/chmod's signature says what it takes and a byte count cannot be passed for one. Build one with mode/parse ("rw-r--r--"), adjust it with mode/with / mode/without, read it with mode/has? or mode/format. mode/of-bits and mode/bits convert to and from the st_mode integer for interop.
Since: stdlib-os-surface P3
PermClass
(defdata PermClass :copy (PermUser) (PermGroup) (PermOther))
whose permission a Mode bit grants: the owning user, the owning group, or everyone else.
Since: stdlib-os-surface P3
Perm
(defdata Perm :copy (PermRead) (PermWrite) (PermExec))
one permission: read, write or execute (search, on a directory).
Since: stdlib-os-surface P3
mode/of-bits
(mode/of-bits [bits :int] :Mode)
a Mode from st_mode-style permission bits.
| bits | permission bits; everything above the low 12 (the file type in a | |
| full st_mode) is dropped |
The Mode.
(mode/format (mode/of-bits 420)) ; => "rw-r--r--" (420 is octal 644)
Since: stdlib-os-surface P3
mode/bits
(mode/bits [m :Mode] :int)
the permission bits of a Mode, as an int (octal 7777 at most).
| m | a Mode |
The bits, for interop with C APIs that take a mode_t.
(mode/bits (mode/of-bits 493)) ; => 493 (octal 755)
Since: stdlib-os-surface P3
mode/bit
(mode/bit [who :PermClass p :Perm] :int)
the single st_mode bit for who + perm (internal).
mode/has?
(mode/has? [m :Mode who :PermClass p :Perm] :bool)
does a Mode grant a permission to a class?
| m | a Mode | |
| who | PermUser, PermGroup or PermOther | |
| p | PermRead, PermWrite or PermExec |
true when the bit is set.
(mode/has? (mode/of-bits 420) (PermUser) (PermWrite)) ; => true
Since: stdlib-os-surface P3
mode/with
(mode/with [m :Mode who :PermClass p :Perm] :Mode)
a Mode with one more permission granted (chmod who+p).
| m | the starting Mode | |
| who | PermUser, PermGroup or PermOther | |
| p | PermRead, PermWrite or PermExec |
m with the bit set.
(mode/format (mode/with (mode/of-bits 420) (PermUser) (PermExec))) ; => "rwxr--r--"
Since: stdlib-os-surface P3
mode/without
(mode/without [m :Mode who :PermClass p :Perm] :Mode)
a Mode with one permission removed (chmod who-p).
| m | the starting Mode | |
| who | PermUser, PermGroup or PermOther | |
| p | PermRead, PermWrite or PermExec |
m with the bit cleared.
(mode/format (mode/without (mode/of-bits 420) (PermOther) (PermRead))) ; => "rw-r-----"
Since: stdlib-os-surface P3
mode/parse
(mode/parse [s :cstr] :(Option Mode))
a Mode from its ls(1) spelling, e.g. "rwxr-xr-x".
Nine characters, rwx for user, group and other, '-' for an unset bit. The execute slots also take ls's s / S (setuid, setgid: with and without execute) and t / T (sticky).
| s | the nine-character permission string |
(Some mode), or None when s is not exactly that shape.
(mode/parse "rw-r--r--") ; => (Some mode) with bits 420 (octal 644)
Since: stdlib-os-surface P3
mode/format
(mode/format [m :Mode] :cstr)
the ls(1) spelling of a Mode, e.g. "rwxr-xr-x".
| m | a Mode |
A fresh nine-character heap cstr the caller owns (release with free). Setuid / setgid / sticky show as s / S / t / T, as ls prints them.
(mode/format (mode/of-bits 493)) ; => "rwxr-xr-x"
Since: stdlib-os-surface P3
fs/stat-perms
(fs/stat-perms [s :StatInfo] :Mode)
the permission bits of a stat result, as a Mode.
| s | a StatInfo from fs/stat or fs/lstat |
The Mode (the file-type bits of st_mode are dropped).
(mode/format (fs/stat-perms info)) ; => "rw-r--r--"
Since: stdlib-os-surface P3
FileKind
(defdata FileKind :copy (KindFile) (KindDir) (KindSymlink) (KindOther))
what a stat result describes.
KindSymlink only ever comes from fs/lstat: fs/stat follows the link. KindOther covers devices, FIFOs and sockets.
Since: stdlib-os-surface P3
fs/stat-kind-code
(fs/stat-kind-code [s :StatInfo] :int)
0 file, 1 dir, 2 symlink, 3 other (internal).
fs/stat-kind
(fs/stat-kind [s :StatInfo] :FileKind)
whether a stat result is a file, directory, symlink or other.
| s | a StatInfo from fs/stat or fs/lstat |
KindFile, KindDir, KindSymlink (from fs/lstat only) or KindOther.
(match (fs/stat-kind info) (KindDir) (println "dir") _ (println "not a dir"))
Since: stdlib-os-surface P3
fs/exists?
(fs/exists? [path :cstr])
test whether a path exists.
| path | NUL-terminated path cstr |
true if stat(2) succeeds on the path, false otherwise.
(fs/exists? "/etc/hosts") ; => true
Since: Phase B2
fs/file?
(fs/file? [path :cstr])
test whether a path is a regular file.
| path | NUL-terminated path cstr |
true if path is a regular file.
(fs/file? "/etc/hosts") ; => true
Since: Phase B2
fs/dir?
(fs/dir? [path :cstr])
test whether a path is a directory.
| path | NUL-terminated path cstr |
true if path is a directory.
(fs/dir? "/tmp") ; => true
Since: Phase B2
fs/mkdir
(fs/mkdir [path :cstr])
create a directory with mode 0755.
| path | NUL-terminated path cstr |
(Ok nil) on success, or (Err e) carrying the errno.
(fs/mkdir "/tmp/mydir") ; => (Ok nil)
Since: Phase B2
fs/mkdirp
(fs/mkdirp [path :cstr])
create a directory and all missing parent directories.
| path | NUL-terminated path cstr |
(Ok nil) on success (including when the directory already exists), or (Err e) for the first mkdir that failed for any other reason.
(fs/mkdirp "/tmp/a/b/c") ; => (Ok nil)
Since: Phase B2
fs/rmdir
(fs/rmdir [path :cstr])
remove an empty directory.
| path | NUL-terminated path cstr |
(Ok nil) on success, or (Err e) carrying the errno.
(fs/rmdir "/tmp/mydir") ; => (Ok nil)
Since: Phase B2
fs/rm
(fs/rm [path :cstr])
remove a regular file.
| path | NUL-terminated path cstr |
(Ok nil) on success, or (Err e) carrying the errno.
(fs/rm "/tmp/tmpfile.txt") ; => (Ok nil)
Since: Phase B2
fs/rename
(fs/rename [src :cstr dst :cstr])
rename or move a file.
| src | source path cstr | |
| dst | destination path cstr |
(Ok nil) on success, or (Err e) carrying the errno.
(fs/rename "/tmp/a.txt" "/tmp/b.txt") ; => (Ok nil)
Since: Phase B2
fs/copy
(fs/copy [src :cstr dst :cstr])
copy a file from src to dst using a read/write loop.
| src | source path cstr | |
| dst | destination path cstr |
(Ok nil) on success, or (Err e) carrying the errno.
(fs/copy "/etc/hosts" "/tmp/hosts.bak") ; => (Ok nil)
Since: Phase B2
fs/glob
(fs/glob [dir :cstr pattern :cstr])
list directory entries matching a fnmatch(3) pattern.
| dir | directory to search (e.g., "/tmp") | |
| pattern | fnmatch pattern applied to entry names (e.g., "*.txt") |
(Ok paths) -- a (Vec cstr) of fresh heap-allocated full paths, in directory order; release it with fs/paths-free -- or (Err e) when the directory cannot be opened.
(match (fs/glob "/tmp" "*.txt") (Ok v) (do (println (vec-len v)) (fs/paths-free v)) (Err e) (println (io-error/message e)))
Since: Phase B2
fs/paths-free
(fs/paths-free [v :(Vec cstr)] :void)
free a (Vec cstr) of owned strings and the Vec itself.
Releases what fs/glob, fs/walk and fs/read-lines hand back (and env/all): every element string, then the vector. Do not use it on a Vec whose strings you did not get from one of those.
| v | a (Vec cstr) whose element strings are heap-owned |
(fs/paths-free paths)
Since: stdlib-os-surface P1
fs/tmpfile
(fs/tmpfile)
create a unique temporary file in the system temp directory.
A linear TmpFile handle wrapping a pair { path-cstr, fd }. The path is heap-allocated; the fd is the open descriptor. Release exactly once with fs/tmpfile-free. Returns 0 on failure. The directory is resolved at run time: TMPDIR (else /tmp) on POSIX, GetTempPath on Windows -- never a hardcoded "/tmp", which a native Windows binary would resolve against the current drive root. Under -Xlinear the checker requires the handle be consumed exactly once (dropping it is TUR-E0100; freeing it twice is TUR-E0101).
(let [t (fs/tmpfile)] (fs/tmpfile-free t))
Since: Phase B2
fs/tmpfile-path
(fs/tmpfile-path [^borrow t :TmpFile] :cstr)
extract the path from an fs/tmpfile pair.
| t | tmpfile pair from fs/tmpfile |
The path cstr (owned by the pair; do not free separately).
(fs/tmpfile-path my-tmp) ; => "/tmp/tur_aB3xYz"
Since: Phase B2
fs/tmpfile-fd
(fs/tmpfile-fd [^borrow t :TmpFile] :Fd)
extract the file descriptor from an fs/tmpfile pair.
| t | tmpfile pair from fs/tmpfile |
The open file descriptor as :int.
(fs/tmpfile-fd my-tmp) ; => 5
Since: Phase B2
fs/tmpfile-free
(fs/tmpfile-free [t :TmpFile])
close the fd and free the tmpfile pair.
| t | tmpfile pair from fs/tmpfile (may be 0) |
(fs/tmpfile-free my-tmp)
Since: Phase B2
fs/read-text
(fs/read-text [path :cstr])
read an entire file into a NUL-terminated heap buffer.
| path | NUL-terminated path cstr |
(Ok text) -- a fresh heap-allocated NUL-terminated cstr of the file contents, which the caller owns (release with free) -- or (Err e).
(match (fs/read-text "/etc/hostname") (Ok s) (println s) (Err e) (println (io-error/message e)))
Since: Phase B2
fs/write-text
(fs/write-text [path :cstr content :cstr])
write a NUL-terminated string to a file.
| path | NUL-terminated destination path cstr | |
| content | NUL-terminated string to write |
(Ok nil) on success, or (Err e) carrying the errno.
(fs/write-text "/tmp/out.txt" "hello\n") ; => (Ok nil)
Since: Phase B2
fs/append-text
(fs/append-text [path :cstr content :cstr])
append a NUL-terminated string to a file.
Creates the file when it does not exist; never truncates it.
| path | NUL-terminated destination path cstr | |
| content | NUL-terminated string to append |
(Ok nil) on success, or (Err e) carrying the errno.
(fs/append-text "/tmp/log.txt" "one more line\n") ; => (Ok nil)
Since: stdlib-os-surface P0
fs/read-lines
(fs/read-lines [path :cstr])
read a whole file as a vector of lines.
Each line has its newline (and a CR before it) stripped. A final line without a trailing newline counts; a trailing newline does not add an empty last line. For files too large to hold at once, open a FileHandle and call file-read-line in a loop.
| path | NUL-terminated path cstr |
(Ok lines) -- a (Vec cstr) of fresh heap strings; release it with fs/paths-free -- or (Err e).
(match (fs/read-lines "/etc/hosts") (Ok v) (do (println (vec-len v)) (fs/paths-free v)) (Err e) (println (io-error/message e)))
Since: stdlib-os-surface P2
fs/read-dir
(fs/read-dir [dir :cstr])
the names in a directory, sorted.
Lists every entry except "." and "..", sorted by byte order so the result is stable across runs and platforms. Names only -- join them onto dir with fs/path-join for a usable path.
| dir | the directory to list |
(Ok names) -- a (Vec cstr) of fresh heap strings; release it with fs/paths-free -- or (Err e) when the directory cannot be opened.
(match (fs/read-dir "/tmp") (Ok v) (do (println (vec-len v)) (fs/paths-free v)) (Err e) (println (io-error/message e)))
Since: stdlib-os-surface P2
fs/path-join
(fs/path-join [dir :cstr name :cstr] :cstr)
join a directory and a name with one separator.
| dir | the directory part (a trailing '/' is not doubled) | |
| name | the entry name |
A fresh heap cstr "dir/name" the caller owns (release with free).
(fs/path-join "/tmp" "a.txt") ; => "/tmp/a.txt"
Since: stdlib-os-surface P2
fs/free-cstr
(fs/free-cstr [s :cstr] :void)
free a heap cstr (internal).
fs/dup-cstr
(fs/dup-cstr [s :cstr] :cstr)
a fresh heap copy of a cstr (internal).
fs/walk-dir?
(fs/walk-dir? [path :cstr])
is path a directory the walk descends into (internal).
fs/walk-names
(fs/walk-names [dir :cstr names :(Vec cstr) i :int f :(fn [cstr bool] nil)])
visit names[i..] of dir, recursing into directories (internal).
fs/walk-fn
(fs/walk-fn [root :cstr f :(fn [cstr bool] nil)])
call f on every path under root, depth first.
Visits entries in fs/read-dir order (sorted), calling (f path is-dir?) for each before descending into it, so nothing is materialized beyond one directory's names at a time. root itself is not visited. Symlinks are not followed on POSIX (no cycles). The path handed to f is freed when f returns -- copy it to keep it.
| root | the directory to walk | |
| f | (fn [path is-dir] nil), called once per entry |
(Ok nil) once the whole tree was visited, or (Err e) for the first directory that could not be read (the walk stops there).
(fs/walk-fn "/tmp/proj" (fn [p : cstr d : bool] : nil (println p)))
Since: stdlib-os-surface P2
fs/walk
(fs/walk [root :cstr])
every path under root, depth first, as a vector.
The convenience form of fs/walk-fn: the same order (sorted per directory, each directory before its contents), root itself excluded, symlinks not followed on POSIX.
| root | the directory to walk |
(Ok paths) -- a (Vec cstr) of fresh heap paths, each starting with root; release it with fs/paths-free -- or (Err e) for the first directory that could not be read.
(match (fs/walk "/tmp/proj") (Ok v) (do (println (vec-len v)) (fs/paths-free v)) (Err e) (println (io-error/message e)))
Since: stdlib-os-surface P2
fs/lstat
(fs/lstat [path :cstr])
file metadata for a path, without following a final symlink.
The same StatInfo as fs/stat, except that when path is a symlink it describes the link itself: fs/stat-kind answers KindSymlink and fs/stat-size is the length of the target path. Windows has no lstat; there this is fs/stat (use fs/symlink? to ask about a link).
| path | NUL-terminated path cstr |
(Ok info) -- release it with fs/stat-free -- or (Err e).
(match (fs/lstat "/tmp/link") (Ok s) (do (println (fs/stat-size s)) (fs/stat-free s)) (Err e) (println (io-error/message e)))
Since: stdlib-os-surface P3
fs/symlink?
(fs/symlink? [path :cstr])
test whether a path is a symbolic link.
Does not follow the link, so a dangling link is still a symlink.
| path | NUL-terminated path cstr |
true if path is a symlink (on Windows: a reparse point -- a symlink or a junction), false otherwise, including when path does not exist.
(fs/symlink? "/tmp/link") ; => true
Since: stdlib-os-surface P3
fs/readlink
(fs/readlink [path :cstr])
the target a symbolic link points at.
One level only, exactly as stored in the link (a relative target stays relative); fs/realpath resolves the whole chain.
| path | the symlink |
(Ok target) -- a fresh heap cstr the caller owns (release with free) -- or (Err e): EINVAL when path is not a symlink. ENOSYS on Windows.
(match (fs/readlink "/tmp/link") (Ok t) (println t) (Err e) (println (io-error/message e)))
Since: stdlib-os-surface P3
fs/symlink
(fs/symlink [target :cstr link-path :cstr])
create a symbolic link at link-path pointing at target.
target is stored as given and need not exist; a relative target is resolved against the link's directory when the link is followed.
| target | what the link points at | |
| link-path | where to create the link |
(Ok nil), or (Err e) -- EEXIST when link-path is taken. On Windows a symlink needs Developer Mode or administrator rights; without them this is an EPERM error rather than a copy or a junction.
(fs/symlink "data.txt" "/tmp/proj/latest") ; => (Ok nil)
Since: stdlib-os-surface P3
fs/chmod
(fs/chmod [path :cstr mode :Mode])
set a file's permission bits.
| path | the file or directory (a symlink is followed) | |
| mode | the new permissions, e.g. (mode/with (fs/stat-perms s) (PermUser) (PermExec)) |
(Ok nil), or (Err e) -- EPERM when the caller does not own the file. Windows keeps only one bit: without PermWrite for PermUser the file becomes read-only; the other bits are ignored there.
(match (mode/parse "rwxr-xr-x") (Some m) (fs/chmod "/tmp/run.sh" m) (None) (Err (io-error/of-code 22)))
Since: stdlib-os-surface P3
fs/realpath
(fs/realpath [path :cstr])
the absolute, canonical form of a path.
Resolves ".", ".." and every symlink on the way; the path must exist.
| path | a relative or absolute path |
(Ok abs) -- a fresh heap cstr the caller owns (release with free) -- or (Err e), e.g. not-found when a component is missing. On Windows this is GetFullPathName: "." and ".." are resolved, symlinks are not.
(match (fs/realpath ".") (Ok p) (println p) (Err e) (println (io-error/message e)))
Since: stdlib-os-surface P3
fs/file-dev
(fs/file-dev [path :cstr])
the volume a path lives on, for cycle checks (internal).
fs/file-ino
(fs/file-ino [path :cstr])
the file's number on its volume, for cycle checks (internal).
fs/walk-on-path?
(fs/walk-on-path? [anc :(Vec int) dev :int ino :int i :int] :bool)
is (dev, ino) one of the directories being walked (internal).
fs/walk-follow-names
(fs/walk-follow-names [dir :cstr names :(Vec cstr) i :int f :(fn [cstr bool] nil) anc :(Vec int)])
fs/walk-names, following symlinks (internal).
fs/walk-fn-follow
(fs/walk-fn-follow [root :cstr f :(fn [cstr bool] nil)])
fs/walk-fn that descends into symlinked directories.
The same visit order and callback as fs/walk-fn, but a symlink to a directory is reported with is-dir? true and walked into. A link that leads back to a directory already on the path from root (a cycle) is reported once and not entered, so the walk always ends; the same tree reached through two different links is visited twice, as find -L does.
| root | the directory to walk | |
| f | (fn [path is-dir] nil), called once per entry |
(Ok nil), or (Err e) for the first directory that could not be read.
(fs/walk-fn-follow "/tmp/proj" (fn [p : cstr d : bool] : nil (println p)))
Since: stdlib-os-surface P3
fs/walk-follow
(fs/walk-follow [root :cstr])
fs/walk that descends into symlinked directories.
The vector form of fs/walk-fn-follow (same order and cycle rule).
| root | the directory to walk |
(Ok paths) -- a (Vec cstr) of fresh heap paths; release it with fs/paths-free -- or (Err e).
(match (fs/walk-follow "/tmp/proj") (Ok v) (do (println (vec-len v)) (fs/paths-free v)) (Err e) (println (io-error/message e)))
Since: stdlib-os-surface P3
fs/touch
(fs/touch [path :cstr])
create a file if missing, else set its access and modification times to now.
Never truncates. Works on a directory too (times only).
| path | the file to touch |
(Ok nil), or (Err e) -- not-found when the parent directory is missing.
(fs/touch "/tmp/stamp") ; => (Ok nil)
Since: stdlib-os-surface P3