Let the shell run a file of commands

'do <file>' runs the lines in a file as though they had been typed. The
only thing a script changes is where the next line comes from: everything
below shellReadLine - splitting the line, matching it, loading a program -
cannot tell the difference and does not have to.

What makes a file a script is '#!' on the front of it, not its name and not
a flag in its entry. The rule this filesystem keeps is that an entry holds
only what the content cannot say about itself, and a script can say what it
is; the loader already refuses anything that is not SBEX, so the two kinds
of runnable file turn each other away without either knowing about the
other. It is also the deferred half of the file-typing design, which said
to wait for a second kind of runnable thing before building any of it. This
is that second kind.

'#' is a directive and ';' is a comment, as in SplitBit assembly - one rule
across the machine rather than two dialects. Not Unix's convention: there
'#!' really is a comment that only the kernel reads, while here the shell
requires it and refuses the file without it, so calling it a comment would
be a lie about what it does.

A script stops at the first line that does not work, which is what the
LineFailed groundwork was for. Comments and blank lines are dropped by the
reader rather than by the dispatch, so they are not echoed either. A script
running out hands back to the console rather than ending the shell, because
running out of file and running out of typing are not the same thing. The
interactive assembler reads through the same path, so a script can contain
a block of assembly.

Three things this cost that were not obvious:

  - RET puts A and B back, so a routine cannot answer in them. scriptByte
    returning the character in A assembled, ran, and handed the caller its
    own A back every time. It answers in memory now.
  - A last line with no newline is still a line. Text files do not reliably
    end with one and an editor eating it is a bad way to find out a command
    did not run.
  - Not LastStatus. See the commit before this one.

Six checks in three tests, two of which are about byte positions rather
than behaviour - a command lying across the boundary between two blocks,
and that missing newline - so their fixtures are generated rather than
committed, where an editor cannot helpfully repair them.

Nesting is not in yet: a script cannot run a script. That wants a stack of
positions rather than the one the reader keeps.

Also derives native.sh's self-hosting source list from cosmos.asm's own
#Include lines. It was a hand written list and went stale the moment
script.asm existed - the fourth time a list beside a thing has drifted from
the thing - so it now asks the thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-30 15:50:26 -04:00
co-authored by Claude Opus 5
parent fc56e815fc
commit 2466d79d9c
16 changed files with 600 additions and 7 deletions
+92 -2
View File
@@ -47,6 +47,7 @@
#Include text.asm
#Include sbfs.asm
#Include services.asm
#Include script.asm
#Program
@@ -133,6 +134,24 @@ bootNoDisk:
; shell. Only saying so leaves the monitor, or a program breaking the machine badly enough
; to need starting again.
prompt:
; ---- A script stops at the first line that did not work ----
;
; Checked here, before the next line is read, because this is the one place every command
; comes back to. A build whose first step failed and whose second step ran anyway produces
; something wrong and says it succeeded, which is the failure this whole flag exists to
; prevent.
SETD.1 ScriptRunning
LDA.1
BRA promptWhere
SETD.1 LineFailed
LDA.1
BRA promptWhere
SETD.0 ScriptStopped
CALL printString
CALL newLine
CALL scriptClose
promptWhere:
; Where you are, but only when that is not obvious. At the root the prompt is the one it
; has always been, so a machine nobody has moved about on looks exactly as it did - and
; every recorded test that never says "cd" keeps its recorded prompt.
@@ -160,7 +179,7 @@ promptSay:
SETD.0 CommandLine
INIB 0d63
CALL readLine
CALL shellReadLine
; Running out of typing is how this ends. It is not the same as an empty line, which is
; just somebody pressing return, and the shell should sit there when that happens.
@@ -230,6 +249,11 @@ promptSay:
CALL textSame
BRQ doRename
SETD.0 CommandLine
SETD.1 DoName
CALL textSame
BRQ doScript
SETD.0 CommandLine
SETD.1 HelpName
CALL textSame
@@ -282,6 +306,31 @@ promptSay:
CALL textSame
BRQ doAssemble
; ---- Where a line comes from ----
;
; The whole of what a script is. Everything below this - splitting the line, matching it
; against the commands, loading a program - cannot tell the difference and does not have to.
;
; A SCRIPT RUNNING OUT IS NOT THE SAME AS TYPING RUNNING OUT. The console ending means there
; is nobody there and the shell should stop; a script ending means go back to whoever asked
; for it. So the end of a script falls through to the console rather than to the door.
shellReadLine:
SETD.1 ScriptRunning
LDA.1
BRA shellReadTyped
CALL scriptLine
BNQ shellReadTyped
; Echoed, so that a script working can be watched and a script failing says where. It is
; printed after the prompt, so it reads exactly like somebody typing it.
CALL printString
CALL newLine
RET
shellReadTyped:
CALL readLine
RET
; ---- A command that did not work ----
;
; The one place a failure is recorded, so that the thing reading lines out of a file can
@@ -1531,6 +1580,37 @@ fileComplain:
CALL newLine
BRI commandFailed
; ---- do ----
;
; Runs the lines in a file as though they had been typed. What makes a file one of these is
; the #! on the front of it, not its name and not a flag in its directory entry: the rule is
; that the entry holds only what the content cannot say about itself, and a script can say
; what it is. The loader already refuses anything that is not SBEX, so the two kinds of
; runnable thing turn each other away without either of them knowing about the other.
doScript:
SETD.1 TextRest
LDD.0.1
LDA.0
BRA scriptNoName
CALL scriptOpen
BRQ prompt ; It is open, and the next line read will come from it.
; Which of the two went wrong. A number would be no use to anybody here.
MVQA
INIB 0x01
CCF
SUB
BRQ scriptNoFile
SETD.0 ScriptNotOne
BRI fileComplain
scriptNoFile:
SETD.0 ScriptNoFile
BRI fileComplain
scriptNoName:
SETD.0 ScriptUsage
BRI fileComplain
; ---- run ----
;
; Hands the machine to whatever was loaded. Where the Stack is now is written down first,
@@ -3094,7 +3174,7 @@ assembleLine:
SETD.0 AsmLine
INIB 0d40
CALL readLine
CALL shellReadLine
INA 0x01
INIB 0x02 ; ENDED, so there is nothing more to assemble.
@@ -3517,6 +3597,14 @@ OnFallback:
"
NoDisk:
"no filesystem on the disk"
ScriptUsage:
"do: give me the name of a script"
ScriptNoFile:
"do: cannot find it"
ScriptNotOne:
"do: that is not a script - it wants #! on the first line"
ScriptStopped:
"stopped: that line did not work"
Unknown:
"I do not know: "
Farewell:
@@ -3696,6 +3784,8 @@ NeedsValue:
"that one needs a value after it"
DirName:
"dir"
DoName:
"do"
LoadName:
"load"
RunName: