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:

  1. Lock() the directory.
  2. Call Examine() on that lock with a FileInfoBlock. This describes the directory itself and initialises the scan state.
  3. Call ExNext() repeatedly with the same lock and the same FileInfoBlock until it returns DOSFALSE.
  4. Check IoErr(): ERROR_NO_MORE_ENTRIES means 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 only fib_DiskKey and fib_DirEntryType are 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 new FileInfoBlock, so the parent's scan state is preserved.
  • Never Examine() a file and then call ExNext() 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 to dos.library. DupLock() it before scanning.
  • The FIB must be longword-aligned: use AllocDosObject(DOS_FIB, NULL) or the D_S() macro, not a plain local variable.
  • Do not scan the ZERO lock; lock the root explicitly (for example Lock(":", SHARED_LOCK) or Lock("DH0:", SHARED_LOCK)). See below.
  • IoErr() is undefined after a successful ExNext(); only read it after DOSFALSE.
  • 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 check DVPF_ASSIGN before 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; set eac_LastKey to 0 again only if you reuse the structure for another scan, and never touch it between the calls of one scan.
  • eac_Entries can be 0 even though the scan continues. Always decide on the return value of ExAll(), never on eac_Entries.
  • eac_MatchString must be a pattern already tokenised by ParsePatternNoCase() (V37; destination buffer at least 2 × source length + 2 bytes). Names are then matched case-insensitively.
  • eac_MatchFunc and eac_MatchString are mutually exclusive; set at most one. With both NULL, 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.h header states that every file system supporting ExAll() must support all types up to ED_COMMENT, and marks ED_OWNER as 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 with ExAllEnd().
  • DOSFALSE: the scan is over. If IoErr() is ERROR_NO_MORE_ENTRIES, it completed normally; ERROR_BAD_NUMBER means an unsupported type; anything else is an error (corrupt file system, medium removed…). The buffer may still hold valid entries from this last call (check eac_Entries). Do not call ExAllEnd() after a DOSFALSE return.
  • The lock must not be ZERO.
  • The buffer must be at least word-aligned (the autodoc recommends longword alignment) and large enough for at least one ExAllData plus 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 before ExAll() (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


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