Shell Internals

This article looks at the AmigaDOS Shell from the inside: how a command name becomes running code, what the Execute command really does with a script, how System() decides between synchronous and asynchronous shells and which streams it closes, how RunCommand() overlays the shell process, what each field of struct CommandLineInterface means, and how to write a custom shell that starts correctly through CliInitRun(). The shell language itself (redirection, operators, quoting) is described in The AmigaDOS Shell; argument parsing in ReadArgs, ReadItem and FindArg; variables in Shell Variables; and the resident list in Resident Segments.

All facts are for AmigaOS 3.x (dos.library V36–V47).

Command location

After variable, back-tick and alias expansion the Shell has to turn the command name into a seglist. The order is fixed:

Step Where What matches Notes
1 Resident list (FindSegment()) Resident segments Skipped if the command name is quoted. On a hit seg_UC is incremented and seg_Seg is run.
2 Current directory Files and directories Always searched, always first, cannot be disabled (unlike Unix).
3 Each directory of the path (cli_CommandDir) Files only Only if the name has no directory component.
4 C: (all directories of a multi-assign) Files only Implicitly the last path entry; cannot be removed.

If nothing matches, the Shell reports an unknown command. A directory found in step 2 is turned into CD <dir> and location restarts (CD is a built-in on the resident list).

A file is then classified by its protection bits:

Condition Action Since
s bit set, file starts with /* Prepend RX (ARexx script), restart location V37
s bit set, file starts with #! or ;! The rest of the first line names the interpreter; it is injected as command name, restart location V45
s bit set, otherwise Prepend Execute, restart location
e bit set LoadSeg() the file
e set, plus p and h set Also add the loaded segment to the resident list ("hold") V39, removed in V40, back in V45
e clear, VIEWER variable set and datatypes.library recognises the file Prepend the contents of VIEWER, restart location V45
none of the above "not executable" error (ERROR_FILE_NOT_OBJECT)

There is no built-in default for VIEWER: if it is unset, typing the name of a data file is an error. Setting it to MultiView lets the Shell display any file a datatype understands.

The s bit alone marks a script.

Once a segment is found the Shell sets up input, output and error streams (and its console process if the error output is interactive) and then either:

  • starts a background process for the command, when the command stands left of | or ||, or is followed by &; or
  • overlays the Shell process with the command through RunCommand() in every other case.

The Execute command

Despite its name the Execute command does not execute anything. It performs argument substitution on a script and then redirects the Shell's command input (cli_CurrentInput) to the result; the Shell then reads and runs the lines. The Execute() function is unrelated (see below).

Pseudo-commands

Pseudo-commands must appear at the top of the script. If the first line is not a pseudo-command, Execute skips substitution entirely and feeds the file to the Shell unchanged.

Pseudo-command Argument Purpose
.key / .k ReadArgs template Formal parameters of the script (one .key per script).
.default / .def key value or key=value Default for one parameter (empty string if no value).
.bra character Opening delimiter of a parameter reference (default <).
.ket character Closing delimiter (default >).
.dollar / .dol character Separator of an inline default (default $).
.dot character Character that introduces pseudo-commands (default .).

A reference has the form <NAME> or <NAME$default>. <$$> expands to the shell's CLI number, handy for unique temporary file names. Because < and > are also redirection operators, and $ introduces Shell variables, it is wise to switch the delimiters:

.key FROM/A,TO/K
.bra {
.ket }
.def TO RAM:
Copy "{FROM}" TO "{TO}" QUIET
Echo "done in shell {$$}"

A script that only wants <$$> still needs a first-line pseudo-command to switch substitution on:

.dot .  ; enable argument substitution
Echo "The shell number is <$$>"

Substitution rules:

  • Inside double quotes, * and " in the substituted value are escaped with *.
  • A value that was quoted on the command line (it contains spaces or tabs) is wrapped in quotes again, unless the reference is already inside quotes.
  • A /M parameter expands to several arguments.

Where the substituted script goes

The result is written to a temporary file in T:. Its name is stored in cli_CommandFile, and cli_CurrentInput is pointed at it. When the file is exhausted the Shell closes it, deletes the file named in cli_CommandFile and continues from cli_StandardInput (or terminates, depending on how the shell was started; see CliInitRun flags).

Nesting and redirection, by version

Behaviour V40 and earlier Current (V47)
Nested Execute inside a script The inner script was concatenated to the outer one; a script could not skip backwards over an Execute The Shell keeps a stack of scripts and redirections; each nested Execute gets a clean environment
Execute script >out Had no effect on the commands inside the script Special-cased by the Shell: the output of the whole script goes to out
Execute <stream / pipe into Execute Not supported The redirected input supplies the script, so no temporary file is needed

Example of the V47 pipe form (mounts every monitor driver):

List LFORMAT="%p%n" DEVS:Monitors/~(#?.info) | Execute

System() — create a shell

LONG SystemTagList(CONST_STRPTR command, CONST struct TagItem *tags);  /* V36 */
LONG System(CONST_STRPTR command, CONST struct TagItem *tags);         /* same entry */
LONG SystemTags(CONST_STRPTR command, ULONG tag1, ...);                /* varargs */

