collapseos/kernel/shell.asm

362 lines
7.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
; maximum length for shell commands. Should be confortably below stdio's
; readline buffer length.
.equ SHELL_MAX_CMD_LEN 0x10
; *** 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 @+2
; Pointer to a hook to call when a cmd name isn't found
.equ SHELL_CMDHOOK @+PARSE_ARG_MAXCOUNT
.equ SHELL_RAMEND @+2
; *** CODE ***
shellInit:
xor a
ld (SHELL_MEM_PTR), a
ld (SHELL_MEM_PTR+1), a
ld hl, noop
ld (SHELL_CMDHOOK), hl
; print welcome
ld hl, .welcome
jp printstr
.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:
call stdioReadLine
call printcrlf
call shellParse
ld hl, .prompt
call printstr
jr shellLoop
.prompt:
.db "> ", 0
; Parse command (null terminated) at HL and calls it
shellParse:
; first thing: is command empty?
ld a, (hl)
or a
ret z ; empty, nothing to do
push af
push bc
push de
push hl
push ix
; Before looking for a suitable command, let's make the cmd line more
; usable by replacing the first ' ' with a null char. This way, cmp is
; easy to make.
push hl ; --> lvl 1
ld a, ' '
call findchar
jr z, .hasArgs
; no arg, (HL) is zero to facilitate processing later, add a second
; null next to that one to indicate unambiguously that we have no args.
inc hl
; Oh wait, before we proceed, is our cmd length within limits? cmd len
; is currently in A from findchar
cp SHELL_MAX_CMD_LEN
jr c, .hasArgs ; within limits
; outside limits
ld a, SHELL_ERR_UNKNOWN_CMD
jr .error
.hasArgs:
xor a
ld (hl), a
pop hl ; <-- lvl 1, beginning of cmd
ld de, shellCmdTbl
ld b, SHELL_CMD_COUNT
.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.
xor a
call findchar
inc hl ; beginning of args
; 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 '.inc "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 the number of bytes specified by argument where memory pointer points to
; and display their value. If 0 is specified, 0x100 bytes are peeked.
;
; Example: peek 2 (will print 2 bytes)
shellPeekCmd:
.db "peek", 0b001, 0, 0
shellPeek:
push bc
push hl
ld a, (hl)
ld b, a
ld hl, (SHELL_MEM_PTR)
.loop: ld a, (hl)
call printHex
inc hl
djnz .loop
call printcrlf
.end:
pop hl
pop bc
xor a
ret
; poke specified number of bytes where memory pointer points and set them to
; bytes typed through stdioGetC. Blocks until all bytes have been fetched.
shellPokeCmd:
.db "poke", 0b001, 0, 0
shellPoke:
push bc
push hl
ld a, (hl)
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