diff --git a/include/shell.h b/include/shell.h index a219490..71ced81 100644 --- a/include/shell.h +++ b/include/shell.h @@ -1,6 +1,181 @@ #ifndef FROSTBYTE_SHELL_H #define FROSTBYTE_SHELL_H +#include "types.h" + void shellRun(void); +/* Mirrors elf.h's elfMaxArgs (kept as shell.c's own named copy rather than + including elf.h just for one constant, the same "each caller of this + pattern owns its own named copy" choice jobctl.c/kmain.c/syscall.c + already made for their own shared "one slot's worth of stack" constant) + -- an argv built with more tokens than this could never be handed to + elfBuildInitialStack anyway. */ +#define shellMaxArgs 16 + +/* Upper bound on how many `|`-separated stages shellSplitPipelineStages + below (and shell.c's runPipelineStages) will ever build at once -- a + generous cap for anything a human could plausibly type on one + lineCapacity-bounded (128 bytes, shell.c) command line, not a claim that + real shells have such a limit. Named analogously to shellMaxArgs (a + per-command cap) but for the orthogonal "how many commands are chained + together" axis. */ +#define shellMaxPipelineStages 4 + +/* Splits `text` in place (each run of spaces becomes a NUL terminator, + like strtok but reentrant -- no hidden static state) into at most + maxTokens pointers into that same buffer, returning how many it found. + Takes an explicit capacity rather than hardcoding shellMaxArgs + internally so a caller that already consumed some of its own argv[] + array (runProgram, shell.c: argv[0] is always the program name itself, + not something shellTokenize ever produces) can correctly pass only the + REMAINING room, not the full array size -- passing the full array size + there would let this function write one past the end of it. Exposed + here (rather than kept static in shell.c) so shellTest.c (ktest.h) can + exercise its edge cases -- leading/trailing/repeated spaces, empty + input, hitting the capacity -- as pure string manipulation with no VFS + or scheduler involved, safe to run from KTEST's pre-scheduler + context. */ +int shellTokenize(char* text, char* tokens[], int maxTokens); + +/* Splits `line` in place at every top-level '|' into up to `maxStages` + NUL-terminated stages, written into `stages[0..result-1]` -- shell.c's + `cmd1 [args] | cmd2 [args] | ... | cmdN [args]` pipeline syntax, now a + REAL N-stage chain rather than only ever the first two commands (jobctl.h's + jobctlSpawnPiped already took independent stdin/stdout pipe handles per + spawn from the day it was written, so a middle stage with BOTH ends piped + at once needed no new kernel primitive -- only this function and + shell.c's runPipelineStages were ever the limiting factor). + Whitespace directly touching each '|' is trimmed from both sides of it + (so "a | b | c" and "a|b|c" produce the same stages) -- shellTokenize's + own token splitter only ever strips LEADING spaces from whatever it is + handed, never trailing ones, so every stage but the last has its trailing + spaces trimmed here first. + Returns 0 (leaving `line` and `stages` completely untouched) if there is + no '|' anywhere in the line at all, so the caller (execute()) knows to + fall back to running a single command exactly as before. Returns -1 + (also leaving everything untouched) if the line contains MORE than + `maxStages` stages -- a real shell has no such limit, but this kernel's + per-pipeline arrays (shell.c's runPipelineStages, jobctl.c's + jobctlWaitPipelineStages) are fixed-size, so a caller must be able to + tell "malformed count" apart from "no pipe found" to report the right + error instead of silently truncating a longer chain. Otherwise returns + the real stage count (2..maxStages). Exposed here (rather than kept + static in shell.c) for the same pre-scheduler, no-VFS KTEST coverage + shellTokenize already gets (shellTest.c). */ +int shellSplitPipelineStages(char* line, char* stages[], int maxStages); + +/* Upper bound on how many `&&`/`||`-separated segments shellSplitAndOr + below (and shell.c's execute()) will ever build at once -- the exact + same "generous cap for one lineCapacity-bounded command line, not a + claim real shells have such a limit" reasoning shellMaxPipelineStages + above already gives, on the orthogonal "how many conditionally-run + commands are chained together" axis. */ +#define shellMaxAndOrSegments 4 + +/* The operator immediately BEFORE segments[i+1] in shellSplitAndOr's own + output (there is no operator before segments[0], so andOrOps[] below + only ever has count-1 meaningful entries) -- real shell `&&` + (short-circuit: run the next command only if the previous one + succeeded, exit status 0) vs `||` (run the next command only if the + previous one FAILED, exit status != 0). */ +typedef enum { + shellAndOrAnd = 1, + shellAndOrOr = 2 +} ShellAndOrOp; + +/* Splits `line` in place at every top-level `&&`/`||` into up to + `maxSegments` NUL-terminated segments, written into + `segments[0..result-1]`, with `andOrOps[0..result-2]` recording which + operator sat between each adjacent pair -- shell.c's new + `cmd1 && cmd2 || cmd3 && ...` conditional-chaining syntax, run BEFORE + pipeline splitting (`|` binds tighter than `&&`/`||`, exactly like a + real shell: `a | b && c` means "(a | b) && c", so each of THIS + function's own segments is independently handed to + shellSplitPipelineStages afterward, never the other way around). + Whitespace directly touching each operator is trimmed from both sides + of it, the same "a&&b and a && b produce the same segments" convention + shellSplitPipelineStages already established for `|`. A lone `&` + (shell.c's own trailing background marker, checked much later after + this function has already run) is never mistaken for half of `&&`, + since this function only ever matches the exact two-character sequence + -- a single `&` anywhere else in the line is simply left inside + whichever segment it falls in, untouched. + Returns 0 (leaving `line` and `segments` completely untouched) if there + is no top-level `&&`/`||` anywhere in the line, so the caller knows to + run it as a single command-or-pipeline exactly as before. Returns -1 + (also leaving everything untouched) if the line contains MORE than + `maxSegments` segments, the same "malformed count, do not silently + truncate" shape shellSplitPipelineStages already uses for its own + analogous limit. Otherwise returns the real segment count + (2..maxSegments). Exposed here (rather than kept static in shell.c) for + the same pre-scheduler, no-VFS KTEST coverage shellSplitPipelineStages + already gets (shellTest.c). */ +int shellSplitAndOr(char* line, char* segments[], ShellAndOrOp andOrOps[], int maxSegments); + +/* Result of shellExtractRedirects below -- nullptr inPath/outPath/errPath/ + hereString means "no such redirection was found, leave that fd at its + normal default". */ +typedef struct ShellRedirect { + const char* inPath; + const char* outPath; + bool appendOut; + const char* errPath; + bool appendErr; + /* `<<<` TEXT -- shell.c's new here-string syntax. Unlike inPath (a file + to open), this is the literal next token's own text, fed to the + child's fd 0 through a real anonymous pipe the SHELL itself writes + into (jobctl.h's jobctlSpawnForegroundHereString/ + jobctlRunBackgroundHereString), never opened as a path. Mutually + exclusive with inPath at the SAME fd 0 -- see shellExtractRedirects' + own doc comment below for the last-one-wins rule between the two. */ + const char* hereString; +} ShellRedirect; + +/* Scans an already-tokenized argv (shellTokenize's own output shape) for + `<`, `<<<`, `>`, `>>`, `2>`, and `2>>` tokens -- shell.c's new + `cmd [ARGS] [< IN | <<< TEXT] [> OUT | >> OUT] [2> ERR | 2>> ERR]` + redirection syntax -- and removes each one together with the token + immediately after it (a filename for every operator except `<<<`, whose + own following token is the literal here-string text instead), compacting + the remaining tokens down to a real argv with no gaps (exactly what + jobctl.h's spawn functions need to hand a program, which must never see + its own redirection operators as if they were its own arguments). + Writes the compacted count back through `argc` and fills `out` with + whichever paths/text were found (nullptr for whichever operator never + appeared). Returns false, leaving `argv`/`*argc` completely untouched, if + `<`, `<<<`, `>`, `>>`, `2>`, or `2>>` appears as the LAST token with no + filename/text after it -- the same "malformed input, caller falls back to + reporting an error" shape shellSplitPipelineStages uses for "no pipe + found" (though this one look more like an error since every OTHER token + still gets scanned first, unlike shellSplitPipelineStages's single + up-front counting pass). A later `>`/`>>` overwrites an earlier one (and vice versa) + rather than erroring, the same "last one wins" a real shell gives + `cmd > a > b` -- only ever ONE is meaningful since a single fd 1 can only + be redirected once; `2>`/`2>>` follow the exact same last-one-wins rule + independently for fd 2, since the two operator families target different + fds and never conflict with each other. `<` and `<<<` follow the SAME + last-one-wins rule for fd 0 across the two of them together (whichever + appears later wins, clearing the other field) -- a real shell gives fd 0 + to whichever of `< file`/`<<< text` was written last too, since both + target the exact same single fd. Exposed here (rather than kept static in + shell.c) for the same pre-scheduler, no-VFS KTEST coverage + shellTokenize/shellSplitPipelineStages already get (shellTest.c). */ +bool shellExtractRedirects(char** argv, int* argc, ShellRedirect* out); + +/* Recognizes shell.c's `$NAME` unquoted-parameter-expansion syntax (the + glue that actually resolves it against a real environment, shell.c's + new shellExpandArgv, is not pure -- it reads sched.h's Thread.env via + currentThreadGetEnv, off limits for KTEST's pre-scheduler context -- so + only the SYNTAX half lives here, the same "pure string logic here, the + thread/VFS-touching glue stays static in shell.c" split shellTokenize/ + shellSplitPipelineStages/shellExtractRedirects above already use). + `token` must be exactly '$' followed by one or more characters drawn + from [A-Za-z0-9_] (real shell variable-name syntax) and nothing else; + on a match, copies just the name (without the leading '$') into + `nameOut` and returns true. Returns false, `nameOut` untouched, for a + bare "$", a name that does not fit `nameCap`, or anything not starting + with '$' at all -- callers treat "false" as "not a variable reference, + leave this token exactly as typed". */ +bool shellParseVarRef(const char* token, char* nameOut, int nameCap); #endif