mirror of
https://github.com/alfishe/amiga-bootcamp.git
synced 2026-06-12 16:16:28 +00:00
948 lines
36 KiB
Markdown
948 lines
36 KiB
Markdown
[← Home](../README.md) · [AmigaDOS](README.md)
|
||
|
||
# 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 `NewShell` is the command that opens a Shell window on OS 2.0+.
|
||
|
||
---
|
||
|
||
## Architecture — How Shell Creates Processes
|
||
|
||
```mermaid
|
||
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 (`|` for stdout, `|&` for stdout+stderr) |
|
||
| **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.txt` sends stdout to `out.txt` and stderr to `err.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:
|
||
|
||
1. **Create a temporary file** in `T:` (which is typically assigned to `RAM:T`)
|
||
2. **Run the first command** with stdout redirected to the temp file: `cmd1 >T:pipe.XXXXXX`
|
||
3. **Wait for the first command to complete** (this is the key difference from Unix — Amiga pipes are **sequential**, not concurrent)
|
||
4. **Run the second command** with stdin redirected from the temp file: `cmd2 <T:pipe.XXXXXX`
|
||
5. **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 calls `EndCLI`, your Shell window closes. If it calls `FailAt 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:
|
||
|
||
```c
|
||
/* 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
|
||
|
||
```c
|
||
/* 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:
|
||
|
||
1. Searches `C:Copy` and finds the binary
|
||
2. 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)
|
||
3. Calls `ReadArgs("FROM/A,TO/A,ALL/S,CLONE/S,BUF/N", array, NULL)` with the user's argument string
|
||
4. `ReadArgs()` parses the string against the template, fills the array
|
||
5. The Shell passes the filled array to the child process via the **WBenchMsg** or via registers (implementation-specific)
|
||
6. 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 ... PURE` and run it 3+ times to verify correct behavior before adding it to `S: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-Sequence` directly. Add customizations to `S:User-Startup` instead. If `S:User-Startup` doesn't exist, create it — the default `Startup-Sequence` checks for it with `IF EXISTS S:User-Startup` and 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
|
||
|
||
1. **Always quote strings with spaces** — `Echo "Hello World"` not `Echo Hello World`
|
||
2. **Put `FailAt 21` at the top of every script** — prevents one failed command from aborting the entire script
|
||
3. **Test commands interactively before adding to scripts** — syntax errors in `S:Startup-Sequence` can prevent booting
|
||
4. **Use `>NIL:` to suppress output in scripts** — keeps the console clean and avoids filling `RAM:` with temp output
|
||
5. **Check return codes with `IF WARN` / `IF ERROR`** — never assume a command succeeded; AmigaDOS commands are chatty about failures
|
||
6. **Use full paths in startup scripts** — during boot, `C:` may not be in the search path yet; write `C:Copy` not just `Copy`
|
||
7. **Make heavily-used commands Resident before they're needed** — put `Resident C:Dir PURE ADD` in `S:User-Startup` so it's ready before scripts run
|
||
8. **Add `QUIET` to Copy/Delete in scripts** — suppresses per-file progress output
|
||
9. **Use `Execute` with an empty string port argument when calling scripts from scripts** — `Execute S:SubScript ""`
|
||
10. **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 prefix
|
||
- **`Execute` and 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 | Unix pipes are anonymous; Amiga PIPE: is named and must be opened by filename |
|
||
| **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](https://wiki.amigaos.net/wiki/AmigaOS_Manual:_AmigaDOS_Advanced_Features) — Pipe, escape sequences, startup files
|
||
|
||
## See Also
|
||
|
||
- [File I/O](file_io.md) — Open/Close/Read/Write that Shell scripts rely on
|
||
- [Process Management](process_management.md) — CreateNewProc, SystemTagList — how Shell launches programs
|
||
- [Environment](environment.md) — GetVar/SetVar, ENV:/ENVARC: variable system
|
||
- [Error Handling](error_handling.md) — IoErr, PrintFault — the return codes that FailAt catches
|