The AmigaDOS Shell

The Shell is AmigaDOS's command-line interpreter, built into Kickstart ROM. It implements a small language descended from Tripos, extended over the years with variable substitution, pipes, back-tick expansion, and redirection. Besides interactive use, the Shell boots the system by running the Startup-Sequence script in the S: assign, and user programs can launch a Shell to interpret scripts. This article summarizes the language from the 2024 ROM Kernel Reference Manual (Ch. 15).

Command lines

The Shell reads commands line by line. Each line is a command plus arguments separated by spaces or tabs. An argument may be a bare value or a keyword=value pair (no spaces around =). Commands are resolved as an alias, a script, or an executable on the path — which always includes the current directory and the C: assign (extend it with the Path command).

I/O redirection

Operator Effect
>path Redirect output to a file (create/overwrite)
>>path Append output to a file
<path Redirect input from an existing file
<<ind Here-doc: read the script/console until a line starting with ind (V45+)
<>path Redirect both stdin and stdout to a file (e.g. detach to NIL:)
*>path Redirect error output to a file; if interactive, also sets the console process (V45+)
*>>path Append error output (V45+)
*<> Create an error stream merged into stdout (V45+)

Redirection operators must be separated from arguments by spaces/tabs. Placement usually doesn't matter, except for Run/Alias (redirection must follow the command directly), and for all commands if the oldredirect variable is set (which emulates the V33 BCPL CLI).

Compound commands (V45+)

Operators that sit between commands (space-separated):

  • | — pipe: left command's stdout becomes the right command's stdin. The pipe can be addressed explicitly as PIPE:. Requires the Queue-Handler.
  • || — concatenate two commands' output into one stream (not the bash meaning).
  • && — run the right command only if the left one's return code is below the FailAt threshold.
  • ( ... ) — group commands into a sub-shell (separate process).

Unary operators

  • & — run the command line in the background (like Run).
  • + — at end of line: append the next input line to this command's argument line (used with Run to build multi-line background scripts).
  • ; — comment: everything after is ignored.

Quoting and escaping

Double quotes ("...") protect spaces, tabs, =, and operators. A quote only starts a quoted string at the start of a line or after a space/tab; a quote always terminates one. The asterisk * is the escape character:

Substituted only inside double quotes (left for the program to replace, typically via ReadArgs()):

