API Docs

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()

Properties

path

string
The absolute path of the file, with symbolic links resolved. Updated by `rename()`.

mode

string
The mode the file was opened with (e.g. `"r"`, `"w+"`).

isOpen

boolean
Whether the file is still open.

position

number
The current byte offset within the file. `0` if the file is closed.

size

number
The current size of the file in bytes. `0` if the file is closed.

atEnd

boolean
Whether the current position is at (or beyond) the end of the file.

lastError

JSValue
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.

Methods

read(byteCount) -> string | null

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)`.
read(byteCount) -> string | null
Name Type Description
byteCount number Maximum number of bytes to read. Omit (or pass `0`) to read to the end of the file.
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.
const everything = f.read()
const first100 = f.read(100)

readLine(keepNewline) -> string | null

Read the next line of UTF-8 text. Both `\n` and `\r\n` line endings are recognised. A final line with no trailing newline is still returned.
readLine(keepNewline) -> string | null
Name Type Description
keepNewline boolean Pass `true` to keep the line ending in the returned string. Defaults to `false`.
string | null
The line, or `null` at end of file or on error.
let line
while ((line = f.readLine()) !== null) {
    console.log(line)
}

eachLine(callback) -> boolean

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"`.
eachLine(callback) -> boolean
Name Type Description
callback function Called once per line. Return `false` to stop.
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.
f.eachLine(line => {
    if (line.startsWith("#")) return
    console.log(line)
})

readLines() -> string[] | null

Read all remaining lines of the file into an array. Line endings are stripped. On error the position is left unchanged.
readLines() -> string[] | null
string[] | null
An array of lines (empty at end of file), or `null` on error.
const lines = f.readLines()
console.log(lines.length + " lines")

readBytes(byteCount) -> Uint8Array | null

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.
readBytes(byteCount) -> Uint8Array | null
Name Type Description
byteCount number Maximum number of bytes to read. Omit (or pass `0`) to read to the end of the file.
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.
const magic = f.readBytes(8)
if (magic && magic[0] === 0x89 && magic[1] === 0x50) console.log("PNG file")

write(text) -> boolean

Write a string to the file as UTF-8 at the current position (or at the end, for files opened in `"a"`/`"a+"` mode).
write(text) -> boolean
Name Type Description
text string The text to write.
boolean
`true` on success, `false` on failure.
f.write("Hello, ")
f.write("world!\n")

writeLine(text) -> boolean

Write a string followed by a newline (`\n`).
writeLine(text) -> boolean
Name Type Description
text string The text to write.
boolean
`true` on success, `false` on failure.
f.writeLine("one")
f.writeLine("two")

writeBytes(bytes) -> boolean

Write raw bytes to the file.
writeBytes(bytes) -> boolean
Name Type Description
bytes JSValue The bytes to write. Any typed array is accepted; its underlying bytes are written as-is.
boolean
`true` on success, `false` on failure (including if `bytes` is not a typed array or `ArrayBuffer`).
f.writeBytes(new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]))

flush() -> boolean

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.
flush() -> boolean
boolean
`true` on success, `false` on failure.
f.write(importantData)
f.flush()

truncate(length) -> boolean

Truncate (or extend with zero bytes) the file to the given length. The position is not changed.
truncate(length) -> boolean
Name Type Description
length number The new length in bytes. Omit (or pass `0`) to empty the file.
boolean
`true` on success, `false` on failure.
f.truncate()        // empty the file
f.truncate(1024)    // make it exactly 1KB

seek(offset, whence) -> number | null

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.
seek(offset, whence) -> number | null
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"`.
number | null
The new position, or `null` on failure (including `EINVAL` if `offset` is missing or not an integer).
f.seek(0, "end")       // jump to the end
f.seek(-10, "cur")     // back 10 bytes
const pos = f.seek(0, "cur")

rewind() -> boolean

Move the current position back to the start of the file.
rewind() -> boolean
boolean
`true` on success, `false` on failure.
f.write("data")
f.rewind()
console.log(f.read())

rename(newPath, overwrite) -> boolean

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`.
rename(newPath, overwrite) -> boolean
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`.
boolean
`true` on success, `false` on failure.
// Atomically replace a file with new contents
const tmp = hs.fs.tempFile()
tmp.write(newContents)
tmp.flush()
tmp.rename("~/.myconfig", true)
tmp.close()

duplicate(destination) -> boolean

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.
duplicate(destination) -> boolean
Name Type Description
destination string The path for the copy. `~` is expanded.
boolean
`true` on success, `false` on failure.
f.duplicate("~/notes-backup.txt")

remove() -> boolean

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.
remove() -> boolean
boolean
`true` on success, `false` on failure.
const scratch = hs.fs.tempFile()
scratch.remove()   // invisible to other processes from now on
scratch.write("private data")

attributes() -> Record | null

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.
attributes() -> Record | null
Record | null
Attributes object, or `null` on failure.
console.log(f.attributes().modificationDate)

setPermissions(permissions) -> boolean

Set the POSIX permission bits of the file.
setPermissions(permissions) -> boolean
Name Type Description
permissions number The permission bits, as an integer between `0` and `0o7777`, e.g. `0o600`. Use an octal literal: decimal `600` is a different (and unusual) mode.
boolean
`true` on success, `false` on failure (including `EINVAL` if `permissions` is missing or out of range).
f.setPermissions(0o600)

touch(accessDate, modificationDate) -> boolean

Set the access and modification times of the file. The argument order matches `hs.fs.touch()` and Hammerspoon v1: access time first.
touch(accessDate, modificationDate) -> boolean
Name Type Description
accessDate number Seconds since the Unix epoch (fractions allowed). Defaults to now.
modificationDate number Seconds since the Unix epoch (fractions allowed). Defaults to `accessDate`.
boolean
`true` on success, `false` on failure (including `EINVAL` for a `NaN`, infinite, or out-of-range timestamp).
f.touch()                          // now
f.touch(Date.now() / 1000 - 3600)  // both times one hour ago

lock(shared, wait) -> boolean

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.
lock(shared, wait) -> boolean
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.
boolean
`true` if the lock was acquired, `false` otherwise.
const lockFile = hs.fs.open("~/.myscript.lock", "w")
if (!lockFile.lock()) console.log("Another instance is running")

unlock() -> boolean

Release a lock taken with `lock()`.
unlock() -> boolean
boolean
`true` on success, `false` on failure.
f.unlock()

close() -> boolean

Close the file. Calling `close()` on a file that is already closed does nothing.
close() -> boolean
boolean
`true` on success (or if the file was already closed), `false` on failure.
f.close()