Directory Scanning¶
AmigaDOS offers two ways to list a directory: ExNext(), which returns one entry per call in a FileInfoBlock, and ExAll() (V36), which fills a caller-supplied buffer with as many entries as fit and can filter them with a pattern or a hook. ExAll() looks like the better design, but it has a long defect history in dos.library's fall-back code and in several file systems, and the ROM FFS gains nothing from it. The RKM AmigaDOS manual concludes that "ExAll() should probably be better left alone"; prefer ExNext() unless you target the RAM-Handler or a directory-cache FFS flavour, the only ROM file systems that noticeably profit from ExAll().
Both functions have strict usage rules that are easy to violate. This page lists them, gives correct examples, and collects the version-specific bugs. For the FileInfoBlock fields and entry types see Locks, FileInfoBlock and Examine.
Contents may change while you look. A lock on a directory keeps the directory itself alive, but other processes can still add, delete or rename entries during a scan. An entry you just received may already be gone, and new entries may not be reached by the current scan.
Choosing between ExNext() and ExAll()¶
ExNext() |
ExAll() |
|
|---|---|---|
| Available | All versions | V36; usable from V37 (V36 broken); ExAllEnd() V39 |
| Result | One FileInfoBlock per call |
Linked list of ExAllData in your buffer |
| Filtering | None (filter yourself) | eac_MatchString (pre-parsed pattern) or eac_MatchFunc (hook) |
| Abort | Just stop calling | Must call ExAllEnd() (fall-back implementation broken) |
| Owner IDs | fib_OwnerUID/GID |
ED_OWNER, not supported up to V37 |
| ROM FFS speed | Same as ExAll() |
Only faster with the directory-cache flavours (DOS\4, DOS\5) |
| Fall-back for file systems without native support | Not needed | Emulated by dos.library with Examine()/ExNext(); defective in several versions |
Scanning with Examine() and ExNext()¶
BOOL Examine(BPTR lock, struct FileInfoBlock *fib);
BOOL ExNext(BPTR lock, struct FileInfoBlock *fib);
Protocol:
Lock()the directory.- Call
Examine()on that lock with aFileInfoBlock. This describes the directory itself and initialises the scan state. - Call
ExNext()repeatedly with the same lock and the sameFileInfoBlockuntil it returnsDOSFALSE. - Check
IoErr():ERROR_NO_MORE_ENTRIESmeans the scan completed; anything else is a real error.
ExNext() does not resolve links: it can report ST_SOFTLINK, ST_LINKFILE and ST_LINKDIR, provided the file system distinguishes links at all. AmigaDOS file systems never return . or .. entries.
The rules¶
- Same lock, same FIB. State is kept in both the lock and the
FileInfoBlock. Unlocking and relocking the directory, or switching to another FIB, loses that state and forces the file system to rebuild it, which is slow. (File systems must cope with it, but onlyfib_DiskKeyandfib_DirEntryTypeare guaranteed to survive.) - Never reuse a FIB from one directory to scan another; it can confuse the file system. For a recursive scan, lock the subdirectory and
Examine()it into a newFileInfoBlock, so the parent's scan state is preserved. - Never
Examine()a file and then callExNext()on that FIB. - One scan per lock at a time. This matters for locks returned by
GetDeviceProc()/DeviceProc()for assigns: every caller receives the same lock, which also belongs todos.library.DupLock()it before scanning. - The FIB must be longword-aligned: use
AllocDosObject(DOS_FIB, NULL)or theD_S()macro, not a plain local variable. - Do not scan the
ZEROlock; lock the root explicitly (for exampleLock(":", SHARED_LOCK)orLock("DH0:", SHARED_LOCK)). See below. IoErr()is undefined after a successfulExNext(); only read it afterDOSFALSE.- You may stop an
ExNext()scan at any point; no explicit termination is needed. File systems must release scan state when the lock is released.
Example: list a directory¶
#include <dos/dos.h>
#include <dos/dosextens.h>
#include <dos/dosasl.h> /* ERROR_BREAK */
#include <proto/dos.h>
/* Returns 0 on success or a DOS error code. */
LONG ListDirectory(BPTR lock)
{
struct FileInfoBlock *fib;
LONG error = 0;
if ((fib = AllocDosObject(DOS_FIB, NULL)) == NULL)
return ERROR_NO_FREE_STORE;
if (Examine(lock, fib)) {
if (fib->fib_DirEntryType > 0) { /* Examine() never reports links */
while (ExNext(lock, fib)) {
if (CheckSignal(SIGBREAKF_CTRL_C)) {
error = ERROR_BREAK;
break; /* no cleanup needed for ExNext() */
}
if (fib->fib_DirEntryType == ST_SOFTLINK) /* test before "> 0" */
Printf("%s (soft link)\n", fib->fib_FileName);
else if (fib->fib_DirEntryType > 0)
Printf("%s (dir)\n", fib->fib_FileName);
else
Printf("%s %ld\n", fib->fib_FileName, fib->fib_Size);
}
if (error == 0) {
error = IoErr();
if (error == ERROR_NO_MORE_ENTRIES)
error = 0;
}
} else {
error = ERROR_OBJECT_WRONG_TYPE; /* a file, not a directory */
}
} else {
error = IoErr();
}
FreeDosObject(DOS_FIB, fib);
return error;
}
Example: recursive scan¶
Each level gets its own lock and its own FileInfoBlock. Links are not followed: soft and hard links turn the directory tree into a graph, and following them can loop.
LONG ScanTree(BPTR dirlock, LONG depth)
{
struct FileInfoBlock *fib;
LONG error = 0;
if ((fib = AllocDosObject(DOS_FIB, NULL)) == NULL)
return ERROR_NO_FREE_STORE;
if (Examine(dirlock, fib)) {
while (error == 0 && ExNext(dirlock, fib)) {
LONG i;
for (i = 0; i < depth; i++)
PutStr(" ");
Printf("%s\n", fib->fib_FileName);
if (fib->fib_DirEntryType > 0 &&
fib->fib_DirEntryType != ST_SOFTLINK &&
fib->fib_DirEntryType != ST_LINKDIR) {
BPTR old = CurrentDir(dirlock); /* name is relative to dirlock */
BPTR sub = Lock(fib->fib_FileName, SHARED_LOCK);
CurrentDir(old);
if (sub) {
error = ScanTree(sub, depth + 1); /* new FIB inside */
UnLock(sub);
}
}
}
if (error == 0) {
error = IoErr();
if (error == ERROR_NO_MORE_ENTRIES)
error = 0;
}
} else {
error = IoErr();
}
FreeDosObject(DOS_FIB, fib);
return error;
}
If Lock() on a subdirectory fails, the entry was deleted or is locked exclusively; skipping it is usually the right reaction.
Example: scanning every directory of a multi-assign¶
Lock("LIBS:", …) only reaches the first directory of a multi-assign. To visit them all, iterate with GetDeviceProc() and duplicate each dvp_Lock before scanning it:
void ListAssign(STRPTR name) /* e.g. "LIBS:" */
{
struct DevProc *dp = NULL;
while ((dp = GetDeviceProc(name, dp)) != NULL) {
if (dp->dvp_Lock) {
BPTR dir = DupLock(dp->dvp_Lock); /* dos.library owns dvp_Lock */
if (dir) {
ListDirectory(dir);
UnLock(dir);
}
}
/* dvp_Lock == ZERO: a device or volume root; Lock(name) works there */
if (!(dp->dvp_Flags & DVPF_ASSIGN))
break;
}
FreeDeviceProc(dp); /* NULL is fine */
}
The manual states that GetDeviceProc() may be called again even when DVPF_ASSIGN is clear (it then returns NULL, but without setting IoErr()); testing the flag is harmless and also works on early V36.
Note: the NDK 3.2 autodoc for
GetDeviceProc()states that in 2.0 and 2.01 you had to checkDVPF_ASSIGNbefore calling it again (fixed in the 2.02 release of V36).
Scanning with ExAll()¶
BOOL ExAll(BPTR lock, STRPTR buffer, LONG size, LONG type,
struct ExAllControl *control); /* V36 */
void ExAllEnd(BPTR lock, STRPTR buffer, LONG size, LONG type,
struct ExAllControl *control); /* V39 */
ExAllControl¶
struct ExAllControl {
ULONG eac_Entries; /* out: entries placed in the buffer */
ULONG eac_LastKey; /* private scan state, 0 before the first call */
STRPTR eac_MatchString; /* NULL or a ParsePatternNoCase() token string */
struct Hook *eac_MatchFunc; /* NULL or a filter hook */
};
- Allocate it with
AllocDosObject(DOS_EXALLCONTROL, NULL); the autodoc insists on this. It is zero-initialised; seteac_LastKeyto 0 again only if you reuse the structure for another scan, and never touch it between the calls of one scan. eac_Entriescan be 0 even though the scan continues. Always decide on the return value ofExAll(), never oneac_Entries.eac_MatchStringmust be a pattern already tokenised byParsePatternNoCase()(V37; destination buffer at least 2 × source length + 2 bytes). Names are then matched case-insensitively.eac_MatchFuncandeac_MatchStringare mutually exclusive; set at most one. With bothNULL, every entry matches.
The hook is called once per candidate entry, after the ExAllData has been filled:
| Register | Contents |
|---|---|
| A0 | struct Hook * |
| A1 | struct ExAllData * (the filled-in candidate, inside your buffer) |
| A2 | LONG * pointing to a copy of type |
| D0 (result) | Non-zero keeps the entry, zero discards it |
This is the standard hook convention (object in A2, message in A1), so the amiga.lib stub HookEntry can call a C function func(hook, typeptr, ead) through h_SubEntry. Note that HookEntry does not preserve A4, which matters for small-data-model code.
ExAllData and the type argument¶
struct ExAllData {
struct ExAllData *ed_Next;
STRPTR ed_Name;
LONG ed_Type; /* as fib_DirEntryType */
ULONG ed_Size; /* as fib_Size; undefined for directories */
ULONG ed_Prot; /* as fib_Protection */
ULONG ed_Days; /* the three fields of fib_Date */
ULONG ed_Mins;
ULONG ed_Ticks;
STRPTR ed_Comment; /* as fib_Comment */
UWORD ed_OwnerUID;
UWORD ed_OwnerGID;
};
Only the fields up to the one selected by type exist; the file system packs the next entry (and the strings) right after the last requested field. Never read a field beyond the requested type.
type (dos/exall.h) |
Value | Fields filled |
|---|---|---|
ED_NAME 2.0+ |
1 | ed_Next, ed_Name |
ED_TYPE 2.0+ |
2 | … up to ed_Type |
ED_SIZE 2.0+ |
3 | … up to ed_Size |
ED_PROTECTION 2.0+ |
4 | … up to ed_Prot |
ED_DATE 2.0+ |
5 | … up to ed_Ticks |
ED_COMMENT 2.0+ |
6 | … up to ed_Comment |
ED_OWNER 3.1+ |
7 | … up to ed_OwnerGID |
A file system that does not support the requested type fails with ERROR_BAD_NUMBER; retry with a smaller value. Up to V37 (both dos.library's emulation and the file systems), ED_OWNER always fails this way. ED_OWNER is the largest value.
Note: the NDK 3.2
dos/exall.hheader states that every file system supportingExAll()must support all types up toED_COMMENT, and marksED_OWNERas V39.
Return value and termination¶
- Non-zero: the buffer was too small for the rest of the directory. You must either call
ExAll()again with the identical lock (not merely another lock on the same directory) and the same control structure, or abort withExAllEnd(). DOSFALSE: the scan is over. IfIoErr()isERROR_NO_MORE_ENTRIES, it completed normally;ERROR_BAD_NUMBERmeans an unsupportedtype; anything else is an error (corrupt file system, medium removed…). The buffer may still hold valid entries from this last call (checkeac_Entries). Do not callExAllEnd()after aDOSFALSEreturn.- The
lockmust not beZERO. - The
buffermust be at least word-aligned (the autodoc recommends longword alignment) and large enough for at least oneExAllDataplus a name and a comment.
ExAllEnd()¶
ExAllEnd() (V39) aborts a scan that ExAll() reported as unfinished. Pass exactly the arguments of the ExAll() call it terminates, including the identical lock. It first asks the file system to abort (ACTION_EXAMINE_ALL_END); if the file system cannot, dos.library uses a fall-back implementation that is corrupt in every known version, including the SetPatch replacement. ExAllEnd() sets IoErr() to an undefined value. Before V39 there was no way to abort an ExAll() scan at all; you had to read to the end.
Example: ExAll() with a hook filter¶
The manual's own example installs a hook that tests ed_Type while requesting only ED_NAME, so ed_Type is never filled. The version below requests ED_TYPE, counts entries with eac_Entries (robust against the V36 RAM-Handler leaving ed_Next uninitialised) and aborts cleanly on Ctrl-C. It requires V39 because of ExAllEnd().
#include <exec/memory.h>
#include <dos/dos.h>
#include <dos/dosextens.h>
#include <dos/exall.h>
#include <dos/dosasl.h> /* ERROR_BREAK */
#include <utility/hooks.h>
#include <proto/exec.h>
#include <proto/dos.h>
#include <clib/alib_protos.h> /* HookEntry */
#define EXALL_BUFSIZE 4096
/* Called through HookEntry: (hook, A2 = type pointer, A1 = candidate) */
static ULONG SkipSoftLinks(struct Hook *hook, LONG *typeptr, struct ExAllData *ead)
{
return ead->ed_Type != ST_SOFTLINK;
}
LONG ListWithExAll(BPTR lock) /* lock must not be ZERO */
{
struct ExAllControl *eac;
struct ExAllData *buffer, *ed;
struct Hook hook;
LONG error = ERROR_NO_FREE_STORE;
ULONG n;
BOOL more;
hook.h_Entry = (HOOKFUNC)HookEntry;
hook.h_SubEntry = (HOOKFUNC)SkipSoftLinks;
hook.h_Data = NULL;
if ((buffer = AllocVec(EXALL_BUFSIZE, MEMF_PUBLIC)) == NULL)
return error;
if ((eac = AllocDosObject(DOS_EXALLCONTROL, NULL)) != NULL) {
eac->eac_LastKey = 0;
eac->eac_MatchFunc = &hook; /* eac_MatchString stays NULL */
do {
more = ExAll(lock, (STRPTR)buffer, EXALL_BUFSIZE, ED_TYPE, eac);
error = more ? 0 : IoErr();
for (ed = buffer, n = eac->eac_Entries; n > 0; n--, ed = ed->ed_Next)
Printf("%s%s\n", ed->ed_Name, ed->ed_Type > 0 ? "/" : "");
if (more && CheckSignal(SIGBREAKF_CTRL_C)) {
ExAllEnd(lock, (STRPTR)buffer, EXALL_BUFSIZE, ED_TYPE, eac);
error = ERROR_BREAK;
break;
}
} while (more);
if (error == ERROR_NO_MORE_ENTRIES)
error = 0;
FreeDosObject(DOS_EXALLCONTROL, eac);
}
FreeVec(buffer);
return error;
}
Example: requesting owner information with a fall-back¶
LONG type = ED_OWNER;
BOOL more;
for (;;) {
eac->eac_LastKey = 0;
more = ExAll(lock, (STRPTR)buffer, EXALL_BUFSIZE, type, eac);
if (more || IoErr() != ERROR_BAD_NUMBER || type == ED_NAME)
break;
type--; /* ED_OWNER -> ED_COMMENT -> ... */
}
/* continue the normal loop with 'type'; never read fields beyond it */
Example: pattern filter¶
UBYTE pattern[2 * 7 + 2]; /* strlen("#?.info") == 7 */
if (ParsePatternNoCase("#?.info", pattern, sizeof(pattern)) >= 0) { /* V37 */
eac->eac_MatchString = pattern; /* eac_MatchFunc must stay NULL */
/* ... ExAll() loop as above ... */
}
ParsePatternNoCase() returns 1 if the pattern has wildcards, 0 if not, and −1 on overflow or error.
The ZERO lock and V47¶
The ZERO lock is not a valid scan target. The manual notes that it "is typically not sufficient" for ExNext(), and ExAll() requires a non-ZERO lock. At the packet level, a file system may fail ACTION_EXAMINE_NEXT on ZERO with ERROR_INVALID_LOCK, and applications should lock ":" explicitly instead. The NDK 3.2 autodoc for ExNext() says the same: "Unlike Examine(), ExNext() does not support the 0 lock." In AmigaOS 3.2, the FFS (47.3) and the RAM-Handler (47.6) make such calls fail cleanly instead of causing hits; see OS 3.2 File System and Handler Changes.
Version history of ExAll() defects¶
| Version | Defect |
|---|---|
| V36 | RAM-Handler misused eac_MatchString and did not initialise ed_Next; the dos.library fall-back had similar problems. Use ExAll() only on V37+ |
| ≤ V37 | ED_OWNER unsupported, fails with ERROR_BAD_NUMBER (emulation and file systems) |
| FFS < V39 | ed_Comment not filled in correctly |
| ≤ V38 | No ExAllEnd(): a started scan cannot be aborted |
Directory-cache FFS (DOS\4/DOS\5) < V40 |
ExAll() broken; fixed in release 40, older versions patched by SetPatch |
dos.library fall-back ≤ V45 |
Passes useless arguments to the error requester on failure; SetPatch works around it for earlier versions |
| All versions | ExAllEnd() fall-back (file system cannot abort) is corrupt, including the SetPatch version |
Note: the NDK 3.2 autodoc describes the directory-cache bug as requiring an
Examine()of the lock beforeExAll()(V39, fixed in V40 and by SetPatch), and dates the error-requester fix to V47, with a patch in SetPatch V45.
Packet level¶
| Function | Packet | Notes |
|---|---|---|
Examine() 1.3+ |
ACTION_EXAMINE_OBJECT (23) |
Starts a scan; may initialise state in the lock |
ExNext() 1.3+ |
ACTION_EXAMINE_NEXT (24) |
Handlers must accept a different lock on the same directory and a different FIB; only fib_DiskKey and fib_DirEntryType may carry state. End: Res1 DOSFALSE, Res2 ERROR_NO_MORE_ENTRIES |
ExAll() 2.0+ |
ACTION_EXAMINE_ALL (1033) |
Arg2 (buffer) and Arg5 (ExAllControl) are plain APTRs, not BPTRs. If the handler answers ERROR_ACTION_NOT_KNOWN, dos.library emulates the packet |
ExAllEnd() 3.0+ |
ACTION_EXAMINE_ALL_END (1035) |
Aborts an unfinished ACTION_EXAMINE_ALL scan |
Handler writers: storing a raw block number or object pointer as scan state is a classic bug, because the object may be deleted between calls. Store something you can re-find (for example an index) and rebuild state if the lock changed. See DOS Packet Reference.
See Also¶
- Locks, FileInfoBlock and Examine
- Links
- DOS Pattern Matching —
MatchFirst()/MatchNext()as an alternative way to walk directories - The AmigaDOS Device List —
GetDeviceProc()and multi-assigns - dos.library Reference
- OS 3.2 File System and Handler Changes
- DOS Packet Reference
Sources: Thomas Richter, Amiga ROM Kernel Reference Manual: AmigaDOS (2024, v1.27), §7.1 (p. 69), §7.1.3–7.1.5 (pp. 74–79), §8.2.1 (pp. 109–111), §14.3.1–14.3.4 (pp. 270–275); NDK 3.2 R4 dos/exall.h, dos/dosextens.h, dos.doc (ExAll, ExAllEnd, ExNext, GetDeviceProc, ParsePatternNoCase), amiga_lib.doc (HookEntry)
Updated: 2026-11-13