Skip to content

Files & file manipulation

FILE is bytes at a name. DATA is a revision of an identity.

This page is about working with files from a d/BASIC program: listing a folder, reading and writing bytes, saving safely, renaming, moving, trashing and making directories. After reading it you will know which operation to reach for, what each one promises, and why d/BASIC has no classic OPEN #1 statement.

FILE is d/BASIC's family of file operations. Each one is an intent: a named request such as "list this folder" or "read these bytes" that the machine carries out however it can. The family covers the everyday jobs: list a directory, inspect an entry, copy bytes, save an output, rename, move, trash, restore, or make a directory. It is deliberately separate from the DATA intents, which are d/BASIC's database, and from the small fixed DATA / READ tables you can embed in source.

Need Correct boundary
flash:/NOTES.TXT and its bytes FILE
a d/Write document identity and revision DATA Intents
fixed lookup values compiled into the program DATA / READ

A laptop's home directory, a browser's sandbox and a d/OS volume are not the same thing, and the FILE family does not pretend they are. Instead, the d/OS runtime (the program that runs your compiled code) offers one supervised set of named places that mean the same thing everywhere.

Mounts are the top of the namespace

Every path starts with a mount: a named place, such as flash or dos, that the runtime lists in its mount table. Local mounts are fixed, known entries. The cartridge that carries a modern processor into the 8-bit machine, a removable disk, or a network service may add entries of its own, but none of them can rewrite the local ones.

Example mount Typical role
flash the installed, writable local store, where the product offers one
dos a local FAT or media partition
brain storage offered by the cartridge that carries a modern processor into the 8-bit machine
ftp0, sftp0, scp0 remote storage, listed only after the part of the host that handles network transfers reports it is ready
usb0 the reserved name for removable media

You will see paths written like flash:/NOTES.TXT. Under the hood a request names a mount by its number plus a path relative to that mount; the directory the host actually configured for that mount never becomes a value your program can read. Protected areas, such as where installed packages live, never turn into a handy editable path.

Most applications begin with the typed layer from d/Works, the office suite being written in d/BASIC (d/Write, d/Calc, d/Present). Its file library wraps the raw requests in ordinary procedures:

IMPORT dworks.files BAKED

It supplies builders for mounts, entries, streams and edits while keeping exactly the FILE meaning described below. Imports & Libraries explains BAKED, SHARED and how a library is bound to an exact version. The raw intent examples in this chapter show the underlying layer for library authors and curious readers; application code never has to memorize record layouts.

Declare the need, then ask the mount

APP "MORNING REPORT"
  LABEL "Morning Report"
  NEEDS FILES
END APP

DIM MountRequest%(0 TO 1)
DIM MountReply%(0 TO 17)

' Request mount index zero. A typed library normally decodes the reply.
MountRequest%(0) = 0
INTENT FILE.QUERY_MOUNT(MountRequest%(), MountReply%())

PRINT "MOUNT QUERY COMPLETE"
END

QUERY_MOUNT answers more than "are files available?" It reports one mount's name, its state, its permissions, how quickly it tends to answer, how much space it has, the limits on path and name length, and how many mounts exist right now.

State Meaning
PRESENT the mount answered and lists what it can do
ABSENT the mount is known to be missing; its capability word is zero
DEGRADED the mount is present and serving, but one or more of the abilities it normally lists are currently declined or withdrawn

There is no such thing as a dead mount that simply lists nothing. "SFTP is absent" and "SFTP has no matching files" are different facts, and a script needs both.

The eight file verbs

Intent Job Safe to repeat?
FILE.QUERY_MOUNT inspect one mount yes
FILE.LIST fetch one bounded page of a directory yes
FILE.STAT inspect one named entry yes
FILE.OPEN open for read, append, create or replace no
FILE.READ read a bounded run of bytes yes
FILE.WRITE write a run of bytes at an explicit offset yes
FILE.CLOSE release a session, optionally committing staged output no
FILE.EDIT delete, trash, restore, rename, move or make a directory only against the directory version you observed

Every call passes its request and reply through a one-dimensional INTEGER array of fixed size, two little-endian bytes per element. The array must be exactly the documented size; a larger buffer is not accepted. The runtime holds a reply back until it has fully and successfully decoded it, so a call that is still pending, malformed, out of date, failed or overlapping another argument never shows you half a record. Application authors should call a typed wrapper rather than fill in these fields by hand.

List first, then act on what you saw

