feat: extend shell with ShellRedirect
This commit is contained in:
1 parent
f8ddc6650e
commit
ff4256f987
1 file changed
+175
+175
@@ -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
|
||||
Reference in new issue
Block a user