Sequence Meaning
*N newline (0x0a)
*E ESC (0x1b); use *E[ for the 7-bit CSI equivalent
*" literal double quote
** literal asterisk

Substituted everywhere by the Shell itself:

Sequence Meaning
*$ literal $
*`` literal back-tick
*[ / *] literal square bracket (only meaningful inside alias expansion)

So Echo "Hello*NThere" prints on two lines (Echo/ReadArgs substitutes *N), while Echo Hello*NThere prints literally — escape substitution depends on the executed program using ReadArgs()/ReadItem().

Variables and expansion

A variable reference starts with $ followed by alphanumerics (case-insensitive), or ${name} allowing /-separated hierarchical names. Local variables (in pr_LocalVars) take priority over global variables (files in the ENV: assign). Undefined variables are left unexpanded.

Prefix Behavior
$name expand the variable
$?name substitute 1 if defined, else 0
$??name 1/0 for global variables only
$!name binary expansion (may contain control chars); global only, must be quoted

Predefined / configuration variables

Set by the Shell:

  • process — the CLI task number (unique per Shell; useful for temp-file names).
  • RC — return code of the last command (compared against FailAt to abort scripts).
  • Result2 — the DOS error code (IoErr()) of the last failed command (printed by Why).

Read by the Shell to configure it (booleans accept on/1/yes/TRUE, case-insensitive):

  • VIEWER — program to display non-executable files.
  • echo — echo each command (script debugging).
  • debug — write all commands to the serial port (9600 8N1); captured to RAM:syslog if boot-menu logging is on.
  • oldredirect — V33-style redirection placement.
  • interactive — single-step trace scripts (Return/y run, Del/n skip, Esc/q stop).
  • simpleshell — disable TAB expansion (cooked-mode line input).
  • histsize — history size in lines.
  • histskipdups — don't store duplicate commands.

Back-tick and alias expansion

Back-ticks (` `command` `) run the enclosed command first and substitute its stdout (newlines become spaces) into the command line. V36 allowed one pair per line; V45 lifted the limit. The substituted text is itself re-parsed and may even contain operators — wrap in double quotes to suppress re-interpretation.

Aliases are local macros (Alias name definition). When a command name matches an alias, it is replaced by the definition; the token [] in the definition is replaced by the command's remaining arguments. An alias cannot expand itself twice on one line (recursion guard).

Command execution

After all expansion, the Shell locates the command: resident list first (if unquoted), then the path. If a located file is neither executable nor a script, the VIEWER variable names a program to display it (defaulting to MultiView). Argument parsing inside commands normally uses dos.library's ReadArgs()/ReadItem() (see dos.library Reference), which is what makes the Shell's quoting/*N conventions work consistently.

Creating and controlling shells

The Shell is itself a process started through dos.library. The functions in this section create new shells, execute scripts synchronously or asynchronously, identify shells, and overlay the current process with a single command.

SystemTagList / System / SystemTags — spawn a shell

LONG SystemTagList(STRPTR command, struct TagItem *tags);   /* V36 */
LONG System(STRPTR command, struct TagItem *tags);
LONG SystemTags(STRPTR command, ULONG tag1, ...);

All three share one entry point. command is a shell-script string; if non-NULL it is interpreted by the new shell and on exhausting it the shell terminates. If command is NULL, the shell reads from its standard input (which, combined with SYS_Asynch, is how NewShell works).

Unlike Execute(), command is not subject to Execute-style argument substitution; it is parsed as shell syntax. To use a temporary file or pipe to feed Execute, pass Execute … as command.

Inherited state. A new shell inherits from the caller's shell (if pr_CLI is non-ZERO): path, local variables, prompt, current directory, stack size. If the caller has no shell, the new shell starts with C: + cwd, no local variables, prompt "%N> ", cwd SYS:.

Streams. Defaults are pr_CIS and pr_COS of the caller. The same filehandle cannot be both input and output of the new shell. Set SYS_Output to ZERO to let dos.library derive an output stream from the input stream (via the "*" filename) — useful when command == NULL and SYS_Asynch is set.

Important tags (defined in dos/dostags.h):

Tag Meaning
SYS_Input BPTR to filehandle as the shell's input
SYS_InName (V47+) Path string; dos.library opens it for the new shell, then closes it on exit. Mutually exclusive to SYS_Input.
SYS_Output BPTR to filehandle as the shell's output; must differ from input
SYS_OutName (V47+) Path string; opens and auto-closes. Mutually exclusive to SYS_Output.
SYS_CmdStream (V47+) BPTR to filehandle from which the script is read, used when command == NULL
SYS_CmdName (V47+) Path string of a script file to interpret
SYS_Asynch Detach from the caller; return immediately
SYS_UserShell Name of a different shell resident segment (default "BootShell")
NP_StackSize, NP_ConsoleTask, … Subset of NP_ process-creation tags also recognised

Stream closure policy. If command == NULL and SYS_Asynch is not set, the implicit SYS_Output (or its clone from pr_COS) is not closed. If SYS_Asynch is set, the caller's pr_CIS and pr_COS are closed when the spawned shell exits (unless overridden). Beware — this is a footgun.

Execute — run a script synchronously

LONG Execute(STRPTR command, BPTR input, BPTR output);          /* classic */
LONG ExecuteTags(STRPTR command, struct TagItem *tags);          /* V47+ */

Pre-V47: synchronous only. The new shell reads from input (or pr_CIS if ZERO), writes to output (or pr_COS if ZERO), and the caller blocks until the shell exits.

V47+: same behaviour through ExecuteTags(), accepting the same tags as SystemTagList().

Unlike System(), the command string is subject to Execute-style argument substitution.

RunCommand — overlay the calling process with a command

LONG RunCommand(BPTR seglist, LONG stack, STRPTR cmd, LONG cmdlen);   /* classic */

Loads the seglist, adjusts the stack, installs cmd (length cmdlen) as the new input, then transfers control into the loaded program. The calling process is reused: the seglist becomes the calling process's program; no new process is created.

This is what the Shell uses internally to launch a command — the new program runs in the shell's own process, so it sees pr_CLI set.

CheckSignal — test signals without waiting

ULONG CheckSignal(ULONG mask);   /* V36 */

Tests whether any of the bits in mask are set in the current task's SigWait. Returns a mask of bits set; clears those bits in SigWait. Used by the Shell to check for break signals between commands.

FindCliProc / FindProc / MaxCli — locate shells

struct Process *FindCliProc(LONG clino);   /* V36 */
struct Process *FindProc(STRPTR name);      /* V36 */
LONG            MaxCli(void);               /* V36 */
  • FindCliProc(n) returns the shell whose CLI number is n (the same number printed by %N in the prompt).
  • FindProc(name) returns the process whose task name matches name.
  • MaxCli() returns the highest CLI number currently in use plus one (i.e., the count of shells ever launched this session).

DoShellMethod — call a shell-internal function

LONG DoShellMethod(struct TagItem *tags);   /* V47+ */

Internal entry point used by the Shell to expose services to itself; not generally useful to applications.

The CLI structure

Every shell process carries a struct CommandLineInterface (CLI), allocated via AllocDosObject(DOS_CLI, NULL). The Shell updates most fields itself; the public API gives controlled access:

Function Field exposed Notes
GetCurrentDirName(buf, len) cli_SetName Path printed by %S. Truncates without failing if too short.
SetCurrentDirName(name) cli_SetName Updates only the string — must be paired with CurrentDir() to keep the actual cwd consistent.
GetProgramName(buf, len) cli_CommandName argv[0] source.
SetProgramName(name) cli_CommandName Updates without checking; use with care.
GetPrompt(buf, len) cli_Prompt Un-expanded template (%S, %N, %R, %%).
SetPrompt(name) cli_Prompt Updates the template only.
Cli() entire CommandLineInterface * Returns NULL if the process has no CLI.

The CLI's buffer sizes are unfortunately limited by hard-coded legacy values; AllocDosObject() tags ADO_PromptLen, ADO_CommNameLen, ADO_CommFileLen, ADO_DirLen are accepted but currently ignored.

Prompt tokens

Token Substituted with
%S Current directory path (GetCurrentDirName())
%N CLI number (pr_TaskNum)
%R Return code of last command (cli_ReturnCode)
%% Literal %

Variable expansion ($name) and back-tick expansion also work inside the prompt. % introduced by expanded content is treated as a literal.

Shell variables — GetVar, SetVar, FindVar, DeleteVar

Local shell variables live in the process's pr_LocalVars linked list as struct LocalVar. Global variables live as files under the ENV: assign (typically backed by the RAM-Handler).

LONG  GetVar(STRPTR name, STRPTR buffer, LONG size, ULONG flags);    /* V36 */
BOOL  SetVar(STRPTR name, STRPTR buffer, LONG size, ULONG flags);    /* V36 */
struct LocalVar *FindVar(STRPTR name, ULONG type);                    /* V36 */
LONG  DeleteVar(STRPTR name, ULONG flags);                            /* V36 */
  • Names are case-insensitive and may contain / to form hierarchies (which map to sub-directories of ENV: for globals).
  • GetVar returns the length of what was written, or -1 with IoErr() set on error. IoErr() on success is set to the full variable length (before truncation).
  • SetVar with buffer == NULL deletes the variable (equivalent to DeleteVar).
  • size == -1 means buffer is a NUL-terminated string (the trailing NUL is not part of the value).
  • LV_VAR / LV_ALIAS (in flags's low byte) selects variable vs alias (aliases are always local).
  • GVF_GLOBAL_ONLY / GVF_LOCAL_ONLY selects where to look.
  • GVF_BINARY_VAR allows embedding of NUL / LF bytes (otherwise the value is truncated at the first).
  • GVF_DONT_NULL_TERM (with GVF_BINARY_VAR) suppresses NUL termination.
  • GVF_SAVE_VAR mirrors the change to ENVARC: (used for persistence across reboots).

Race condition. V47 has a known TOCTOU between writing the new value and truncating the file. Concurrent SetVar() on the same global variable is unsafe.

ReadArgs — template-driven argument parsing

ReadArgs() is the standard argument parser for Shell-launched programs. It reads the calling process's command line (or a custom source via struct RDArgs), matches it against a template, and fills a LONG * array with the parsed values.

struct RDArgs *ReadArgs(STRPTR template, LONG *array, struct RDArgs *rdargs);   /* V36 */
void           FreeArgs(struct RDArgs *rdargs);

The template is a comma-separated list of keyword[=abbrev]/option entries. Options are:

Option Meaning
/S Boolean switch. The array entry is 0 or non-zero.
/T Boolean switch with explicit argument (On/Y → DOSTRUE, Off/N → DOSFALSE).
/K Keyword must appear on the command line; not matched by position.
/A Argument is required.
/N Numeric; array entry holds LONG * (NULL if not present).
/M Multi-value; array entry holds UBYTE ** (NULL-terminated array of UBYTE *). At most one per template.
/F Rest of line, even if it contains keywords.

A keyword with no options is an optional positional string argument (UBYTE *, or NULL if not present). Example: "FILE/A,PORT/K/N,QUIET/S".

Argument-to-keyword matching. Keywords are matched in two ways: by NAME value (keyword + space or = + value), or positionally for non-/K slots, left to right. /A slots are filled last from the end of any /M slot — this is how Copy FROM/A/M, TO/A works.

The result array. array must be zero-initialised and large enough for one entry per keyword in the template. After ReadArgs(), the slots contain either LONG, UBYTE *, or UBYTE ** depending on the options.

Customising the source. Allocate an RDArgs via AllocDosObject(DOS_RDARGS, NULL) and fill its RDA_Source.CS_Buffer, CS_Length, CS_CurChr (or CS_ISource for a BPTR filehandle). Set RDA_Buffer / RDA_BufSiz to provide an output buffer.

Flags (dos/readargs.h):

  • RDAF_NOALLOC — disable internal buffer growth; rely on the supplied RDA_Buffer.
  • RDAF_NOPROMPT — treat a literal ? as an argument, not a help request.

ReadArgs() returns NULL on failure with IoErr() set; possible values include ERROR_NO_FREE_STORE, ERROR_KEY_NEEDS_ARG, ERROR_LINE_TOO_LONG, ERROR_REQUIRED_ARG_MISSING, ERROR_BAD_NUMBER, ERROR_BAD_TEMPLATE, ERROR_UNMATCHED_QUOTES.

Always call FreeArgs(rdargs) — whether rdargs was passed in or allocated internally. FreeArgs() invalidates the contents of array; zero it again if reused.

Resident segments

The Shell and other system components are stored as resident segments in dos.library's resident list (DosInfo.di_NetHand). Tools like Resident and Avail list the table.

struct Resident *FindSegment(STRPTR name, struct Resident *prev, ULONG type);   /* V36 */
APTR             AddSegment(STRPTR name, BPTR seglist, LONG sysversion);       /* V36 */
BOOL             RemSegment(struct Resident *segment);                          /* V36 */
  • FindSegment(NULL, prev, type) iterates when prev is the previous return value; pass NULL for prev to start. type is RTF_COLDSTART, RTF_AFTERDOS, etc., or NULL to ignore.
  • AddSegment(name, seglist, sysversion) inserts a new resident; if a resident of the same name and equal-or-lower priority exists, it is replaced and the new one returned.
  • RemSegment(segment) removes the named resident. The seglist is not unloaded — manage its memory yourself.

The legacy rt_MatchWord/rt_MatchTag mechanism (RT_MATCHWORD, RT_MATCHTAG) is how exec locates residents during InitResident(). AmigaDOS extends the tag list with RTC_MATCHWORD and additional DOS-specific tags. The structure itself is defined in exec/resident.h.

Custom shells

A custom shell is a resident segment registered under a chosen name (then used by passing SYS_UserShell). To qualify as a shell, it must at minimum:

  1. Accept the startup packet described in Ch. 13.1.2 / Ch. 15.7, with the initial input filehandle, output filehandle, and pr_CurrentDir.
  2. Provide a struct CommandLineInterface (use AllocDosObject(DOS_CLI, NULL)) and set pr_CLI of the process.
  3. Parse and execute commands. The standard Shell uses the syntax in this article as its language.
  4. Cleanly release resources on EndCLI (or its moral equivalent) and exit.

A minimal Shell skeleton:

struct CommandLineInterface *cli;

void MyShell(void)
{
    struct DosPacket *pkt;
    cli = (struct CommandLineInterface *)
        AllocDosObject(DOS_CLI, NULL);
    SysBase->ThisTask->pr_CLI = MKBADDR(cli);

    /* Initialize cli_Prompt, cli_CommandFile, cli_SetName ... */
    cli->cli_DefaultStack = 4096;
    cli->cli_Prompt = "%N> ";

    /* Read commands from pr_CIS, parse, dispatch, repeat. */
    /* On exit, call FreeDosObject(DOS_CLI, BADDR(SysBase->ThisTask->pr_CLI)); */
    /* And reply the startup packet: ReplyPkt(startup_pkt, DOSTRUE, 0); */
}

See Also


Sources: Thomas Richter, "ROM Kernel Reference Manual: AmigaDOS" (First Edition, 2024), Chapter 15 — The AmigaDOS Shell. Raw: raw/rkm/rkm-dos-book.md Updated: 2026-10-02