@sema-agent/core
Version:
Stateless, task-oriented AI agent core
264 lines (263 loc) • 17.1 kB
TypeScript
/**
* The ONE shell lexer: one walk over a bash command mints a structure of words made of PIECES, and every other
* spelling a reader consumes — `raw`, `folded`, the quoted spans, the expansion list, the assignment fact, the
* reason a value cannot be read off the text — is a view of that structure. Dialect: bash (the execution
* environment's last-resort `sh` reads `$'…'`, `+=`, a subscript, `<<<`, `<(…)` and `&>` differently; see
* {@link bashOnlyConstructsOf}).
*
* ONE walk, ONE reading: every construct bash reads is RECORDED — a redirection, a substitution, an escape, a
* comment, a line break, a backgrounding `&` — never refused here. Which of them a face refuses is that face's
* predicate over the result (the classify faces' residual predicate in the read-only classifier); the lexer has
* no modes. Bash's own word rules hold throughout: an unquoted word-start `#` begins a comment, and only a blank
* (space or tab) bounds a word — a non-breaking space or a vertical tab is a word character.
*
* A command bash cannot parse is a {@link LexParseFailure}, never a guessed reading: an unterminated quote,
* substitution, subscript or here-document yields NO segments. A word bash reads but whose VALUE the text does not
* contain carries {@link LexedWord.unresolved}; the word is still delimited and its neighbours still read.
*
* Line continuations. Bash removes a `\<newline>` pair before it recognises anything — except inside single quotes,
* a comment and a QUOTED here-document body. Every lookahead below reads the next LOGICAL character past any
* continuation, so a pair can stand inside a construct's spelling and never inside the three places bash keeps it.
*/
/** The dialect this lexer reads. The execution environment falls back to `sh` when no bash is found; a reader on
* that path must degrade every word carrying a {@link BashOnlyConstruct} to the conservative reading. */
export declare const SHELL_LEX_DIALECT = "bash";
/** What holds a character quoted: the four quote forms and the backslash escape, one reason each. */
export declare const QUOTE_KINDS: readonly ["'", "\"", "$'", "$\"", "\\"];
export type QuoteKind = (typeof QUOTE_KINDS)[number];
/** The expansions this lexer names. Extended globs are not among them: `extglob` is off when `bash -c` starts, where
* `?(…)` is a syntax error and `(` is a group token here — but a text may turn it on (`shopt -s extglob` on an earlier
* line), after which bash reads `pack@(age.json)` as one pattern. A reader that plans reads treats an unquoted opener
* as a pattern it does not expand (`extglobOpenerIn`, the read station's `unreadable` floor). */
export declare const EXPANSION_KINDS: readonly ["parameter", "command", "process", "arithmetic", "tilde", "brace", "glob"];
export type ExpansionKind = (typeof EXPANSION_KINDS)[number];
/** Why a word's value cannot be read off the text. Each member is a spelling whose value the text does not
* contain — never "the lexer did not look". */
export declare const UNRESOLVED_REASONS: readonly ["ansi_c_escape", "locale_string", "parameter_text", "subscript_text", "compound_array_value"];
export type UnresolvedReason = (typeof UNRESOLVED_REASONS)[number];
/** The sentence a consumer hands on for an unresolved word: every reason is one a face ASKS about (the locale
* string included — its translation is not in the text), and a deny row is still judged on the written spelling
* by the readers that keep the word beside the reason. */
export declare const UNRESOLVED_SENTENCE: {
readonly ansi_c_escape: "an ANSI-C quoted word carries an escape this lexer does not fold";
readonly locale_string: "a locale-translated string carries text this lexer does not read";
readonly parameter_text: "a parameter expansion carries text this lexer does not read";
readonly subscript_text: "a subscript carries text this lexer does not read";
readonly compound_array_value: "a compound assignment value is a list this lexer does not read";
};
/** Why a command yields no segments: bash's own syntax errors and the two constructs this lexer cannot delimit. */
export declare const PARSE_FAILURE_REASONS: readonly ["unterminated_single_quote", "unterminated_double_quote", "unterminated_ansi_c", "unterminated_subscript", "unterminated_compound_value", "unterminated_command_substitution", "unterminated_process_substitution", "unterminated_parameter_expansion", "unterminated_heredoc", "undelimited_substitution", "redirect_without_target", "heredoc_without_delimiter", "heredoc_delimiter_expands"];
export type ParseFailureReason = (typeof PARSE_FAILURE_REASONS)[number];
export declare const PARSE_FAILURE_SENTENCE: {
readonly unterminated_single_quote: "a single-quoted word never closes";
readonly unterminated_double_quote: "a double-quoted word never closes";
readonly unterminated_ansi_c: "an ANSI-C quoted word never closes";
readonly unterminated_subscript: "a subscript never closes";
readonly unterminated_compound_value: "a compound assignment value never closes";
readonly unterminated_command_substitution: "a command substitution never terminates";
readonly unterminated_process_substitution: "a process substitution never terminates";
readonly unterminated_parameter_expansion: "a parameter expansion never terminates";
readonly unterminated_heredoc: "a here-document body never terminates";
readonly undelimited_substitution: "a command substitution carries a construct this lexer cannot delimit";
readonly redirect_without_target: "a redirection names no target";
readonly heredoc_without_delimiter: "a here-document operator names no delimiter";
readonly heredoc_delimiter_expands: "a here-document delimiter is not a plain word";
};
/** The constructs of this dialect a POSIX `sh` reads as something else. */
export declare const BASH_ONLY_CONSTRUCTS: readonly ["ansi_c_quoting", "locale_string", "append_assignment", "subscript_assignment", "compound_assignment", "here_string", "process_substitution", "redirect_both", "pipe_both", "brace_expansion"];
export type BashOnlyConstruct = (typeof BASH_ONLY_CONSTRUCTS)[number];
/**
* The ANSI-C escapes this lexer folds: the thirteen single-letter ones, whose byte is the same in every bash and
* in the reference implementation's table. Numeric, control and unicode escapes are NOT here — their bytes differ
* between bash 3.2 and 5.x (`\c?`, `\u`, `\U`) or truncate the region (NUL) — and a body carrying one is
* `unresolved` rather than decoded. Admission to this table is a differential test against the real shells.
*/
export declare const ANSI_C_ESCAPES: {
readonly a: "\u0007";
readonly b: "\b";
readonly e: "\u001B";
readonly E: "\u001B";
readonly f: "\f";
readonly n: "\n";
readonly r: "\r";
readonly t: "\t";
readonly v: "\v";
readonly "\\": "\\";
readonly "'": "'";
readonly '"': "\"";
readonly "?": "?";
};
export interface WordPiece {
/** Source offsets of the written text, `[start, end)`; a continuation between two pieces is nobody's. */
readonly span: readonly [number, number];
readonly quotedBy: QuoteKind | undefined;
/** The text the program receives from this piece — absent for an expansion and for an unresolved piece. */
readonly literal?: string;
readonly expansion?: WordExpansion;
}
export interface WordExpansion {
readonly kind: ExpansionKind;
/** `"many"` where the result is word-split (an unquoted parameter or command substitution, a brace list, a glob). */
readonly yields: "one" | "many";
/** The text between the delimiters, where there is one (a substitution body, a brace list, a tilde-prefix). */
readonly body?: string;
/** The parameter name, where the expansion names one. */
readonly name?: string;
}
export interface QuotedSpan {
/** Offsets into {@link LexedWord.raw}, `[start, end)`; the quote characters belong to their span. */
readonly start: number;
readonly end: number;
readonly by: QuoteKind;
}
/** Bash's assignment fact for a word whose folded spelling is `NAME[subscript]?(+)?=value`, minted wherever the
* word stands. Whether bash STRIPS it before choosing the program is `bare && atCommandPosition`; whether the
* program that receives it reads it as an assignment is the program's own grammar. */
export interface AssignmentFact {
readonly name: string;
readonly op: "=" | "+=";
readonly subscript?: {
readonly raw: string;
readonly folded: string;
readonly expansions: readonly WordPiece[];
};
readonly value: {
readonly raw: string;
readonly folded: string;
readonly pieces: readonly WordPiece[];
readonly expansions: readonly WordPiece[];
readonly unresolved?: UnresolvedReason;
};
/** The name, the brackets and the operator are written unquoted and unescaped — the subscript's CONTENT and the
* value may be quoted and still leave the word an assignment. */
readonly bare: boolean;
readonly form: "scalar" | "compound";
/** `op === "="` with no subscript and a scalar value: the whole name is settled by this word's text. */
readonly settlesWholeName: boolean;
}
/** An argv word. `raw` is what the SHELL sees (quotes, escapes and substitution text retained); `folded` is what
* the PROGRAM receives after quote removal, with every substitution replaced by {@link SUBSTITUTION_PLACEHOLDER},
* every other expansion left in its written spelling, and a literal leading tilde spelled `./~…`
* ({@link literalTildeSpelling}). Both are views of {@link LexedWord.pieces}. */
export interface LexedWord {
readonly kind: "word";
readonly raw: string;
readonly folded: string;
readonly pieces: readonly WordPiece[];
readonly quotedSpans: readonly QuotedSpan[];
/** The pieces that carry an expansion, in order. */
readonly expansions: readonly WordPiece[];
readonly assignment?: AssignmentFact;
/** The first reason, in reading order, that a value in this word cannot be read off the text. */
readonly unresolved?: UnresolvedReason;
/** The word was read where bash accepts an assignment or a command name: at a segment's start, after a bare
* assignment word, after a bare control-prefix word, or after a leading redirection list. */
readonly atCommandPosition: boolean;
}
/** A redirection and the word it takes. A descriptor duplication (`>&2`, `<&0`, `>&-`, `>&1-`) takes none: its
* operand is `dup`, the descriptor (or `-`, or a descriptor followed by `-`) as written. `fd` is the descriptor
* number written in front of the operator (`2>f`, `3>&0`); absent when none was (bash's default, `1` for an
* output form and `0` for an input form, is the reader's to apply). */
export interface LexedRedirect {
readonly kind: "redirect";
readonly op: "<" | ">" | ">>" | "<<<" | "&>" | "&>>" | ">&" | "<&";
readonly fd?: number;
readonly target?: LexedWord;
readonly dup?: string;
}
/** A here-document: the body is DATA the program reads on stdin, never command text. `quoted` records whether
* the delimiter word carried a quote or an escape, which is bash's rule for "the body does not expand". */
export interface LexedHeredoc {
readonly kind: "heredoc";
readonly delimiter: string;
readonly quoted: boolean;
readonly body: string;
}
/** A subshell parenthesis, standing alone: structure, not a word. */
export interface LexedGroup {
readonly kind: "group";
readonly op: "(" | ")";
}
export type LexedToken = LexedWord | LexedRedirect | LexedHeredoc | LexedGroup;
/** The connector bash ran a segment through — `"start"` for the first one. A closed set, because the read
* boundary distinguishes all four: `&&` is the only one under which the previous segment is known to have
* exited ZERO, `||` is the only one under which the previous segment may have been SKIPPED ENTIRELY, `|`
* puts the segment in a subshell whose directory changes never reach the parent, and `;` is plain sequence. */
export type ShellSegmentConnector = "start" | ";" | "&&" | "||" | "|";
/** The two boundaries the read boundary does not distinguish from a plain sequence: a backgrounding `&` and a
* line break. */
export type LenientConnector = ShellSegmentConnector | "&" | "\n";
export interface LexedSegment {
/** The segment's text exactly as written (surrounding whitespace included). Here-document bodies are not part
* of any segment's text. */
readonly text: string;
readonly connector: LenientConnector;
/** The `|` was spelled `|&`: the previous segment's standard error is piped too (bash 4+; a syntax error on
* bash 3.2, so a reader on the fail-closed side treats the pipe as real). */
readonly pipesStderr?: true;
readonly tokens: readonly LexedToken[];
/** The bodies of the substitutions found in THIS segment (its words and its redirection targets), in order. */
readonly substitutions: readonly string[];
}
export interface LexedCommand {
readonly segments: readonly LexedSegment[];
/** Every substitution body of the command, in order (the per-segment lists concatenated). */
readonly substitutions: readonly string[];
}
export interface LexParseFailure {
readonly parseFailure: {
readonly reason: ParseFailureReason;
readonly at: number;
};
}
/** A command: its segments, or the one reason it has none. */
export type ShellLex = LexedCommand | LexParseFailure;
/** One segment's argv words in both spellings, index-aligned by construction (folding is strictly per word — it
* never splits or joins). The one shape every downstream arm of the classifier reads. */
export interface SegmentTokens {
readonly folded: readonly string[];
readonly raw: readonly string[];
}
/** What a substitution leaves in a word's FOLDED spelling: a marker no argv word can spell, so a consumer that
* judges words sees "this word carries text the shell computes" rather than the computing text itself. */
export declare const SUBSTITUTION_PLACEHOLDER = " subst";
/** Shell control words that stand BEFORE a command: transparent for the boundary walk (the command behind them is
* judged), and a position after which bash still accepts an assignment word. */
export declare const SHELL_CONTROL_PREFIXES: ReadonlySet<string>;
/**
* Does the `$` at `at` OPEN a quote form (`$'…'` / `$"…"`)? ONE question, asked by every walker that has to
* answer it. Escaped means an ODD run of backslashes in front of it: `\$'y'` is a literal `$`, while `x\\$'y'`
* is a literal backslash followed by a real ANSI-C quote. Counting the run is what a one-character peek behind
* cannot do.
*/
export declare function dollarOpensQuote(text: string, at: number): boolean;
/** The `'` that CLOSES the ANSI-C quote opened at `open`, or `undefined` when none does. `\'` does not close
* one — the single place a quote region honours a backslash. */
export declare function ansiCQuoteClose(source: string, open: number): number | undefined;
/** The ONE spelling of a LITERAL leading tilde — `./~…`, the relative path it is — for every position bash does not
* expand it in; a word without a leading tilde is returned as is. */
export declare function literalTildeSpelling(word: string): string;
/** The argv words of a segment: redirections, here-documents and groups are not words the program receives (bash
* removes them before exec). */
export declare function argvOf(segment: LexedSegment): readonly LexedWord[];
/** The two-spelling view of {@link argvOf}, for the readers that compare spellings. */
export declare function wordsOf(segment: LexedSegment): SegmentTokens;
/** The substitution bodies of an UNQUOTED here-document body: `$(…)` and backticks are live there, quotes are
* ordinary characters, a backslash protects the next character. `undefined` when a substitution never closes. */
export declare function substitutionsInHeredocBody(body: string): {
readonly body: string;
readonly kind: "command" | "arithmetic";
}[] | undefined;
/** Lex a command: its segments, or the one reason bash (or this lexer) cannot delimit them. */
export declare function lexShell(source: string): ShellLex;
/**
* The refused-reading view of {@link lexShell}: a parse failure, and any word whose value cannot be read off the
* text, become the one `{ reject }` sentence a consumer's unreadable arm already takes. A reader that keeps the
* other words (the read station) reads {@link lexShell} and the word's {@link LexedWord.unresolved} instead.
*/
export declare function lexShellText(source: string): LexedCommand | {
reject: string;
};
/** The constructs of this dialect the command leans on — what a POSIX `sh` would read as something else. Empty
* when the command reads the same under either shell's word grammar. */
export declare function bashOnlyConstructsOf(command: LexedCommand): readonly BashOnlyConstruct[];