UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

265 lines (264 loc) • 17.2 kB
/** 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; /** A glob's pattern — absent where an expansion stands inside it (its text is not the pattern): `folded`, what the * program receives where it matches nothing — quotes removed, the spelling {@link LexedWord.folded} carries at its place * (`[^.]`); `written`, as written, quoting kept (`[^\.]`, `["!"a]`) — the text a shell re-reading it matches by; * `judged`, the spelling a reader judges the names it matches by — `folded`, save a class the judge reads as any one * character: one holding a quoted or escaped character (its quote-removed text reads another class: `["!"a]` is not * the negation `[!a]`) or a `[:…:]` / `[=…=]` / `[.….]` unit — spelled `?`, a superset of its members. */ readonly pattern?: { readonly folded: string; readonly written: string; readonly judged: 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; /** The spelling a reader judges the word's names by, where it is not `folded`: each bracket class holding a quoted or * escaped character or a `[:…:]` / `[=…=]` / `[.….]` unit spelled `?` (`["!"a]*`, `[[:alpha:]]*` ⇒ `?*`, * {@link WordExpansion.pattern}). */ readonly judged?: 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; /** The text a word's pieces hand the program when every piece is literal; `undefined` when one is an expansion or * unresolved. */ export declare function literalTextOf(pieces: readonly WordPiece[]): string | undefined; /** The text an UNQUOTED here-document body delivers when the shell expands nothing in it: a backslash before `$`, a * backquote or a backslash is removed (a line continuation was joined when the body was read), every other character * stands. `undefined` when an unescaped `$` or backquote stands in it: the program reads what the expansion makes. */ export declare function literalHeredocBody(body: string): string | 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[];