Files
frostbyte/include/shell.h
T
2026-09-02 15:10:20 +02:00

277 lines
17 KiB
C

#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);
/* Recognizes shell.c's `$?` special parameter -- real POSIX shells' own
most-recently-completed command status, distinct from any `$NAME`
environment variable (shellParseVarRef above deliberately rejects it:
'?' is not in its own [A-Za-z0-9_] name-character set, so the two
token classes can never collide). `token` must be EXACTLY the two
characters "$?" and nothing else -- same "whole token or nothing"
contract shellParseVarRef already gives `$NAME`, so `$?x` or `x$?` are
both rejected, left for the caller to treat as an ordinary literal
token. The glue that resolves a match into an actual decimal status
string (shell.c's shellExpandToken, which reads shell.c's own
file-scope `lastStatus`) is not pure, so only this syntax check lives
here -- the same "pure logic here, thread/state-touching glue stays
static in shell.c" split shellParseVarRef already established. */
bool shellIsStatusVarToken(const char* token);
/* Classic shell wildcard matcher: '*' matches any run of characters
(including none), '?' matches exactly one character, anything else must
match `name` literally. Pure string logic (no VFS/scheduler involved),
iterative rather than recursive (a single backtrack point remembering
the most recent '*' and where in `name` it started matching from) --
the same shape a real libc fnmatch(3) uses, and the same "pure logic
here, thread/VFS-touching glue stays static in shell.c" split
shellTokenize/shellSplitPipelineStages/shellExtractRedirects/
shellParseVarRef above already established. Exposed here for
shellTest.c's KTEST coverage. */
bool shellGlobMatch(const char* pattern, const char* name);
/* True if `token` contains at least one glob metacharacter ('*' or '?') --
the same "is this token special" gate shellParseVarRef gives '$', used by
shell.c's shellExpandGlobs to decide whether a token needs real directory
listing + matching at all, or can pass through completely untouched (the
common case for most argv words, which have no wildcard in them at
all). */
bool shellTokenHasGlob(const char* token);
/* Splits `token` at its LAST '/' into `dirOut` (everything up to and
including that '/', or an empty string if `token` has no '/' at all --
meaning "the current directory", exactly like a bare `*.txt` in a real
shell) and `patternOut` (everything after it, the actual glob pattern to
match each directory entry's own name against via shellGlobMatch above).
Only this FINAL path component may itself contain glob syntax -- `dirOut`
is never re-scanned for `*`/`?` by anything in shell.c, an
honestly-scoped limitation (e.g. `/tmp/gd/ *.txt` is supported,
`/tmp/ * /file.txt` is not) matching this file's existing pattern of
documenting what its syntax does NOT attempt (see runPipelineStages' own
"no stage may be a builtin" and shellExtractRedirects' own redirect-target
scope limits). Truncates silently (like every other fixed-capacity buffer
in this shell) if either half does not fit its own capacity. Exposed here
for the same pre-scheduler, no-VFS KTEST coverage every other pure
function in this file already gets (shellTest.c). */
void shellSplitGlobToken(const char* token, char* dirOut, int dirCap, char* patternOut, int patternCap);
/* Recognizes shell.c's `$(cmd [args])` command-substitution syntax --
locates the FIRST `$(` anywhere in `line` and its matching `)` (no
nesting: the very next `)` after the opening `$(`, an honestly-scoped
limitation matching shellSplitGlobToken's own "only the final path
component" scope note), and copies the text between them (the command
to run, still completely unparsed) into `cmdOut`, truncated to `cmdCap`
like every other fixed-capacity buffer in this file.
Returns true with `*startOut` set to -1 (cmdOut/`*endOut` left
untouched) if `line` contains no `$(` at all -- the caller's signal to
stop looking, exactly like shellSplitPipelineStages' own "0 means
nothing found" convention but spelled with a sentinel index instead of a
sentinel count, since a real match also needs to report WHERE in the
line it sits. On a real match, `*startOut`/`*endOut` are the byte
offsets of the leading `$` and one-past the matching `)` respectively,
so a caller can splice `line[0..*startOut)` + (whatever `cmdOut`
evaluates to) + `line[*endOut..]` into a new line the same way
shellExtractRedirects' caller already reassembles argv around removed
tokens. Returns false, leaving everything untouched, if a `$(` is found
with no matching `)` anywhere after it -- the same "malformed input,
caller reports an error" shape shellExtractRedirects already uses for an
operator with no filename. Exposed here (rather than kept static in
shell.c) for the same pre-scheduler, no-VFS KTEST coverage every other
pure syntax function in this file already gets (shellTest.c) -- the glue
that actually RUNS `cmdOut` and splices its captured output back in
(shell.c's shellExpandCommandSubst) is not pure, so only the syntax half
lives here, the same split shellParseVarRef/shellIsStatusVarToken above
already established for `$NAME`/`$?`. */
bool shellFindCommandSubst(const char* line, int* startOut, int* endOut, char* cmdOut, int cmdCap);
/* The pid most recently spawned by the shell's own `NAME [ARGS] [&]`
fallback (shell.c's runProgram) -- foreground or background alike, set
the instant jobctlSpawnForeground/jobctlRunBackground (jobctl.h) hand
one back, never only after a foreground job has already exited -- or
threadInvalidId if the shell has never run anything yet. Lets a
boot-time live demo (kmain.c) that drives the real interactive shell
through injected keystrokes (there being no automated way to drive a
real PS/2 keyboard under headless QEMU) find out what pid it just asked
the shell to run, so it can act on that specific job (e.g. proving a
backgrounded one stays alive across an injected Ctrl-C the same way
user/pgroupbg.c's boot-time-spawned counterpart already does). */
uint32 shellLastSpawnedPid(void);
#endif