36 KiB
CLI and Shell — Command Interpreter, Scripting, and Cookbook
Overview
AmigaDOS provides two command interpreters: the original CLI (Command Line Interface, OS 1.0–1.3) and its successor, the Shell (OS 2.0+). Both read text commands from the user or from script files, launch programs as separate Exec Processes, and provide I/O redirection, environment variables, and control-flow primitives (IF/ELSE/LAB/SKIP). Unlike Unix shells which are separate user-space programs, the Amiga Shell is tightly integrated with dos.library — it calls ReadArgs() for argument parsing, SystemTagList() for sub-process creation, and Execute() for script file interpretation. This integration means Shell scripting shares the exact same argument syntax as every AmigaDOS command and every C program that uses ReadArgs().
The Shell inherited its design from the BCPL CLI of the original AmigaDOS (written at MetaComCo in BCPL, running on the CAOS kernel). When AmigaDOS was rewritten in C for OS 2.0, the CLI evolved into the Shell — gaining command history, line editing, pipes, and resident commands — while retaining full backward compatibility with CLI scripts.
Note
Throughout this article, "Shell" refers to the OS 2.0+ interpreter and "CLI" refers to the OS 1.x predecessor. In practice, Amiga users use the terms interchangeably, and
NewShellis the command that opens a Shell window on OS 2.0+.
Architecture — How Shell Creates Processes
graph TB
User[User Types Command] --> ReadArgs[ReadArgs - Parse Template]
ReadArgs --> Resolve[Resolve command:<br/>1. Built-in?<br/>2. Resident?<br/>3. C:Path search<br/>4. Current dir]
Resolve --> Binary{Runnable Binary?}
Binary -->|No| Script[Execute as Script<br/>via dos.library/Execute]
Binary -->|Yes| Load[LoadSeg + CreateProc]
Script --> Load
Load --> Wait{Wait for<br/>completion?}
Wait -->|Run or &| Return[Return prompt immediately<br/>child runs in background]
Wait -->|Normal| Block[Shell blocks on<br/>WaitPort/child MsgPort]
Block --> RC[Read return code<br/>set $RC variable]
RC --> Prompt[Display prompt]
Key principle: Every command — built-in or external — runs as a separate Exec Process with its own message port, stack, and input/output filehandles. The Shell inherits the parent Shell's Input() and Output() filehandles, which are typically connected to the CON: console window. This is fundamentally different from Unix where built-in commands run within the shell process — on AmigaOS, CD and Echo are internal to the Shell, but Dir, List, and Copy are separate programs launched via SystemTagList().
Shell vs CLI — Evolution
| Feature | CLI (OS 1.0–1.3) | Shell (OS 2.0+) |
|---|---|---|
| Binary | CLI command in ROM |
Shell-Seg in L: directory |
| Open command | NewCLI |
NewShell |
| Command history | No | Yes (↑/↓ arrows, CON: history buffer) |
| Line editing | Backspace only | Full: cursor keys, delete word, insert/overtype |
| Pipes | No | Yes (` |
| Wildcard expansion | Manual #? in command code |
Automatic in many built-in commands |
| Resident commands | No | Yes (Resident command keeps binary in RAM) |
| Background execution | Run command only |
Run + & suffix + RunBack |
| Return code ($RC) | Limited | Full — stored after every command |
| Script control | IF/ELSE/SKIP/LAB/QUIT | Same, plus WARN/ERROR/FAIL interactive handling |
| Aliases | No | Yes (Alias command, local and global) |
| Prompt customization | Fixed format | Prompt command with escape sequences |
| Path resolution | C: only for commands | Full search path via Path command |
The BCPL to C Transition
The original CLI was written in BCPL (the precursor to C) by MetaComCo in 1985. BCPL's word-oriented memory model (no byte addressing) and lack of standard string handling produced the infamous BPTR (Byte Pointer — actually a word pointer shifted left by 1) and BSTR (byte-length-prefixed string) conventions that still persist in dos.library today. The C rewrite for OS 2.0 preserved these conventions for backward compatibility but replaced the CLI binary with Shell-Seg, a relocatable code segment loaded from L:Shell-Seg at boot.
I/O Redirection
AmigaDOS supports a complete set of redirection operators, all handled by the Shell before the child process is created (the child sees only its Input() and Output() filehandles):
| Operator | Meaning | Example |
|---|---|---|
< |
Redirect stdin from file | Type <RAM:data.txt |
> |
Redirect stdout to file (overwrite) | Dir >RAM:listing.txt SYS: |
>> |
Redirect stdout to file (append) | Echo "entry" >>RAM:log.txt |
*> |
Redirect stderr to file (OS 2.0+) | command *>RAM:errors.txt |
*>> |
Append stderr to file | command *>>RAM:errors.txt |
>NIL: |
Discard stdout | Copy >NIL: file1 file2 |
*>NIL: |
Discard stderr | command *>NIL: |
<> |
Redirect both stdout and stderr (3.2+) | command <>RAM:all.txt |
Note
Redirection operators are processed left to right.
command >out.txt *>err.txtsends stdout toout.txtand stderr toerr.txt. The*>operator is unique to AmigaDOS — it comes from the BCPL convention where*represents the "alternate" or error output stream.
NIL: — The Bit Bucket
NIL: is a special DOS device that discards all writes and returns EOF on reads. It is not a file — it is handled internally by dos.library without touching any filesystem. Use it to suppress output without creating temporary files:
; Suppress all output from a noisy command:
Copy >NIL: *>NIL: SYS:C/#? RAM: ALL CLONE QUIET
Pipes
Syntax
; Pipe stdout of cmd1 to stdin of cmd2:
List SYS:C | Sort
; Multi-stage pipeline:
List SYS:C ALL | Sort | More
; Pipe both stdout and stderr:
command |& Filter
Internal Implementation
Pipes are NOT true Unix-style byte streams. The Shell implements them as follows:
- Create a temporary file in
T:(which is typically assigned toRAM:T) - Run the first command with stdout redirected to the temp file:
cmd1 >T:pipe.XXXXXX - Wait for the first command to complete (this is the key difference from Unix — Amiga pipes are sequential, not concurrent)
- Run the second command with stdin redirected from the temp file:
cmd2 <T:pipe.XXXXXX - Delete the temp file when done
This has important implications:
| Property | Unix Pipe | Amiga Pipe |
|---|---|---|
| Concurrency | Producer and consumer run simultaneously | Producer completes before consumer starts |
| Memory usage | Fixed kernel buffer (~64 KB) | Temp file — can be MBs in RAM |
| Back-pressure | Producer blocks when buffer full | No back-pressure — producer runs to completion |
| Failure isolation | SIGPIPE if consumer exits early | Consumer's failure does not affect producer |
| Disk I/O | None (in-memory) | If T: is on disk, heavy disk I/O |
Warning
Because Amiga pipes are sequential, you cannot write interactive pipelines like
tail -f log | grep ERROR. The first command must exit before the second starts. If the first command runs forever, the second never executes and the temp file grows until RAM is exhausted.
PIPE: Device (Alternative)
For concurrent, Unix-like pipes, OS 2.0+ provides the PIPE: device:
; Create a named pipe:
Run >PIPE:mypipe MyProducer
MyConsumer <PIPE:mypipe
PIPE: is a RAM-based handler that implements a true circular buffer between producer and consumer. Unlike Shell pipes, PIPE: allows concurrent execution. The buffer defaults to 4 KB and blocks the producer when full.
Script Execution
Running Scripts
Scripts are plain text files containing AmigaDOS commands. Three ways to execute:
; Method 1: Execute command (always works):
Execute S:MyScript
; Method 2: Set script bit and run by name:
Protect S:MyScript +s
S:MyScript
; Method 3: Dot command (interactive, runs in current Shell):
. S:MyScript
Warning
Method 3 (
.) runs the script within the current Shell process — not as a sub-process. If the script callsEndCLI, your Shell window closes. If it callsFailAt 30, that error threshold is now set in your interactive Shell. Use.only for scripts that set environment variables or aliases that you want to persist.
The PORT Argument
Scripts launched by icon double-click or via Execute receive a message port name as their first argument (accessible as <scriptname>.PORT). This is how Workbench communicates with running scripts:
; Script checks if it was launched with a PORT:
.key PORT
.bra {
.ket }
If "{PORT}" EQ ""
Echo "Running interactively"
Else
Echo "Launched from Workbench, PORT = {PORT}"
EndIf
Note
When launching scripts from other scripts, pass an empty string as the port argument to avoid confusion:
Execute S:MyScript ""
Script Control Structures
IF / ELSE / ENDIF
; IF with EXISTS, VAL, WARN, ERROR, FAIL:
IF EXISTS SYS:Libs/68040.library
Echo "68040 detected"
ELSE IF EXISTS SYS:Libs/68881.library
Echo "68881 FPU detected"
ELSE
Echo "No accelerator"
ENDIF
; String comparison:
IF "$CPU" EQ "68060"
Echo "060 rules"
ENDIF
; Numeric comparison:
IF $RC GT 5 VAL
Echo "Return code > 5"
ENDIF
IF tests available:
| Test | Meaning | Example |
|---|---|---|
EXISTS |
File or directory exists | IF EXISTS SYS:Prefs |
WARN |
Last return code ≥ 5 (warning) | IF WARN |
ERROR |
Last return code ≥ 10 (error) | IF ERROR |
FAIL |
Last return code ≥ 20 (failure) | IF FAIL |
EQ / GT / GE / NOT |
String comparison | IF "$A" EQ "hello" |
VAL |
Treat arguments as numeric (precedes EQ/GT/GE) | IF $RC GT 10 VAL |
LAB / SKIP — Loops
; Infinite loop with EXIT condition:
LAB loop
Ask "Continue? (Y/N)"
IF WARN
QUIT
ENDIF
Echo "Looping..."
SKIP loop BACK
; Counted loop:
Set count 1
LAB countloop
Echo "Iteration $count"
Set count `Evaluate $count + 1`
IF $count GT 10 VAL
QUIT
ENDIF
SKIP countloop BACK
SKIP with BACK jumps backward (creates a loop). Without BACK, it jumps forward (like GOTO). The LAB target name is case-insensitive.
QUIT — Exit Script
; QUIT with return code:
QUIT 5 ; Exit script, return code 5 (warning)
QUIT 20 ; Exit script, return code 20 (failure)
QUIT ; Exit with current $RC
FAILAT — Error Threshold
FailAt 21 ; Tolerate return codes up to 20
Copy SYS:Missing RAM:
IF WARN ; True if return code 5-9
Echo "Copy had minor issues"
ENDIF
IF FAIL ; True only if return code ≥ 21
Echo "Copy failed"
QUIT 20
ENDIF
The default FAILAT value is 10. FailAt 21 is standard in Startup-Sequence to prevent a single missing file from aborting the entire boot. The maximum useful value is 30 (return codes above 30 are Guru-level failures that cannot be caught).
ASK — Interactive Prompts
; Simple yes/no:
Ask "Proceed with installation?"
IF WARN
Echo "Installation cancelled"
QUIT 5
ENDIF
; ASK with default answer:
Ask "Format disk? (N)"
The user presses Y (return code 0, no warning) or N/anything else (return code 5, warning). IF WARN catches a "No" answer.
REQUESTCHOICE — GUI Prompt
; OS 2.0+: Show a requester dialog from a script:
RequestChoice "Title" "Select action" "Save" "Discard" "Cancel"
IF $RC EQ 0
Echo "Save selected"
ELSE IF $RC EQ 1
Echo "Discard selected"
ENDIF
Returns 0 for the first button, 1 for the second, etc. Useful for scripts that need a quick GUI without writing a full Intuition application.
Script Cookbook
1. Safe Copy with Verification
.key SRC/A,DST/A
.bra {
.ket }
FailAt 21
If NOT EXISTS {SRC}
Echo "Source {SRC} not found"
Quit 20
EndIf
Copy {SRC} {DST} CLONE
If WARN
Echo "Copy had warnings — check destination"
Quit 10
EndIf
Echo "{SRC} → {DST} OK"
2. Conditional Hardware Detection
; Detect accelerator and load appropriate libraries:
If EXISTS SYS:Libs/68060.library
Echo "68060 detected"
SetEnv CPU 68060
Else If EXISTS SYS:Libs/68040.library
Echo "68040 detected"
SetEnv CPU 68040
Else
SetEnv CPU 68000
EndIf
3. Backup with Timestamp
; Create dated backup of a file:
.key FILE/A
.bra {
.ket }
Set date `Date`
Copy {FILE} RAM:Backup/{FILE}.$date
If NOT WARN
Echo "Backed up to RAM:Backup/{FILE}.$date"
Else
Echo "Backup failed!"
EndIf
4. Multi-Volume Copy with Prompt
.key SRC/A,DST/A
.bra {
.ket }
FailAt 21
Lab retry
Copy {SRC} {DST} ALL CLONE
If ERROR
Ask "Disk full or error. Retry?"
If NOT WARN
Skip retry BACK
EndIf
EndIf
5. Loop Over Files with Pattern Matching
; Process all .info files in a directory:
List SYS:Prefs/#?.info LFORMAT="Process %s" >T:filelist
Execute T:filelist
Delete T:filelist QUIET
6. Wait for a Volume to Appear
; Wait up to 30 seconds for a disk to be inserted:
Lab waitdisk
Wait 2
If NOT EXISTS DF0:Disk.info
Set count `Evaluate $count + 2`
If $count GT 30 VAL
Echo "Timeout waiting for disk"
Quit 20
EndIf
Skip waitdisk BACK
EndIf
Echo "Disk found"
7. Toggle a Feature
; Toggle DF0: boot priority check:
If EXISTS ENVARC:NoDiskBoot
Delete ENVARC:NoDiskBoot QUIET
Echo "Disk boot will be checked at startup"
Else
Echo "" >ENVARC:NoDiskBoot
Echo "Disk boot will NOT be checked"
EndIf
ReadArgs — Argument Parsing
ReadArgs() is the standard AmigaDOS argument parser used by both C programs and the Shell itself. It implements keyword-based argument passing with type validation:
/* From dos/rdargs.h — NDK 3.9 */
struct RDArgs *ReadArgs(CONST_STRPTR template,
LONG *array,
struct RDArgs *rdargs);
void FreeArgs(struct RDArgs *args);
Template Format
| Qualifier | Meaning | What ReadArgs Returns | Example |
|---|---|---|---|
/A |
Required argument | Error if not provided | FROM/A |
/K |
Keyword (must use KEYWORD=value) |
String pointer | PUBSCREEN/K |
/S |
Switch (boolean flag) | Non-zero if present, zero if absent | ALL/S |
/N |
Numeric value (base-10) | Pointer to LONG containing value | BUF/N |
/M |
Multiple values (array) | Pointer to array, terminated by NULL | FILES/M |
/F |
Rest of line (everything after keyword) | String pointer to remainder | CMD/F |
= |
Alias (make one name an alias for another) | Same slot | FILE=FROM/A |
/T |
Toggle (set to opposite of default) | Toggles value | CASE/T |
Complete Example
/* Command template: "COPY FROM/A,TO/A,ALL/S,CLONE/S,BUF/N"
* Usage: Copy FROM DH0:file TO RAM: ALL CLONE BUF 4096
*/
LONG args[5] = {0};
struct RDArgs *rd = ReadArgs("FROM/A,TO/A,ALL/S,CLONE/S,BUF/N",
args, NULL);
if (rd) {
STRPTR from = (STRPTR)args[0]; /* FROM/A — always present */
STRPTR to = (STRPTR)args[1]; /* TO/A — always present */
BOOL all = (BOOL)args[2]; /* ALL/S — TRUE if present */
BOOL clone = (BOOL)args[3]; /* CLONE/S — TRUE if present */
LONG *buf = (LONG *)args[4]; /* BUF/N — pointer if given, NULL if not */
printf("Copy %s to %s, all=%d clone=%d buf=%ld\n",
from, to, all, clone, buf ? *buf : 0);
FreeArgs(rd);
}
How the Shell Uses ReadArgs
When the user types Copy FROM DH0:file TO RAM: ALL CLONE, the Shell:
- Searches
C:Copyand finds the binary - Reads the binary's resident template string (embedded in the executable at a known offset, or stored in the resident list if the command is Resident)
- Calls
ReadArgs("FROM/A,TO/A,ALL/S,CLONE/S,BUF/N", array, NULL)with the user's argument string ReadArgs()parses the string against the template, fills the array- The Shell passes the filled array to the child process via the WBenchMsg or via registers (implementation-specific)
- The child process receives already-parsed arguments — it never sees the raw command line
This differs from Unix where each program parses its own argv[]. On AmigaOS, ReadArgs() centralizes parsing, ensuring consistent behavior across all commands.
Resident Commands
Making a command Resident keeps its binary in memory, eliminating disk access on every invocation:
; Make resident:
Resident C:Dir PURE ADD
Resident C:List PURE ADD
Resident C:Copy PURE ADD
; List resident commands:
Resident
; Remove a resident:
Resident C:Dir REMOVE
Requirements for Resident
| Requirement | Why |
|---|---|
| PURE | Binary must be position-independent — no self-modifying code, no writable data in code section. Compiled with -resident flag or equivalent. |
| Single segment | Multi-segment executables cannot be made resident (the resident system stores a single contiguous hunk). |
| No global constructors | The binary's init code runs only on first load. If it allocates memory or opens libraries in constructors, those must be guarded against re-execution. |
Warning
A resident command that is NOT truly PURE (writes to its own code section) will work once, then produce corrupted results on subsequent invocations. The corruption is silent — no error, just wrong output. Always test a command with
Resident ... PUREand run it 3+ times to verify correct behavior before adding it toS:Shell-Startup.
Benefits and Costs
| Benefit | Cost |
|---|---|
| Instant command execution (no disk I/O) | Consumes RAM (~size of binary, permanently) |
| Reduces floppy swapping on floppy-only systems | Memory fragmentation from many small resident segments |
| Commands available even if C: is not assigned | Cannot be updated without reboot (or REMOVE + ADD) |
On a stock A500 with 1 MB RAM, keeping 10 common commands resident consumes ~50 KB — a reasonable trade-off for eliminating floppy access during script execution.
Shell Environment
Startup File Chain
The following files execute in order, each building on the previous:
| File | When Executed | Purpose |
|---|---|---|
S:Startup-Sequence |
At system boot (after dos.library init) |
Core system configuration: assigns, mounts, SetPatch, IPrefs, LoadWB |
S:User-Startup |
Called by Startup-Sequence (before LoadWB) | User customizations: extra assigns, resident commands, startup programs |
S:Shell-Startup |
Each time a NEW Shell window opens | Per-Shell setup: aliases, prompt, local variables, path |
Note
Do NOT modify
S:Startup-Sequencedirectly. Add customizations toS:User-Startupinstead. IfS:User-Startupdoesn't exist, create it — the defaultStartup-Sequencechecks for it withIF EXISTS S:User-Startupand executes it if present.
Local vs Global Variables
| Scope | Command | Storage | Example |
|---|---|---|---|
| Local | Set |
In Shell's private memory — lost when Shell closes | Set myvar hello |
| Global (volatile) | SetEnv |
ENV: (RAM) — shared across all Shells, lost on reboot |
SetEnv EDITOR C:ED |
| Global (persistent) | SetEnv + Copy ENV: ENVARC: |
ENVARC: (disk) — survives reboot |
SetEnv SAVE EDITOR C:ED |
; Set a persistent global variable:
SetEnv SAVE MYAPP_DATA DH1:MyApp/Data
; Reference in any Shell:
Echo $MYAPP_DATA ; Shows "DH1:MyApp/Data"
; Override locally:
Set MYAPP_DATA RAM:Temp
Echo $MYAPP_DATA ; Shows "RAM:Temp" (local overrides global)
; Remove local override:
Unset MYAPP_DATA
Echo $MYAPP_DATA ; Shows "DH1:MyApp/Data" (back to global)
Aliases
; Local alias (this Shell only):
Alias ll "List LFORMAT=\\"%M %L %N\""
; Global alias (add to S:Shell-Startup):
Alias x "Execute"
Alias .. "CD /"
Alias ? "Which"
Prompt Customization
; Default: 1.Workbench:>
; Show return code of last command:
Prompt "%R.%S> "
; Result: 0.Workbench:>
; Show only current directory:
Prompt "%S> "
; Result: Workbench:>
; Minimal prompt:
Prompt "> "
; Result: >
; Multi-line prompt with escape codes:
Prompt "*E[33m%S*E[0m*N$ "
; Result: (directory in blue, newline, dollar sign)
Prompt format codes:
| Code | Meaning |
|---|---|
%S |
Current directory |
%N |
Process number |
%R |
Return code of last command |
%E |
$RC as text (OK/WARN/ERROR/FAIL) |
*E |
Escape character (for ANSI sequences) |
*N |
Newline |
Console Window Customization
Shell windows are CON: console windows. You can control their appearance via the Shell icon's Tool Types or the NewShell command:
; Open a custom Shell window from command line:
NewShell "CON:0/10/640/200/My Shell/CLOSE/SCREEN=Workbench"
; Shell icon Tool Type:
WINDOW=CON:100/50/600/400/Custom/CLOSE/AUTO/SCREEN=Workbench
| CON: Option | Meaning |
|---|---|
CLOSE |
Window has close gadget |
AUTO |
Auto-scroll output |
SCREEN=<name> |
Open on named public screen |
BACKDROP |
Window is a backdrop (no depth arrangement) |
NOBORDER |
No window border |
SIMPLE |
Simple refresh (faster, no smart refresh) |
SMART |
Smart refresh (saves obscured content) |
WAIT |
Window waits for user to close it after command exits |
ANSI Escape Sequences
The Shell console supports ANSI X3.64 escape sequences for text styling:
; Print "Error" in red (CSI 31m = red foreground):
Echo "*E[31mError*E[0m: something went wrong"
; Bold text:
Echo "*E[1mImportant*E[22m notice"
; Clear screen:
Echo "*Ec"
Common sequences: color (30–37 foreground, 40–47 background), bold (1), underline (4), reverse video (7), reset (0), clear screen (*Ec).
Built-in Shell Commands
These commands execute within the Shell process — they do NOT create sub-processes:
| Command | Function | Example |
|---|---|---|
CD |
Change current directory | CD SYS:Prefs |
Echo |
Print text to stdout | Echo "Hello $USER" |
If/Else/EndIf |
Conditional execution | IF EXISTS file ... ENDIF |
Skip/Lab |
Jump / loop | LAB loop ... SKIP loop BACK |
Quit |
Exit script with return code | Quit 10 |
FailAt |
Set error threshold | FailAt 21 |
Set/Unset |
Local variables | Set myvar value |
SetEnv/GetEnv |
Global environment variables | SetEnv SAVE EDITOR C:ED |
Alias |
Command aliases | Alias ll List |
Path |
Manage command search path | Path C: SYS:Utilities ADD |
Prompt |
Set Shell prompt format | Prompt "%S> " |
Protect |
Set file protection bits | Protect file rwed |
Run |
Execute command in background | Run C:Dir SYS: |
Execute |
Run script file | Execute S:Script |
EndCLI |
Close this Shell | EndCLI |
NewCLI/NewShell |
Open new Shell window | NewShell |
Stack |
Display or set stack size | Stack 20000 |
Why |
Explain last error code | Why |
Which |
Show which binary a command resolves to | Which Dir |
Wait |
Pause for specified seconds | Wait 5 |
Ask |
Prompt user for Y/N | Ask "Continue?" |
RequestChoice |
GUI requester with buttons | RequestChoice "Title" "Body" "OK" "Cancel" |
Status |
Show last command return info | Status |
Date |
Display or set system date/time | Date |
Best Practices
- Always quote strings with spaces —
Echo "Hello World"notEcho Hello World - Put
FailAt 21at the top of every script — prevents one failed command from aborting the entire script - Test commands interactively before adding to scripts — syntax errors in
S:Startup-Sequencecan prevent booting - Use
>NIL:to suppress output in scripts — keeps the console clean and avoids fillingRAM:with temp output - Check return codes with
IF WARN/IF ERROR— never assume a command succeeded; AmigaDOS commands are chatty about failures - Use full paths in startup scripts — during boot,
C:may not be in the search path yet; writeC:Copynot justCopy - Make heavily-used commands Resident before they're needed — put
Resident C:Dir PURE ADDinS:User-Startupso it's ready before scripts run - Add
QUIETto Copy/Delete in scripts — suppresses per-file progress output - Use
Executewith an empty string port argument when calling scripts from scripts —Execute S:SubScript "" - Do not use
.(dot) for general scripting — it runs in the current Shell and can corrupt your interactive environment
Antipatterns
1. The Naked WARN
Bad:
Copy SYS:Important RAM:
IF WARN
Echo "Something went wrong"
EndIf
Good:
Copy SYS:Important RAM:
IF WARN
Echo "Copy of Important had warnings — check destination"
Ask "Continue anyway?"
IF WARN
Quit 10
EndIf
EndIf
Why it breaks: IF WARN fires for return codes 5–9 (warnings). The Copy may have partially succeeded — reporting "Something went wrong" gives no actionable information. Always provide context and offer the user a choice when possible.
2. The Phantom PORT
Bad:
; Top-level script launched by icon double-click
Echo "Starting backup..."
Copy DH0: DH1: ALL CLONE
Good:
.key PORT
.bra {
.ket }
If NOT "{PORT}" EQ ""
Echo "Running from Workbench"
EndIf
Echo "Starting backup..."
Copy DH0: DH1: ALL CLONE
Why it breaks: When Workbench launches a script (via icon double-click), it passes a message port name as the first argument. If the script uses .key with no PORT argument, the port name becomes the value of the first .key parameter — or worse, causes argument parsing to fail. Always declare .key PORT in scripts intended for icon launch.
3. The Dot-Eater
Bad:
; In S:User-Startup:
. S:MyAliases
. S:MyAssigns
Good:
; In S:User-Startup:
Execute S:MyAliases
Execute S:MyAssigns
Why it breaks: The dot command executes the script in the current Shell. If S:MyAliases contains EndCLI (even as a stray commented-out line), the entire boot sequence terminates. If it calls FailAt 30, the rest of User-Startup and Startup-Sequence continues under that threshold. Use Execute for robustness — it creates a separate process that cannot damage the parent Shell.
4. The Infinite Pipe
Bad:
; This never returns!
Run >T:log MyServer
MyClient <T:log
Good:
; Use PIPE: for concurrent producer/consumer
Run >PIPE:mylog MyServer
MyClient <PIPE:mylog
Why it breaks: Amiga Shell pipes are sequential — the first command must finish before the second starts. Run >T:log MyServer detaches the server, so it never "finishes" from the Shell's perspective. The pipe never completes. Use PIPE: for concurrent producer-consumer patterns.
Pitfalls
1. Resident Command Corruption
Commands made Resident that are not truly PURE will corrupt themselves on second invocation. The classic example: a C program compiled with BSS (zero-initialized globals) that actually writes to BSS during first execution. On second invocation, those BSS values are whatever the first invocation left them as.
Symptom: Dir works correctly the first time, shows garbled output the second time.
Fix: Recompile with -resident (SAS/C) or ensure all global writes go through AllocMem-allocated buffers, not BSS. Test commands 5+ times before making them resident in Shell-Startup.
2. FAILAT Won't Catch Division by Zero
FailAt controls whether the Shell continues after a command returns a non-zero return code. It does NOT prevent the 68000 exception that results from a division by zero (which triggers a Guru Meditation). The Shell cannot intercept hardware exceptions — that's the job of exec.library's trap handling.
Symptom: Script with FailAt 999 still crashes on division by zero.
Fix: FailAt handles DOS return codes only (0–30). Protect against arithmetic errors in the program itself, not in the script.
3. Pipe Temp File Exhausts RAM
Since Shell pipes create a temporary file in T: (usually RAM:T), piping large data streams fills RAM:
; DANGER: List ALL on a large volume → temp file may be MBs
List SYS: ALL | Sort
On a 1 MB A500, a List ALL of a full hard drive can produce a temp file larger than available RAM, causing the system to freeze.
Fix: For large data, use PIPE: with a known buffer size, or redirect to a disk-based T::
Assign T: DH1:T
List SYS: ALL | Sort ; Temp file now on DH1: (disk)
4. CON: Window Options Case Sensitivity
CON: window specifiers are case-sensitive. CLOSE enables the close gadget; Close is silently ignored. This is a frequent source of "my Shell window won't close" bugs.
; WRONG — lowercase 'close' is ignored:
NewShell "CON:0/10/640/200/My Shell/close"
; CORRECT:
NewShell "CON:0/10/640/200/My Shell/CLOSE"
Use Cases
Real-World Applications
| Scenario | Shell Feature | Example |
|---|---|---|
| System boot | Startup-Sequence script | Every Amiga — configures assigns, runs IPrefs, loads Workbench |
| WHLoad slave launchers | Script with icon + PORT | WHDLoad game icons launch .info-associated scripts that set up assigns before running the slave |
| Demo scene | Execute + ASK | Demo disks with menu scripts: "Press Y for main demo, N for credits" |
| Software installers | RequestChoice + GUI |
Commodore's Installer utility is script-driven; many third-party installers are pure Shell scripts |
| Build systems | Execute from Makefile |
Amiga Make can call Shell scripts for pre/post build steps |
| ARexx → Shell bridge | ADDRESS COMMAND |
ARexx scripts execute Shell commands via the command address — ReadArgs ensures consistent parsing |
| Network boot | Custom Startup-Sequence | SANA-II network-booted systems use Shell scripts to mount remote volumes before Workbench starts |
| Emergency recovery | Boot-with-no-startup | Holding both mouse buttons → "Boot With No Startup-Sequence" drops to a raw Shell for disk repair |
Historical Context
BCPL Heritage
The Amiga Shell's design is a direct descendant of the TRIPOS operating system (developed at Cambridge University, 1978), which was written in BCPL. Commodore licensed TRIPOS from MetaComCo in 1985 to provide the Amiga's disk operating system. BCPL's influence persists in:
- BPTR (Byte Pointer): actually a word pointer — in BCPL, the machine word IS the addressable unit
- BSTR (Byte String): length-prefixed strings (a leading byte stores the length) — BCPL had no native string type
*for stderr: BCPL used*as the standard error stream prefixExecuteand script files: BCPL's CLI had the same concept of text-based command files
Competitive Landscape (1985–1994)
| Platform | Shell | Year | Key Features |
|---|---|---|---|
| AmigaDOS (CLI 1.x) | CLI | 1985 | IF/ELSE/SKIP/LAB, redirection, no pipes, BCPL-based |
| AmigaDOS (Shell 2.0+) | Shell-Seg | 1990 | Full scripting + pipes + aliases + history + resident commands |
| MS-DOS | COMMAND.COM | 1981 | Batch files with GOTO, IF EXIST; no structured loops until DOS 6 CHOICE (1993) |
| Macintosh System 1–6 | None | 1984 | No CLI at all — GUI only until A/UX and MPW Shell |
| Atari ST TOS | COMMAND.PRG (GEMDOS) | 1985 | Minimal batch support in early versions; no pipes until MiNT |
| Unix (BSD/SysV) | sh / csh / ksh | 1977+ | Pipes, backticks, job control — far more powerful, but multi-user focus |
The Amiga Shell was competitive for its era: more capable than MS-DOS batch files (structured IF/ELSE vs GOTO), and dramatically more accessible than Unix shells (the template system meant every command had discoverable syntax via ?). Its main weakness was the lack of true concurrent pipes and the absence of sub-shell command substitution (no backticks — you need Execute + temp files instead).
Modern Analogies
| Amiga Concept | Modern Equivalent | Why the Analogy Holds | Where It Breaks |
|---|---|---|---|
| Shell-Seg | bash (/bin/bash) | Both read commands, launch sub-processes, provide scripting | bash has sub-shells $(), job control, signals; Shell has no sub-shell command substitution |
| ReadArgs template | argparse / clap (Rust) / getopt | Both provide declarative argument definition with type validation | ReadArgs is centralized — every program uses the same parser; Unix has dozens of incompatible parsers |
| Resident command | shell built-in (bash's echo, cd) |
Both keep binary in memory to avoid disk I/O | Resident works on ANY executable; shell built-ins are hard-coded into the shell binary |
| PIPE: device | **Unix pipe ` | `** | Both provide concurrent streaming between processes |
| Startup-Sequence | systemd units / init scripts | Both orchestrate system initialization | systemd is declarative (dependencies, parallel); Startup-Sequence is imperative and sequential |
Execute with PORT |
D-Bus activation | Both allow external events to trigger script execution | D-Bus is IPC; PORT is a simple MsgPort name string with no structured messaging |
FAQ
Q: How do I run a command and capture its output in a variable?
AmigaDOS has no backtick or $() equivalent. You must redirect to a temp file and read it back:
Date >T:myoutput
Set mydate `Type T:myoutput`
The backtick syntax `command` used with Set runs the command and substitutes its output into the variable.
Q: Why does my script fail when launched from Workbench but work from Shell?
Workbench passes a PORT name as the first argument. Your script MUST declare .key PORT as the first line to consume this argument. Without it, the PORT name becomes a stray argument that may cause template parsing errors.
Q: Can I read keyboard input without pressing Enter?
No. The Shell reads complete lines via CON: console input. For single-key input, use inkey.library or write a small C program that calls RawKeyConvert(). This is a fundamental limitation of line-buffered console I/O.
Q: What's the maximum script nesting depth?
There is no hard limit, but each nested Execute call consumes ~4 KB of stack. After ~20 levels of nesting on a default 4096-byte stack, the script will overflow and crash. Use Stack 20000 before deeply nested scripts.
Q: Can I pass arguments with spaces to a script?
Yes — use double quotes. The .key directive respects quoted arguments:
; Script with KEY:
.key FILENAME/A
Echo "File: {FILENAME}"
; Invocation:
Execute MyScript "My Long Filename.txt"
Q: How do I detect if I'm running in a script vs interactive Shell?
Check for .KEY or test if a PORT was passed:
.key PORT
If "{PORT}" EQ ""
Echo "Interactive"
Else
Echo "Script with PORT = {PORT}"
EndIf
Q: Why doesn't FailAt 999 prevent my script from stopping?
FailAt only affects return codes 0–30. A return code of 31+ always aborts the script. Additionally, FailAt does not catch hardware exceptions (division by zero, bus errors) — those trigger Guru Meditation at the exec level, not at the DOS level.
References
- NDK 3.9:
dos/rdargs.h— ReadArgs structure and constants - RKRM: AmigaDOS Manual — Shell commands, scripting, template system
- ADCD 2.1: AmigaDOS Guide — complete command reference
- AmigaDOS Inside & Out (Kerkloh/Tornsdorf/Zoller, Abacus, 1991) — practical script cookbook
- AmigaOS Wiki: AmigaDOS Advanced Features — Pipe, escape sequences, startup files
See Also
- File I/O — Open/Close/Read/Write that Shell scripts rely on
- Process Management — CreateNewProc, SystemTagList — how Shell launches programs
- Environment — GetVar/SetVar, ENV:/ENVARC: variable system
- Error Handling — IoErr, PrintFault — the return codes that FailAt catches