command is a shell script in a string (several lines separated by line feeds). The full Shell syntax is available, but no Execute-style <arg> substitution; to get it, pass Execute … as the command and feed the script through a file or a pipe. When the string runs out the shell terminates; it does not go on to read its input stream.

By default the new shell runs the BootShell resident segment, inherits the caller's pr_CIS/pr_COS, and, if the caller has a CLI (pr_CLI non-ZERO), copies its path, local variables, prompt, current directory and stack size. Without a calling CLI it gets a path of the current directory plus C:, no local variables, the prompt "%N> " and SYS: as current directory.

Synchronous or asynchronous

command SYS_Asynch Mode Return value
non-NULL FALSE Synchronous: waits for the script Return code of the last command; IoErr() = its error code
non-NULL TRUE Asynchronous 0, IoErr() = 0
NULL any Always asynchronous (legacy rule, independent of the tag) 0, IoErr() = 0
any any, creation failed −1, IoErr() = reason

Because a NULL command forces asynchronous execution, a synchronous run of a SYS_CmdStream script is impossible; the manual calls this a probable defect.

Who closes which stream

Stream Closed when the shell exits?
SYS_Input (or inherited pr_CIS) Not when synchronous. Yes when asynchronous, including when command is NULL, even if it is the caller's own pr_CIS.
SYS_Output (explicit, non-ZERO) Not when synchronous; yes with SYS_Asynch. With command NULL and no SYS_Asynch, the output is cloned (if interactive) and the original stays open for the caller.
SYS_Output = ZERO dos.library makes a stream itself and closes it at exit: if the input is interactive and (command non-NULL or SYS_Asynch), it opens "*" from the input's handler; otherwise "*" on NP_ConsoleTask or the caller's console; if that fails, NIL:.
SYS_InName, SYS_OutName (V47) Always closed.
SYS_CmdStream, SYS_CmdName (V47) Always closed, cannot be prevented.

The same filehandle must never be used for both input and output. For a console shell, pass only the input and set SYS_Output to ZERO.

Up to V45 SYS_Input was closed even when the shell could not be created and synchronous execution had been requested. This was fixed in V47.

Tags

