collapseos/kernel/shell.asm

398 lines
8.7 KiB
NASM

; shell
;
; Runs a shell over a block device interface.
; The shell spits a welcome prompt, wait for input and compare the first 4 chars
; of the input with a command table and call the appropriate routine if it's
; found, an error if it's not.
;
; To determine the correct routine to call we first go through cmds in
; shellCmdTbl. This means that we first go through internal cmds, then cmds
; "grafted" by glue code.
;
; If the command isn't found, SHELL_CMDHOOK is called, which should set A to
; zero if it executes something. Otherwise, SHELL_ERR_UNKNOWN_CMD will be
; returned.
;
; See constants below for error codes.
;
; All numerical values in the Collapse OS shell are represented and parsed in
; hexadecimal form, without prefix or suffix.
; *** REQUIREMENTS ***
; err
; core
; parse
; stdio
; *** DEFINES ***
; SHELL_EXTRA_CMD_COUNT: Number of extra cmds to be expected after the regular
; ones. See comment in COMMANDS section for details.
; SHELL_RAMSTART
; *** CONSTS ***
; number of entries in shellCmdTbl
.equ SHELL_CMD_COUNT 6+SHELL_EXTRA_CMD_COUNT
; Size of the shell command buffer. If a typed command reaches this size, the
; command is flushed immediately (same as pressing return).
.equ SHELL_BUFSIZE 0x20
; *** VARIABLES ***
; Memory address that the shell is currently "pointing at" for peek, load, call
; operations. Set with mptr.
.equ SHELL_MEM_PTR SHELL_RAMSTART
; Places where we store arguments specifiers and where resulting values are
; written to after parsing.
.equ SHELL_CMD_ARGS SHELL_MEM_PTR+2
; Command buffer. We read types chars into this buffer until return is pressed
; This buffer is null-terminated and we don't keep an index around: we look
; for the null-termination every time we write to it. Simpler that way.
.equ SHELL_BUF SHELL_CMD_ARGS+PARSE_ARG_MAXCOUNT
; Pointer to a hook to call when a cmd name isn't found
.equ SHELL_CMDHOOK SHELL_BUF+SHELL_BUFSIZE
.equ SHELL_RAMEND SHELL_CMDHOOK+2
; *** CODE ***
shellInit:
xor a
ld (SHELL_MEM_PTR), a
ld (SHELL_MEM_PTR+1), a
ld (SHELL_BUF), a
ld hl, noop
ld (SHELL_CMDHOOK), hl
; print welcome
ld hl, .welcome
jp printstr ; returns
.welcome:
.db "Collapse OS", ASCII_CR, ASCII_LF, "> ", 0
; Inifite loop that processes input. Because it's infinite, you should jump
; to it rather than call it. Saves two precious bytes in the stack.
shellLoop:
; First, let's wait until something is typed.
call stdioGetC
jr nz, shellLoop ; nothing typed? loop
; got it. Now, is it a CR or LF?
cp ASCII_CR
jr z, .do ; char is CR? do!
cp ASCII_LF
jr z, .do ; char is LF? do!
; Echo the received character right away so that we see what we type
call stdioPutC
; Ok, gotta add it do the buffer
; save char for later
ex af, af'
ld hl, SHELL_BUF
xor a ; look for null
call findchar ; HL points to where we need to write
; A is the number of chars in the buf
cp SHELL_BUFSIZE
jr z, .do ; A == bufsize? then our buffer is full. do!
; bring the char back in A
ex af, af'
; Buffer not full, not CR or LF. Let's put that char in our buffer and
; read again.
ld (hl), a
; Now, write a zero to the next byte to properly terminate our string.
inc hl
xor a
ld (hl), a
jr shellLoop
.do:
call printcrlf
ld hl, SHELL_BUF
call shellParse
; empty our buffer by writing a zero to its first char
xor a
ld (hl), a
ld hl, .prompt
call printstr
jr shellLoop
; no ret because we never return
.prompt:
.db "> ", 0
; Parse command (null terminated) at HL and calls it
shellParse:
push af
push bc
push de
push hl
push ix
ld de, shellCmdTbl
ld a, SHELL_CMD_COUNT
ld b, a
.loop:
push de ; we need to keep that table entry around...
call intoDE ; Jump from the table entry to the cmd addr.
ld a, 4 ; 4 chars to compare
call strncmp
pop de
jr z, .found
inc de
inc de
djnz .loop
; exhausted loop? not found
ld a, SHELL_ERR_UNKNOWN_CMD
; Before erroring out, let's try SHELL_HOOK.
ld ix, (SHELL_CMDHOOK)
call callIX
jr z, .end ; oh, not an error!
; still an error. Might be different than SHELL_ERR_UNKNOWN_CMD though.
; maybe a routine was called, but errored out.
jr .error
.found:
; we found our command. DE points to its table entry. Now, let's parse
; our args.
call intoDE ; Jump from the table entry to the cmd addr.
; advance the HL pointer to the beginning of the args.
ld a, ' '
call findchar
or a ; end of string? don't increase HL
jr z, .noargs
inc hl ; char after space
.noargs:
; Now, let's have DE point to the argspecs
ld a, 4
call addDE
; We're ready to parse args
ld ix, SHELL_CMD_ARGS
call parseArgs
or a ; cp 0
jr nz, .parseerror
; Args parsed, now we can load the routine address and call it.
; let's have DE point to the jump line
ld hl, SHELL_CMD_ARGS
ld a, PARSE_ARG_MAXCOUNT
call addDE
push de \ pop ix
; Ready to roll!
call callIX
or a ; cp 0
jr nz, .error ; if A is non-zero, we have an error
jr .end
.parseerror:
ld a, SHELL_ERR_BAD_ARGS
.error:
call shellPrintErr
.end:
pop ix
pop hl
pop de
pop bc
pop af
ret
; Print the error code set in A (in hex)
shellPrintErr:
push af
push hl
ld hl, .str
call printstr
call printHex
call printcrlf
pop hl
pop af
ret
.str:
.db "ERR ", 0
; *** COMMANDS ***
; A command is a 4 char names, followed by a PARSE_ARG_MAXCOUNT bytes of
; argument specs, followed by the routine. Then, a simple table of addresses
; is compiled in a block and this is what is iterated upon when we want all
; available commands.
;
; Format: 4 bytes name followed by PARSE_ARG_MAXCOUNT bytes specifiers,
; followed by 3 bytes jump. fill names with zeroes
;
; When these commands are called, HL points to the first byte of the
; parsed command args.
;
; If the command is a success, it should set A to zero. If the command results
; in an error, it should set an error code in A.
;
; Extra commands: Other parts might define new commands. You can add these
; commands to your shell. First, set SHELL_EXTRA_CMD_COUNT to
; the number of extra commands to add, then add a ".dw"
; directive *just* after your '#include "shell.asm"'. Voila!
;
; Set memory pointer to the specified address (word).
; Example: mptr 01fe
shellMptrCmd:
.db "mptr", 0b011, 0b001, 0
shellMptr:
push hl
; reminder: z80 is little-endian
ld a, (hl)
ld (SHELL_MEM_PTR+1), a
inc hl
ld a, (hl)
ld (SHELL_MEM_PTR), a
ld hl, (SHELL_MEM_PTR)
ld a, h
call printHex
ld a, l
call printHex
call printcrlf
pop hl
xor a
ret
; peek byte where memory pointer points to any display its value. If the
; optional numerical byte arg is supplied, this number of bytes will be printed
;
; Example: peek 2 (will print 2 bytes)
shellPeekCmd:
.db "peek", 0b101, 0, 0
shellPeek:
push bc
push de
push hl
ld a, (hl)
cp 0
jr nz, .arg1isset ; if arg1 is set, no need for a default
ld a, 1 ; default for arg1
.arg1isset:
ld b, a
ld hl, (SHELL_MEM_PTR)
.loop: ld a, (hl)
call printHex
inc hl
djnz .loop
call printcrlf
.end:
pop hl
pop de
pop bc
xor a
ret
; poke byte where memory pointer points and set them to bytes types through
; stdioGetC. If the optional numerical byte arg is supplied, this number of
; bytes will be expected from stdioGetC. Blocks until all bytes have been
; fetched.
shellPokeCmd:
.db "poke", 0b101, 0, 0
shellPoke:
push bc
push hl
ld a, (hl)
or a ; cp 0
jr nz, .arg1isset ; if arg1 is set, no need for a default
ld a, 1 ; default for arg1
.arg1isset:
ld b, a
ld hl, (SHELL_MEM_PTR)
.loop: call stdioGetC
jr nz, .loop ; nothing typed? loop
ld (hl), a
inc hl
djnz .loop
pop hl
pop bc
xor a
ret
; Calls the routine where the memory pointer currently points. This can take two
; parameters, A and HL. The first one is a byte, the second, a word. These are
; the values that A and HL are going to be set to just before calling.
; Example: run 42 cafe
shellCallCmd:
.db "call", 0b101, 0b111, 0b001
shellCall:
push hl
push ix
; Let's recap here. At this point, we have:
; 1. The address we want to execute in (SHELL_MEM_PTR)
; 2. our A arg as the first byte of (HL)
; 2. our HL arg as (HL+1) and (HL+2)
; Ready, set, go!
ld ix, (SHELL_MEM_PTR)
ld a, (hl)
ex af, af'
inc hl
ld a, (hl)
exx
ld h, a
exx
inc hl
ld a, (hl)
exx
ld l, a
ex af, af'
call callIX
.end:
pop ix
pop hl
xor a
ret
shellIORDCmd:
.db "iord", 0b001, 0, 0
push bc
ld a, (hl)
ld c, a
in a, (c)
call printHex
xor a
pop bc
ret
shellIOWRCmd:
.db "iowr", 0b001, 0b001, 0
push bc
ld a, (hl)
ld c, a
inc hl
ld a, (hl)
out (c), a
xor a
pop bc
ret
; This table is at the very end of the file on purpose. The idea is to be able
; to graft extra commands easily after an include in the glue file.
shellCmdTbl:
.dw shellMptrCmd, shellPeekCmd, shellPokeCmd, shellCallCmd
.dw shellIORDCmd, shellIOWRCmd