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
/Mparameter 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;
commandStringnon-NULL: synchronous. The string runs first; then, ifinputis non-ZERO, commands are read frominputuntil EOF orEndCLI. Ifoutputis ZERO,"*"is opened from an interactive input, else from the caller's console, elseNIL:. Neither stream is closed.commandStringNULL: the shell runs asynchronously andExecute()returns at once.inputis closed when the shell ends;outputis 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.
stackis in bytes and must be supplied; nothing is guessed. The Shell usescli_DefaultStack * 4and raises it if the seglist carries a$STACK:cookie.tc_SPLower/tc_SPUpperare updated, so a program may rely on their difference as its stack size. - Arguments are delivered three ways:
a0=paramptrandd0=paramlen;pr_Argumentstemporarily =paramptr(forGetArgStr()); and a copy is put into a freshly allocated buffer of the caller'spr_CIS, where buffered reads andReadArgs()find it. - The argument line must end with a line feed (0x0a).
ReadArgs()ignores anything after the first one;Runuses the whole string. - Set the program name with
SetProgramName()first: it is not part of the arguments and C startup code buildsargv[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 withERROR_NO_FREE_STOREif 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 untilPermit(). 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 touchesIoErr().
/* 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 withCurrentDir().- 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¶
- Get the startup packet. Whether the shell is BCPL or C, it receives a
DosPacketlike a handler. With a C startup stub it must wait onpr_MsgPortand take the packet frommn_Node.ln_Name. - Check it. In V47 the only startup the system sends has
dp_Typenon-zero anddp_Res1=dp_Res2= 0. (V36–V45 also haddp_Type= 0 for the Initial CLI anddp_Res1non-zero for NewShell; V34 passed a BCPL function indp_Type.) The packet's arguments are deliberately undocumented. - Allocate resources. On failure set
dp_Res1= 0, put an error indp_Res2, return the packet todp_Portand exit. - Move the shell's own seglist from
pr_SegList[3]to[4]: commands need slot 3. - Call
CliInitRun(pkt): it builds the CLI from the packet and returns the flags below. - 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_Interactiveas "not background,cli_CurrentInput == cli_StandardInput, and notFROM_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), storeIoErr()incli_Result2, thenUnLoadSeg(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), closecli_CurrentInputand fall back tocli_StandardInput, or stop ifFROM_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.hdeclaresstruct ExtendedCommandLineInterfacewith the CLI fields flattened ascle_Result2…cle_Moduleandcle_Thistyped asstruct ExtendedCommandLineInterface *, rather than an embeddedcle_CLIand aCommandLineInterface *; the layout is the same.
See Also¶
- The AmigaDOS Shell
- ReadArgs, ReadItem and FindArg
- Shell Variables
- Resident Segments
- Stream and String Handles
- Hunk Version and Stack Cookies
- DOS Objects Lifecycle
- DOS Packets
- Process Creation and Structure
- LoadSeg and Segment Lists
- AmigaDOS Library Internals
- OS 3.2 Shell Commands
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