LIST takes a folder path relative to its mount, a filter, an optional short match pattern and a skip count; it returns at most eight entries. The host does the matching, so a small machine does not have to scan every name itself. The pattern can be an exact name, prefix* or *.suffix; it is deliberately not a full glob.

DIM ListRequest%(0 TO 107)
DIM Page%(0 TO 167)

' A file-library encoder supplies mount, directory, filter, "*.BAS", and skip.
INTENT FILE.LIST(ListRequest%(), Page%())
END

The reply carries the count, whether there is more, the directory's generation (a version number that changes whenever the directory changes), an optional total, and up to eight 40-byte entries. Each entry has a kind (file, directory or other), flags, a length and a bounded name. A name too long to fit is flagged, never cut short into a name that might open the wrong file.

Pages are numbered rather than held open as sessions, so a script can abandon a walk without leaking a handle. The honest trade-off is that if the directory changes between pages, an entry can be skipped or repeated. Keep the generation the reply gave you; if it changes, start the walk again. STAT answers the smaller question "what is this entry now?" A plain not-found status is different from the error you get for a malformed or protected request.

Open, read, write, close

OPEN has four modes:

Mode Meaning
READ read existing bytes
CREATE stage new output; it replaces the old file only when the commit succeeds
APPEND add to an existing file
REPLACE stage a replacement, but decline if no file of that name exists

The reply carries a handle and its generation, the current length, and the largest run of bytes that handle will accept in one call. A stale handle is declined rather than pointed at a slot that has since been reused. READ and WRITE each move a bounded chunk, up to 256 bytes.

DIM OpenRequest%(0 TO 97)
DIM Handle%(0 TO 7)
DIM ReadRequest%(0 TO 7)
DIM Chunk%(0 TO 131)
DIM CloseRequest%(0 TO 7)

' Typed encoders provide path, mode, handle/generation, offset, and count.
INTENT FILE.OPEN(OpenRequest%(), Handle%())
INTENT FILE.READ(ReadRequest%(), Chunk%())
INTENT FILE.CLOSE(CloseRequest%(), Handle%())
END
Row Request Reply
QUERY_MOUNT 4 B / 2 INTEGERs 36 B / 18
LIST 216 B / 108 336 B / 168
STAT / OPEN 196 B / 98 16 B / 8
READ 16 B / 8 264 B / 132
WRITE 272 B / 136 16 B / 8
CLOSE 16 B / 8 16 B / 8
EDIT 392 B / 196 16 B / 8

Atomic replacement makes Save honest

For CREATE and REPLACE, CLOSE(commit = 1) is the moment the new file replaces the old one, all at once. CLOSE(commit = 0) throws the staged bytes away. A cancelled Save therefore never leaves you with the old file replaced by the first half of the new one.

The commit can still be declined; a mount backed by a journal might run out of room at exactly that moment. The commit reply is what tells you the bytes are safe, not the last WRITE and not the Save button. WRITE takes an explicit offset, so writing the same bytes to the same place twice gives the same result.

Copy is a workflow, not a magic verb

A truthful copy is built from the four stream operations:

  1. OPEN the source for READ.
  2. OPEN the destination for staged CREATE or REPLACE.
  3. Repeat READ and WRITE at explicit offsets. Advance by READ.got and WRITE.put; keep and retry whatever a short transfer left behind.
  4. CLOSE(commit = 1) the destination and require a successful commit reply.
  5. Close the source on every path.
  6. If the user asked to move a file to a different mount, delete the source only after the destination commit, and report a failed delete separately.

That last distinction matters. A finished copy followed by a delete that was declined is still a finished copy; calling the whole thing "failed" would hide where the new bytes went.

Exporting a database object with revision history uses this FILE workflow only after reading its exact DATA identity and revision. Importing reads a FILE, checks it in private, then creates a new DATA object. Both are explicit copies across two boundaries, never a live link dressed up as a path.

Safe edits: delete, trash, rename, move, mkdir

FILE.EDIT gathers the operations that change names rather than bytes into one compact request:

Operation Meaning
DELETE remove one entry where permitted
TRASH / RESTORE use the mount's supervised trash
RENAME change a name within one mount
MOVE move within the same mount
MKDIR make one directory

An edit includes expect_generation: the directory version number you saw when listing or inspecting. If someone changed the directory in between, the edit is declined as out of date instead of renaming or deleting a surprise replacement. expect_generation = 0 says the script deliberately did not look first.