Tag Meaning
SYS_Input 2.0+ BPTR input filehandle (default: caller's pr_CIS).
SYS_InName 3.2+ V47. Path opened as input. Mutually exclusive with SYS_Input.
SYS_Output 2.0+ BPTR output filehandle (default: caller's pr_COS); ZERO = derive one.
SYS_OutName 3.2+ V47. Path opened as output. Mutually exclusive with SYS_Output.
SYS_CmdStream 3.2+ V47. BPTR filehandle with the script, used only when command is NULL. With SYS_Asynch, the shell reads it to EOF, closes it, then reads SYS_Input until EOF or EndCLI: this is exactly NewShell.
SYS_CmdName 3.2+ V47. Path of the script; mutually exclusive with SYS_CmdStream.
SYS_Asynch 2.0+ Detach; implies the input and output streams are closed at exit.
SYS_UserShell 2.0+ TRUE: run the "shell" resident segment (user shell) instead of "BootShell". Default FALSE.
SYS_CustomShell 2.0+ Name of a resident segment to use as the shell.
NP_StackSize 2.0+ Stack in bytes for the shell itself (default 4096) and for the commands it runs. Without the tag, commands get the stack of the caller's shell, or 4096 bytes if the caller is not a shell.
NP_Name 2.0+ Process name (default "Background CLI"). Ignored up to V45.
NP_Priority 2.0+ Default: caller's priority.
NP_ConsoleTask 2.0+ Console handler port for "*" and CONSOLE:. An interactive output (NULL command) or input (otherwise) overrides it; an interactive input also receives ACTION_CHANGE_SIGNAL so that break signals reach the new shell.
NP_CopyVars 2.0+ Copy local variables (default TRUE).
NP_Path 2.0+ BPTR to a PathComponent list; taken over, not copied, and freed by the shell.
NP_ExitCode, NP_ExitData Exit hook, as for CreateNewProc().

Recipes used by the system

Run (input is a string stream holding the rest of its command line):

SystemTags(NULL, SYS_Input,     instream,
                 SYS_Output,    Output(),
                 SYS_UserShell, TRUE,
                 TAG_DONE);

The shell detaches because command is NULL. It is System(), not Run, that prints the new CLI number.

NewShell and the Workbench Shell icon (V47):

SystemTags(NULL, SYS_InName,    window,                       /* e.g. "CON:..." */
                 SYS_CmdStream, Open(from, MODE_OLDFILE),     /* S:Shell-Startup */
                 SYS_Output,    0,                            /* ZERO */
                 SYS_Asynch,    TRUE,
                 SYS_UserShell, TRUE,
                 NP_Name,       "Shell Process",
                 TAG_DONE);

The boot shell is created the same way with NP_Name "Initial CLI" and S:Startup-Sequence as command stream. V45 and earlier used an undocumented NewShell mechanism.

A synchronous script with its own console, owned by the caller:

BPTR con = Open("CON:0/0/640/200/Build/WAIT", MODE_NEWFILE);
if (con) {
    LONG rc = SystemTags("CD RAM:\nList\n",
                         SYS_Input,  con,
                         SYS_Output, 0,        /* ZERO: derive "*" from con */
                         TAG_DONE);
    if (rc == -1)
        PrintFault(IoErr(), "System");
    Close(con);                                /* synchronous: still ours */
}

Execute() — the legacy function

BOOL Execute(CONST_STRPTR commandString, BPTR input, BPTR output);

Obsolete; use System(). It always uses the BootShell and returns only DOSTRUE/DOSFALSE (whether the shell could be created), never a return code. It is roughly:

SystemTags(NULL, SYS_CmdStream, cmd,        /* string stream from commandString */
                 SYS_Input,     in,
                 SYS_Output,    out,
                 SYS_UserShell, FALSE,
                 NP_Priority,   0,
                 TAG_DONE) ? DOSTRUE : DOSFALSE;
  • commandString non-NULL: synchronous. The string runs first; then, if input is non-ZERO, commands are read from input until EOF or EndCLI. If output is ZERO, "*" is opened from an interactive input, else from the caller's console, else NIL:. Neither stream is closed.
  • commandString NULL: the shell runs asynchronously and Execute() returns at once. input is closed when the shell ends; output is cloned (if interactive) and the original stays open.

It has nothing to do with the Execute command: neither needs the other. Up to V34 it was built on Run. From V36 to V45 it closed input when the shell could not be created.

RunCommand — overlay the calling process

LONG RunCommand(BPTR seg, LONG stack, CONST_STRPTR paramptr, LONG paramlen);  /* V36 */

Runs an already loaded seglist (from LoadSeg(), or seg_Seg of a resident segment) inside the calling process. No process and no shell are created; this is how the Shell runs its commands, which is why a command's struct Process is the shell's own.

  • Stack. stack is in bytes and must be supplied; nothing is guessed. The Shell uses cli_DefaultStack * 4 and raises it if the seglist carries a $STACK: cookie. tc_SPLower/tc_SPUpper are updated, so a program may rely on their difference as its stack size.
  • Arguments are delivered three ways: a0 = paramptr and d0 = paramlen; pr_Arguments temporarily = paramptr (for GetArgStr()); and a copy is put into a freshly allocated buffer of the caller's pr_CIS, where buffered reads and ReadArgs() find it.
  • The argument line must end with a line feed (0x0a). ReadArgs() ignores anything after the first one; Run uses the whole string.
  • Set the program name with SetProgramName() first: it is not part of the arguments and C startup code builds argv[0] from it.
  • Everything is restored on return (stack, input buffer, arguments), except the FPU: its configuration leaks in both directions. A command must initialise the FPU itself; a caller that uses a mathieee library may need to reopen it afterwards.
  • Returns the command's return code, with IoErr() as the command left it, or −1 with ERROR_NO_FREE_STORE if the stack or input buffer could not be allocated.
/* Run C:List inside this process (caller must be a process). */
BPTR seg = LoadSeg("C:List");
if (seg) {
    static const char args[] = "RAM: ALL\n";       /* note the line feed */
    LONG rc;
    SetProgramName("C:List");                      /* only works from a shell */
    rc = RunCommand(seg, 8192, (STRPTR)args, sizeof(args) - 1);
    if (rc == -1)
        PrintFault(IoErr(), "RunCommand");
    UnLoadSeg(seg);
}

Finding shells: FindCliProc and MaxCli

struct Process *FindCliProc(ULONG num);   /* V36 */
ULONG           MaxCli(void);             /* V36 */
  • Only shells and commands started from shells get a task number (pr_TaskNum); Workbench programs, handlers and file systems do not.
  • The process table has no semaphore. Call both functions under Forbid() and use the result only until Permit().
  • MaxCli() is the capacity of the table, not the number of running shells, and it can change at any moment.
  • FindCliProc() returns NULL for a free slot and never touches IoErr().
/* Collect the numbers of running shells; print only after Permit(). */
ULONG i, n, found = 0;
LONG  nums[32];

Forbid();
n = MaxCli();
for (i = 1; i <= n && found < 32; i++)
    if (FindCliProc(i))
        nums[found++] = (LONG)i;
Permit();

for (i = 0; i < found; i++)
    Printf("CLI %ld is in use\n", nums[i]);

The CommandLineInterface structure

pr_CLI (a BPTR) or Cli() gives the shell's struct CommandLineInterface (dos/dosextens.h). Always create one with AllocDosObject(DOS_CLI, …): it then has hidden extended fields, and a hand-built CLI lacks TAB expansion and history.

Field Type Meaning
cli_Result2 LONG IoErr() of the last command (or the shell's own error). Read by Why; copied to $Result2.
cli_SetName BSTR Current directory name printed by %S. Updated by CD/PushCD/PopCD/SwapCD, not by the shell itself.
cli_CommandDir BPTR Command path: list of PathComponent (below).
cli_ReturnCode LONG Return code (d0) of the last command; copied to $RC.
cli_CommandName BSTR Command name as typed (may contain directories). Source of argv[0].
cli_FailLevel LONG Set by FailAt; return codes ≥ this abort a script.
cli_Prompt BSTR Prompt template.
cli_StandardInput BPTR Primary input (console, Run's string stream, SYS_Input/SYS_InName).
cli_CurrentInput BPTR Where commands are read now (command string, SYS_CmdStream, Execute script).
cli_CommandFile BSTR Temporary Execute script in T:, deleted by the shell when done.
cli_Interactive LONG Computed by the shell: prompts are printed; FALSE while running a script, when Ctrl-D is checked.
cli_Background LONG Set for asynchronous shells or non-interactive output. A synchronous System() with a command string is always foreground.
cli_CurrentOutput BPTR Unused; initialised to cli_StandardOutput.
cli_DefaultStack LONG Minimum command stack in long words (set by Stack). Not the shell's own stack.
cli_StandardOutput BPTR Default output of commands, prompt output.
cli_Module BPTR Seglist of the running command; the shell unloads it after the command returns. A load-and-stay-resident program sets it to ZERO to keep its code.

Whether a shell ends at EOF is decided by the CliInitRun flags, not by cli_Background, contrary to older documentation.

The path is a singly linked list of this structure, which no NDK header defines:

struct PathComponent {
    BPTR pc_Next;   /* next element, ZERO at the end */
    BPTR pc_Lock;   /* lock on the directory */
};

The current directory (first) and C: (last) are never in this list.

Accessor functions and IoErr()

Function From a shell Not from a shell
GetCurrentDirName(buf,len) Copies cli_SetName; DOSTRUE even if truncated (IoErr() = ERROR_LINE_TOO_LONG, else 0) NameFromLock(pr_CurrentDir): DOSFALSE + ERROR_LINE_TOO_LONG if too short
SetCurrentDirName(name) Truncates silently, DOSTRUE, no IoErr() DOSFALSE, no error code
GetProgramName(buf,len) DOSTRUE even if truncated (ERROR_LINE_TOO_LONG, else 0) Empty string, DOSFALSE, ERROR_OBJECT_WRONG_TYPE
SetProgramName(name) Truncates silently, DOSTRUE DOSFALSE; IoErr() never changed
GetPrompt(buf,len) As GetProgramName() Empty string, DOSFALSE, ERROR_OBJECT_WRONG_TYPE
SetPrompt(name) Truncates silently, DOSTRUE DOSFALSE; IoErr() never changed
Cli() 2.0+ CLI pointer NULL; IoErr() never changed

Pitfalls:

  • GetCurrentDirName() with a zero-sized buffer misbehaved in V36 (fixed in V37).
  • SetCurrentDirName() does not move the current directory; pair it with CurrentDir().
  • The buffer sizes of the CLI strings are fixed legacy values; the size tags of AllocDosObject() are ignored. Put long prompts in a local variable and reference it from the prompt.

Writing a custom shell

A shell is a resident segment. It is chosen with SYS_CustomShell, or it replaces the default by taking the name "shell":

Resident shell MyShell REPLACE SYSTEM

"BootShell" should be left alone: Execute() and the boot process use it. "CLI" is the same shell with BCPL binding, kept for compatibility.

Startup sequence

  1. Get the startup packet. Whether the shell is BCPL or C, it receives a DosPacket like a handler. With a C startup stub it must wait on pr_MsgPort and take the packet from mn_Node.ln_Name.
  2. Check it. In V47 the only startup the system sends has dp_Type non-zero and dp_Res1 = dp_Res2 = 0. (V36–V45 also had dp_Type = 0 for the Initial CLI and dp_Res1 non-zero for NewShell; V34 passed a BCPL function in dp_Type.) The packet's arguments are deliberately undocumented.
  3. Allocate resources. On failure set dp_Res1 = 0, put an error in dp_Res2, return the packet to dp_Port and exit.
  4. Move the shell's own seglist from pr_SegList[3] to [4]: commands need slot 3.
  5. Call CliInitRun(pkt): it builds the CLI from the packet and returns the flags below.
  6. Reply the packet at the time the flags demand, then run the command loop.

CliInitRun flags and when to reply

LONG CliInitRun(struct DosPacket *dp);   /* a0 */

No header defines the bits; the manual suggests these names:

Name Bit Meaning
FN_VALID 31 The other bits are valid
FN_ASYNC 3 Asynchronous call (SYS_Asynch)
FN_SYSTEM 2 Terminate at EOF of cli_CurrentInput (command string given); never interactive
FN_USERINPUT 1 Set: do not close cli_StandardInput at exit
FN_RUNOUTPUT 0 Set: do close cli_StandardOutput at exit (inverse logic of bit 1)
Result Meaning Reply the packet…
Bit 31 clear, IoErr() == own struct Process * Initialisation failed Never: it was already replied. Free resources and exit.
Bit 31 clear, any other IoErr() Success: Run, or Execute()/System() with NULL command and no SYS_Asynch After the first command has been loaded, with dp_Res1/dp_Res2 unchanged. Close both standard streams at exit.
Bits 31 + 3 Asynchronous System() Immediately, with dp_Res1/dp_Res2 unchanged.
Bit 31, bit 3 clear Synchronous System()/Execute() At exit, with dp_Res1 = cli_ReturnCode, dp_Res2 = cli_Result2.

The delayed reply for Run avoids two processes moving the floppy head at once (the Run caller and the background shell loading its command). Without FN_SYSTEM the shell switches to cli_StandardInput when cli_CurrentInput runs dry and only stops when the two are the same stream and hit EOF (Execute() with a command string).

Skeleton

The startup code below follows the manual (SAS/C register syntax; ShellMain() is the command loop and returns the packet if it has not been replied yet). The shell is assumed to start through a BCPL-compatible stub that passes the packet in a0, or NULL.

#include <exec/types.h>
#include <exec/alerts.h>
#include <dos/dos.h>
#include <dos/dosextens.h>
#include <proto/exec.h>
#include <proto/dos.h>

#define FN_VALID      31
#define FN_ASYNC       3
#define FN_SYSTEM      2
#define FN_USERINPUT   1
#define FN_RUNOUTPUT   0
#define FROM_SYSTEM   ((1L << FN_VALID) | (1L << FN_SYSTEM))
#define IS_ASYNC      ((1L << FN_VALID) | (1L << FN_ASYNC))

struct DosPacket *ShellMain(struct DosPacket *, LONG,
                            struct ExecBase *, struct DosLibrary *);

void __asm main(register __a0 struct DosPacket *pkt)
{
    struct ExecBase   *SysBase = *(struct ExecBase **)4L;
    struct DosLibrary *DOSBase;
    struct Process    *proc = (struct Process *)FindTask(NULL);
    struct CommandLineInterface *cli;
    BPTR *segs;
    LONG fn;

    if (pkt == NULL) {                      /* C startup: fetch it */
        struct Message *msg;
        WaitPort(&proc->pr_MsgPort);
        msg = GetMsg(&proc->pr_MsgPort);
        pkt = (struct DosPacket *)msg->mn_Node.ln_Name;
    }
    if (pkt->dp_Type == 0 || pkt->dp_Res1 || pkt->dp_Res2) {
        Alert(AN_CLIObsolete | AT_DeadEnd); /* not a V47 shell startup */
        return;
    }
    DOSBase = (struct DosLibrary *)OpenLibrary("dos.library", 47);
    if (DOSBase == NULL) {
        pkt->dp_Res1 = 0;
        pkt->dp_Res2 = ERROR_INVALID_RESIDENT_LIBRARY;
        PutMsg(pkt->dp_Port, pkt->dp_Link);
        return;
    }
    segs = BADDR(proc->pr_SegList);         /* free slot 3 for commands */
    if (segs[4] == 0) {
        segs[4] = segs[3];
        segs[3] = 0;
    }
    fn = CliInitRun(pkt);
    if (!(fn & (1L << FN_VALID)) && IoErr() == (LONG)proc) {
        CloseLibrary((struct Library *)DOSBase);   /* packet already replied */
        return;
    }
    if ((fn & IS_ASYNC) == IS_ASYNC) {
        ReplyPkt(pkt, pkt->dp_Res1, pkt->dp_Res2);
        pkt = NULL;
    }
    /* ShellMain() replies early for the !FN_VALID case after loading
       the first command, and sets pkt to NULL when it does so. */
    pkt = ShellMain(pkt, fn, SysBase, DOSBase);

    cli = Cli();
    if (!(fn & (1L << FN_VALID)))           /* Run, System(NULL,...) */
        fn = (1L << FN_RUNOUTPUT) | (1L << FN_ASYNC);
    Flush(Output());
    if (!(fn & (1L << FN_USERINPUT)))
        Close(cli->cli_StandardInput);
    if (fn & (1L << FN_RUNOUTPUT))
        Close(cli->cli_StandardOutput);
    if (pkt)
        ReplyPkt(pkt, cli->cli_ReturnCode, cli->cli_Result2);
    CloseLibrary((struct Library *)DOSBase);
}

Inside the command loop the essentials are:

  • compute cli_Interactive as "not background, cli_CurrentInput == cli_StandardInput, and not FROM_SYSTEM";
  • SelectInput(cli_CurrentInput) / SelectOutput(cli_StandardOutput) before each command;
  • in scripts check CheckSignal(SIGBREAKF_CTRL_D);
  • load into cli_Module, SetProgramName(), RunCommand(cli_Module, cli_DefaultStack << 2, args, len), store IoErr() in cli_Result2, then UnLoadSeg(cli_Module) unless the command cleared it;
  • stop a script when cli_ReturnCode >= cli_FailLevel;
  • at the end of a script delete cli_CommandFile (clear its protection first), close cli_CurrentInput and fall back to cli_StandardInput, or stop if FROM_SYSTEM.

CliInit() (Initial CLI, private) and CliInitNewcli() (NewShell) were used from V36 to V45. In V47 every shell, including the boot shell, is initialised through CliInitRun().

The shell hook (V47)

DoShellMethod() reaches the shell through a hook in the extended CLI. A shell gets the structure from a private function:

struct ExtendedCommandLineInterface *ExtendedCli(void);   /* V47, private, at offset 0x3c0 */

It returns NULL if the CLI was not made by AllocDosObject() (no extension); then the shell simply offers no methods, and TAB expansion and history are unavailable. Otherwise the shell initialises cle_Hook; cle_This points back to the structure and cle_Version is 47. The hook is called as

UBYTE *ShellHook(struct Hook *hook /* a0 */, ULONG *method /* a1 */,
                 struct TagItem *tags /* a2 */);   /* result in d0 */

where *method is the first argument of DoShellMethod() and tags its tag list. Every shell should answer SHELL_METH_METHODS. SHELL_METH_FGETS takes SHELL_FGETS_FULL: FALSE (default) expands relative to the current directory (used by ReadArgs() for ? input); TRUE treats the first word as a command and searches the whole path.

Note: the NDK 3.2 dos/shell.h declares struct ExtendedCommandLineInterface with the CLI fields flattened as cle_Result2 … cle_Module and cle_This typed as struct ExtendedCommandLineInterface *, rather than an embedded cle_CLI and a CommandLineInterface *; the layout is the same.

See Also


Sources: Thomas Richter, Amiga ROM Kernel Reference Manual: AmigaDOS (2024, v1.27), §15.1.9–15.1.10, §15.2, §15.3, §15.7 (pp. 319–337, 349–357); NDK 3.2 R4 Include_H/dos/dosextens.h, dos/dostags.h, dos/shell.h; Autodocs/dos.doc (CliInitRun, Execute, RunCommand, SystemTagList); SFD/dos_lib.sfd Updated: 2026-11-13