An open file, created by hs.fs.open() or hs.fs.tempFile().
HSFile is the equivalent of a Lua io file handle in Hammerspoon v1. It supports
reading and writing text (UTF-8) or binary data (Uint8Array), seeking, truncating,
advisory locking, and operating on the underlying path (rename, duplicate, remove).
All reads and writes go straight to the file descriptor with no user-space buffering,
so position is always exact and data written is immediately visible to other
processes. Use flush() if you also need it forced to stable storage.
Operations that fail return null or false and set lastError to an object of the
form {code: "ENOENT", message: "No such file or directory"}. lastError is not cleared
by successful calls, so only consult it after a call has reported failure.
Always close() files when you are finished with them, or use hs.fs.withFile(), which
closes the file for you. Any files still open are closed when your configuration is reloaded.
Example:
const f = hs.fs.open("~/notes.txt", "a+")
f.writeLine("Remember the milk")
f.rewind()
let line
while ((line = f.readLine()) !== null) console.log(line)
f.close()
The error from the most recent failed operation on this file, or `null` if no operation has failed.
The object has a `code` (a POSIX error name such as `"ENOENT"`, `"EACCES"`, `"EAGAIN"`, or `"EILSEQ"`
for invalid UTF-8) and a human-readable `message`. It is not cleared by successful operations.
Read UTF-8 text from the current position.
When `byteCount` is given, at most that many bytes are read. If that would split a multi-byte
UTF-8 character, reading stops before the character so it is returned whole by the next read.
If the data is not valid UTF-8, `null` is returned, the position is left unchanged, and
`lastError.code` is `"EILSEQ"`. Use `readBytes()` for binary data.
Like Lua's `f:read("a")`, reading to the end of the file never signals end of file: at the end it
returns `""`. Only a read with a `byteCount` returns `null` at end of file, so loop on `read(n)`.
Declaration
read(byteCount) -> string | null
Parameters
Name
Type
Description
byteCount
number
Maximum number of bytes to read. Omit (or pass `0`) to read to the end of the file.
Returns
string | null
The text read; `""` when reading to the end of the file from its end; `null` for a `byteCount` read at end of file, or on error.
Call a function for each remaining line of the file.
Line endings are stripped. The callback may return `false` to stop early, leaving the position
immediately after the last line delivered. If the callback throws, iteration stops and the
exception propagates to the caller.
The callback may use this file: if it moves the position or changes the file's contents, iteration
continues from the current position with the file as it now is. If it closes the file, iteration
stops and `eachLine` returns `false` with `lastError.code === "EBADF"`.
Declaration
eachLine(callback) -> boolean
Parameters
Name
Type
Description
callback
function
Called once per line. Return `false` to stop.
Returns
boolean
`true` if iteration completed or was stopped by the callback returning `false`; `false` on error, if the callback threw, or if the file was closed during iteration.
Example
f.eachLine(line => {
if (line.startsWith("#")) return
console.log(line)
})
Read raw bytes from the current position.
As with `read()`, reading to the end of the file returns an empty `Uint8Array` at the end, while a
read with a `byteCount` returns `null` at end of file.
Declaration
readBytes(byteCount) -> Uint8Array | null
Parameters
Name
Type
Description
byteCount
number
Maximum number of bytes to read. Omit (or pass `0`) to read to the end of the file.
Returns
Uint8Array | null
The bytes read; empty when reading to the end of the file from its end; `null` for a `byteCount` read at end of file, or on error.
Force any written data out to stable storage (`fsync`).
Writes are not buffered by `HSFile`, so other processes see them immediately; `flush()` additionally
guarantees they survive a crash or power loss.
Move the current position.
Unlike Lua's `f:seek()`, the offset is required: to find the current position or the size of the
file without moving, use the `position` and `size` properties.
Declaration
seek(offset, whence) -> number | null
Parameters
Name
Type
Description
offset
number
The byte offset (an integer), relative to `whence`. May be negative for `"cur"` and `"end"`.
whence
string
`"set"` (from the start of the file), `"cur"` (from the current position), or `"end"` (from the end of the file). Defaults to `"set"`.
Returns
number | null
The new position, or `null` on failure (including `EINVAL` if `offset` is missing or not an integer).
Example
f.seek(0, "end") // jump to the end
f.seek(-10, "cur") // back 10 bytes
const pos = f.seek(0, "cur")
Rename (move) the file on disk. The file stays open and `path` is updated.
Both paths must be on the same volume. By default this fails if `newPath` already exists.
While the file is open, this fails with `"ESTALE"` if `path` no longer refers to it (for example
because another process moved it and put something else in its place), rather than acting on
the wrong file. Once the file is closed, it acts on whatever is at `path`.
Declaration
rename(newPath, overwrite) -> boolean
Parameters
Name
Type
Description
newPath
string
The new path. `~` is expanded.
overwrite
boolean
Pass `true` to atomically replace any existing file at `newPath`. Defaults to `false`.
Returns
boolean
`true` on success, `false` on failure.
Example
// Atomically replace a file with new contents
const tmp = hs.fs.tempFile()
tmp.write(newContents)
tmp.flush()
tmp.rename("~/.myconfig", true)
tmp.close()
Copy the file on disk to a new path, including its metadata. The original stays open.
Fails if `destination` already exists, if the file has been removed, or (while the file is open)
with `"ESTALE"` if `path` no longer refers to it.
Remove the file from disk.
The file stays open: you can keep reading and writing it until `close()`, after which its storage is freed.
While the file is open, this fails with `"ESTALE"` if `path` no longer refers to it, so a file that
has since replaced it is never removed by mistake.
Declaration
remove() -> boolean
Returns
boolean
`true` on success, `false` on failure.
Example
const scratch = hs.fs.tempFile()
scratch.remove() // invisible to other processes from now on
scratch.write("private data")
Get metadata for the open file.
Returns the same object as `hs.fs.attributes()`, but reads it from the open file itself, so it
remains accurate after the file has been renamed or removed.
Take an advisory lock on the file (`flock`).
Advisory locks only affect other processes that also use locking; they do not prevent reads or writes.
Locks are released by `unlock()` or when the file is closed.
Declaration
lock(shared, wait) -> boolean
Parameters
Name
Type
Description
shared
boolean
Pass `true` for a shared (read) lock, which several processes may hold at once. Defaults to `false` (an exclusive lock).
wait
boolean
Pass `true` to wait until the lock is available. **This blocks Hammerspoon until the lock is acquired.** Defaults to `false`, which fails immediately with `lastError.code === "EAGAIN"` if the lock is held elsewhere.
Returns
boolean
`true` if the lock was acquired, `false` otherwise.
Example
const lockFile = hs.fs.open("~/.myscript.lock", "w")
if (!lockFile.lock()) console.log("Another instance is running")