DIM EditRequest%(0 TO 195)
DIM EditReply%(0 TO 7)

' Encoder supplies mount, EDIT_RENAME, observed generation, old path, new path.
INTENT FILE.EDIT(EditRequest%(), EditReply%())
END

MOVE between two mounts is declined, because it is really a copy followed by a delete and can fail between the two. Let the interface say "Copying", commit the destination, then delete as a separate, visible step.

A file-script phrasebook

Familiar shell idea d/BASIC FILE workflow
ls / dir page through FILE.LIST, restarting if the directory generation changes
stat FILE.STAT one mount-relative name
cat / read bytes OPEN(READ) → bounded READ loop → CLOSE
redirect / replace OPEN(CREATE or REPLACE) → bounded WRITE loop → CLOSE(commit = 1)
copy a read loop on the source plus a staged write loop on the destination
rename FILE.EDIT(RENAME) with the observed generation
move FILE.EDIT(MOVE) on one mount; an explicit copy then delete across mounts
remove FILE.EDIT(DELETE) for one entry
new directory FILE.EDIT(MKDIR)
trash / restore FILE.EDIT(TRASH or RESTORE) when that mount offers it

There is no hidden recursive delete. Walking a tree is a visible script that can be interrupted, which is exactly where repeated, consequential actions belong.

Console scripts and GUI applications use the same surface

A d/BASIC program with no window or event handlers is a console program, which makes FILE a good base for small pipeline jobs:

APP "LISTING"
  LABEL "BASIC listing"
  NEEDS FILES
END APP

' Query mounts, page FILE.LIST output, PRINT entries, then choose a shell code.
PRINT "LISTING COMPLETE"
END

Local mounts answer on the first poll. Media, cartridge and network mounts may pause the script's task while the desktop carries on. A window program must never loop and wait inside DRAW: keep the operation's progress in the program's model, ask for a repaint when it changes, and keep cancel and close reachable.

Permissions and protected paths stay visible

Files are places, not records owned by a user. Every mount lists its own abilities:

Capability Permits
LIST, STAT, READ inspect names and bytes
WRITE, CREATE append or stage output
DELETE, RENAME, MOVE, MKDIR change names and directories
TRASH, RESTORE use of the mount's supervised trash
ATOMIC_REPLACE staged replacement that lands all at once

A read-only volume can honestly offer inspection alone. A protected system path answers PROTECTED PATH, not a fake NOT FOUND. A bad pattern, a full disk, an out-of-date generation, a missing operation and a cross-mount move each stop with a named error that says what blocked them.

ON REFUSAL GOTO FileProblem

' A FILE operation belongs here.
PRINT "DONE"
END

FileProblem:
PRINT "FILE OPERATION REFUSED. RULE "; ERR
END

One source, several routes

Where it runs Honest route
browser / WebAssembly a sandbox namespace the runtime has checked and approved, never arbitrary files the browser can see
Windows, Linux, macOS configured mount directories, through the same bounded requests and the same all-or-nothing commit
d/OS local store the local flash or media route, with its limits published
d/OS cartridge or network an extra mount only once the part of the host that serves it is actually ready
no file support at all FILES and every FILE.* request are declined by name before the program starts

The runtime may make transfers faster or offer more mounts. It may not borrow a home directory, invent write permission, call a half-finished copy a move, or quietly shorten names or bytes.

Why this is not classic OPEN #1

d/BASIC does not add OPEN #, INPUT #, PRINT #, LINE INPUT #, GET, PUT or FIELD as file-number statements that block until done. A channel number would hide the mount, the paused operation, the version number and the exact error: the very facts portable file work needs to keep in view.

Typed d/Works helpers can make the eight FILE operations pleasant to call without inventing a second model of the filesystem. Raw host paths, a process-wide current directory, native file descriptors, recursive changes and general glob engines do not cross into the language.

File checklist

  1. Use FILE for named bytes; use DATA for records and revisions.
  2. Query each mount; never guess its abilities from its name.
  3. Read listings a page at a time and start over if the generation changes.
  4. Keep a handle and its generation together.
  5. Respect the largest chunk the mount advertises.
  6. Treat CLOSE(commit) as the moment a Save is real.
  7. Pass the generation you observed with every destructive edit.
  8. Spell a move between mounts as copy, then delete.
  9. Close on success, on cancel, on an error, and when the window closes.

Next: Database & DATA Intents →