@sema-agent/core 7.46.0 → 7.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/core/allow-entry.d.ts +1 -1
- package/dist/core/allow-entry.js +3 -4
- package/dist/core/ask-origin.d.ts +35 -12
- package/dist/core/ask-origin.js +23 -6
- package/dist/core/engine-notice.d.ts +7 -4
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/gate-lanes.js +2 -1
- package/dist/core/gate-outcome.d.ts +4 -2
- package/dist/core/gate-outcome.js +2 -2
- package/dist/core/permission-rule-model.d.ts +12 -6
- package/dist/core/permission-rule-model.js +10 -2
- package/dist/core/permission-rules.d.ts +4 -5
- package/dist/core/permission-rules.js +28 -15
- package/dist/core/runner/gate-exit.d.ts +4 -1
- package/dist/core/runner/gate-exit.js +5 -1
- package/dist/core/runner/prepare-hands-readface.js +5 -5
- package/dist/core/runner/prepare-policy-chain.js +2 -1
- package/dist/core/shell-wrapper-projection.js +1 -1
- package/dist/core/shell-wrapper-table.d.ts +6 -6
- package/dist/core/shell-wrapper-table.js +2 -2
- package/dist/core/tool-catalog-entries.js +3 -2
- package/dist/core/tool-face.d.ts +10 -1
- package/dist/core/tool-name-readings.d.ts +8 -0
- package/dist/core/tool-name-readings.js +2 -0
- package/dist/core/tool-policy.d.ts +10 -9
- package/dist/core/tool-policy.js +2 -2
- package/dist/core/tool-registry.d.ts +9 -0
- package/dist/core/tool-registry.js +10 -0
- package/dist/core/write-protect.d.ts +10 -3
- package/dist/core/write-protect.js +11 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shell/lex.d.ts +15 -0
- package/dist/shell/lex.js +58 -13
- package/dist/tools/fs/bash-program-position.d.ts +9 -6
- package/dist/tools/fs/bash-program-position.js +13 -10
- package/dist/tools/fs/program-read-model.d.ts +24 -7
- package/dist/tools/fs/program-read-model.js +7 -0
- package/dist/tools/fs/program-read-model.rows.js +18 -7
- package/dist/tools/fs/removal-targets.js +14 -4
- package/dist/tools/fs/shell-read-argv.d.ts +5 -4
- package/dist/tools/fs/shell-read-argv.js +5 -3
- package/dist/tools/fs/shell-read-bindings.d.ts +46 -19
- package/dist/tools/fs/shell-read-bindings.js +38 -29
- package/dist/tools/fs/shell-read-establish.js +3 -3
- package/dist/tools/fs/shell-read-plan.d.ts +2 -1
- package/dist/tools/fs/shell-read-plan.js +32 -36
- package/dist/tools/fs/shell-read-word.d.ts +38 -7
- package/dist/tools/fs/shell-read-word.js +26 -8
- package/dist/tools/fs/shell-read-write-targets.js +4 -3
- package/dist/tools/fs/shell-reentry-text.d.ts +98 -0
- package/dist/tools/fs/shell-reentry-text.js +115 -2
- package/dist/tools/monitor.d.ts +3 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +9 -3
- package/wire-changes/core@7.47.0.json +412 -0
|
@@ -2,6 +2,13 @@ import { argvOf, lexShell, literalTildeSpelling, substitutionsInHeredocBody } fr
|
|
|
2
2
|
import { readProgramPosition } from "./bash-program-position.js";
|
|
3
3
|
import { declaredAttributes, programRow, readArgRoles } from "./program-read-model.js";
|
|
4
4
|
import { codeBodies } from "./shell-reentry-text.js";
|
|
5
|
+
function sameValue(a, b) {
|
|
6
|
+
if (a.spelled !== b.spelled)
|
|
7
|
+
return false;
|
|
8
|
+
if (a.glob !== true || b.glob !== true)
|
|
9
|
+
return a.glob === b.glob;
|
|
10
|
+
return a.parts.length === b.parts.length && a.parts.every((p, i) => p.text === b.parts[i].text && p.pattern === b.parts[i].pattern);
|
|
11
|
+
}
|
|
5
12
|
export const SHELL_INTEGER_VARIABLES = new Set([
|
|
6
13
|
"RANDOM", "SECONDS", "LINENO", "OPTIND", "MAILCHECK", "HISTCMD", "SRANDOM", "EPOCHSECONDS", "EPOCHREALTIME", "BASHPID", "BASH_MONOSECONDS",
|
|
7
14
|
"BASH_TRAPSIG", "COLUMNS", "LINES", "SHLVL", "ERRNO", "TMOUT", "HISTSIZE", "SAVEHIST", "TRY_BLOCK_ERROR", "TRY_BLOCK_INTERRUPT", "KEYTIMEOUT",
|
|
@@ -199,16 +206,28 @@ function blockStructure(segments) {
|
|
|
199
206
|
const ONE_WORD = /^[A-Za-z0-9_./+@:%=,-]+$/;
|
|
200
207
|
const WORD_VALUES_MAX = 64;
|
|
201
208
|
export function wordValues(word, table, quoted, overlay = new Map()) {
|
|
202
|
-
|
|
203
|
-
if (!pieces.some((p) => p.expansion?.kind === "parameter" || p.expansion?.kind === "tilde"))
|
|
209
|
+
if (!word.pieces.some((p) => p.expansion?.kind === "parameter" || p.expansion?.kind === "tilde"))
|
|
204
210
|
return undefined;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
211
|
+
return valueOf(word, table, quoted, overlay);
|
|
212
|
+
}
|
|
213
|
+
const runsOf = (v) => (v.glob === true ? v.parts : [{ text: v.spelled, pattern: false }]);
|
|
214
|
+
function joinRuns(runs, more) {
|
|
215
|
+
const out = [...runs];
|
|
216
|
+
for (const r of more) {
|
|
217
|
+
const last = out[out.length - 1];
|
|
218
|
+
if (last !== undefined && last.pattern && r.pattern)
|
|
219
|
+
out[out.length - 1] = { text: last.text + r.text, pattern: true, judged: last.judged + r.judged };
|
|
220
|
+
else if (last !== undefined && !last.pattern && !r.pattern)
|
|
221
|
+
out[out.length - 1] = { text: last.text + r.text, pattern: false };
|
|
222
|
+
else if (r.text !== "")
|
|
223
|
+
out.push(r);
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
export function valueOf(word, table, quoted, overlay = new Map()) {
|
|
228
|
+
const stateOf = (name) => (table === undefined ? "unknown" : overlay.get(name) ?? table.stateOf(name));
|
|
229
|
+
let combos = [{ text: "", glob: false, parts: [] }];
|
|
230
|
+
for (const p of word.pieces) {
|
|
212
231
|
const e = p.expansion;
|
|
213
232
|
let options;
|
|
214
233
|
if (e === undefined) {
|
|
@@ -217,10 +236,9 @@ export function wordValues(word, table, quoted, overlay = new Map()) {
|
|
|
217
236
|
options = [{ spelled: p.literal }];
|
|
218
237
|
}
|
|
219
238
|
else if (e.kind === "glob") {
|
|
220
|
-
|
|
221
|
-
if (written === undefined)
|
|
239
|
+
if (e.pattern === undefined)
|
|
222
240
|
return "unknown";
|
|
223
|
-
options = [{ spelled:
|
|
241
|
+
options = [{ spelled: e.pattern.folded, glob: true, parts: [{ text: e.pattern.written, pattern: true, judged: e.pattern.judged }] }];
|
|
224
242
|
}
|
|
225
243
|
else if (e.kind === "tilde") {
|
|
226
244
|
if (e.body !== "~")
|
|
@@ -234,7 +252,7 @@ export function wordValues(word, table, quoted, overlay = new Map()) {
|
|
|
234
252
|
const state = stateOf(e.name);
|
|
235
253
|
if (state === "unknown")
|
|
236
254
|
return "unknown";
|
|
237
|
-
if (p.quotedBy === undefined && !quoted && (table
|
|
255
|
+
if (p.quotedBy === undefined && !quoted && (table?.ifsTouched === true || state.some((v) => v.glob === true || !ONE_WORD.test(v.spelled))))
|
|
238
256
|
return "unknown";
|
|
239
257
|
options = state;
|
|
240
258
|
}
|
|
@@ -243,15 +261,17 @@ export function wordValues(word, table, quoted, overlay = new Map()) {
|
|
|
243
261
|
const next = [];
|
|
244
262
|
for (const c of combos)
|
|
245
263
|
for (const o of options)
|
|
246
|
-
next.push({ text: c.text + o.spelled, glob: c.glob || o.glob === true });
|
|
264
|
+
next.push({ text: c.text + o.spelled, glob: c.glob || o.glob === true, parts: joinRuns(c.parts, runsOf(o)) });
|
|
247
265
|
if (next.length > WORD_VALUES_MAX)
|
|
248
266
|
return "unknown";
|
|
249
267
|
combos = next;
|
|
250
268
|
}
|
|
251
269
|
const out = [];
|
|
252
|
-
for (const c of combos)
|
|
253
|
-
|
|
254
|
-
|
|
270
|
+
for (const c of combos) {
|
|
271
|
+
const v = c.glob ? { spelled: c.text, glob: true, parts: c.parts } : { spelled: c.text };
|
|
272
|
+
if (!out.some((x) => sameValue(x, v)))
|
|
273
|
+
out.push(v);
|
|
274
|
+
}
|
|
255
275
|
return out;
|
|
256
276
|
}
|
|
257
277
|
export function wordFloorValues(word, table) {
|
|
@@ -284,17 +304,6 @@ export function wordFloorValues(word, table) {
|
|
|
284
304
|
}
|
|
285
305
|
return combos;
|
|
286
306
|
}
|
|
287
|
-
export function valueOf(word, table, quoted, overlay = new Map()) {
|
|
288
|
-
const values = wordValues(word, table, quoted, overlay);
|
|
289
|
-
if (values !== undefined)
|
|
290
|
-
return values;
|
|
291
|
-
if (word.pieces.every((p) => p.expansion === undefined && p.literal !== undefined))
|
|
292
|
-
return [{ spelled: word.pieces.map((p) => p.literal).join("") }];
|
|
293
|
-
const folded = word.folded;
|
|
294
|
-
if (folded !== undefined && word.pieces.every((p) => p.expansion === undefined ? p.literal !== undefined : p.expansion.kind === "glob"))
|
|
295
|
-
return [{ spelled: folded, glob: true }];
|
|
296
|
-
return "unknown";
|
|
297
|
-
}
|
|
298
307
|
export function bindingEffectOf(segment, argv, shape, readValue) {
|
|
299
308
|
const bindings = [];
|
|
300
309
|
let all = shape.definesFunction ? "reset" : undefined;
|
|
@@ -652,7 +661,7 @@ export class BindingThread {
|
|
|
652
661
|
return "unknown";
|
|
653
662
|
const values = [];
|
|
654
663
|
const add = (vs) => { for (const v of vs)
|
|
655
|
-
if (!values.some((x) => x
|
|
664
|
+
if (!values.some((x) => sameValue(x, v)))
|
|
656
665
|
values.push(v); };
|
|
657
666
|
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
658
667
|
const e = this.entries[i];
|
|
@@ -557,7 +557,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
557
557
|
for (const m of walkedTree.members)
|
|
558
558
|
out.push({ ...(await canonicalReadOf(candidate, m.path, [m.path], "expansion", folded, m.canonical, signal)), node: m.node, walked: true });
|
|
559
559
|
};
|
|
560
|
-
for (const lexical of lexicalReadsOf(candidate, boundary)) {
|
|
560
|
+
for (const [i, lexical] of lexicalReadsOf(candidate, boundary).entries()) {
|
|
561
561
|
if (lexical.established !== "lexical") {
|
|
562
562
|
out.push(lexical);
|
|
563
563
|
continue;
|
|
@@ -567,7 +567,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
567
567
|
const walked = spellings.find((s) => s !== folded) ?? folded;
|
|
568
568
|
if (candidate.expands === "glob") {
|
|
569
569
|
let matched = false;
|
|
570
|
-
for (const p of spellings) {
|
|
570
|
+
for (const p of candidate.pattern === undefined ? spellings : lexicalReadsOf({ ...candidate, spelled: candidate.pattern }, boundary)[i]?.spellings ?? spellings) {
|
|
571
571
|
const expanded = await expandGlobCandidate(p, roots, signal);
|
|
572
572
|
if (expanded !== undefined && "unverifiable" in expanded) {
|
|
573
573
|
out.push({ candidate, established: "unestablishable", spellings, origin: folded, cause: expanded.unverifiable });
|
|
@@ -581,7 +581,7 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
581
581
|
await land(read, spellings, folded);
|
|
582
582
|
}
|
|
583
583
|
}
|
|
584
|
-
if (!matched)
|
|
584
|
+
if (!matched || candidate.pattern !== undefined)
|
|
585
585
|
for (const read of await atSeats(walked, spellings, "canonical", undefined, undefined))
|
|
586
586
|
await land(read, spellings, folded);
|
|
587
587
|
continue;
|
|
@@ -49,8 +49,9 @@ export interface ShellReadDeclarations {
|
|
|
49
49
|
readonly face?: "open" | "roots";
|
|
50
50
|
}
|
|
51
51
|
export interface ReadCandidate {
|
|
52
|
-
/** The folded word (quotes removed, a literal leading tilde spelled `./~`). */
|
|
52
|
+
/** The folded word (quotes removed, a literal leading tilde spelled `./~`) — a pattern's name where nothing matches. */
|
|
53
53
|
readonly spelled: string;
|
|
54
|
+
/** The spelling a pattern's names are matched by where the matcher widens a class (`["!"a]`, `[[:alpha:]]`: any one character). */ readonly pattern?: string;
|
|
54
55
|
/** The raw spelling, quotes retained — where "is this metacharacter quoted" is still answerable. */
|
|
55
56
|
readonly raw: string;
|
|
56
57
|
readonly provenance: ReadCandidateProvenance;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { lexShell, PARSE_FAILURE_SENTENCE, substitutionsInHeredocBody, literalTextOf, literalTildeSpelling, } from "../../shell/lex.js";
|
|
2
|
-
import { readOfText, readsOf, rewritten, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js";
|
|
2
|
+
import { programTextOf, readOfText, readsOf, renamed, rewritten, settledFieldsOf, spelledWithHome, isArithmeticBody, unreadableSentenceOf } from "./shell-read-word.js";
|
|
3
3
|
import { assignmentArithmetic, bare, listedVerbCandidates, pathSubcommandOf, programTextSentence, pushAssignment, roleReads, subscriptOnly, unlistedWords } from "./shell-read-argv.js";
|
|
4
4
|
import { ARITHMETIC_TEST_OPERATORS, BindingThread, arithmeticSubstitutions, isArithmeticCommand, bindingEffectOf, parameterBodySubstitutions, projectSegment, valueOf, } from "./shell-read-bindings.js";
|
|
5
5
|
import { programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
|
|
6
6
|
import { POLL_LOOP_MAX_BEATS, attachedOptionPayloads, resolveOperandLexically, isPathShapedToken, programTextSlotFiles, } from "./bash-readonly-classifier.js";
|
|
7
7
|
import { BLOCK_WORD_NAMES, commandLineOptionSpellings, declaredAttributes, isDeclarationBuiltin, isInertBuiltin, launcherOptionPathRole, launcherOptionStep, readOptionRole, programLanguageOf, programTextOpensOf, selectedOptions, movesBaseUnfollowed, opensNoOperand, programDelivery, programRow, readArgRoles, runsShellText, } from "./program-read-model.js";
|
|
8
8
|
import { isAbsolutePathForm } from "./safety.js";
|
|
9
|
-
import { codeBodies, pipedTextOf, reentryText, scriptFileAt, stdinTextsOf } from "./shell-reentry-text.js";
|
|
9
|
+
import { codeBodies, launchedInput, pipedTextOf, reentryText, scriptFileAt, stdinTextsOf } from "./shell-reentry-text.js";
|
|
10
10
|
export const READ_CANDIDATE_PROVENANCES = Object.freeze([
|
|
11
11
|
"operand",
|
|
12
12
|
"option_value",
|
|
@@ -271,7 +271,7 @@ function segmentFacts(segment, p, decl, table, nestedPlan, piped) {
|
|
|
271
271
|
const each = (c) => {
|
|
272
272
|
const { alternatives, floors, ...one } = c;
|
|
273
273
|
const shadows = (floors ?? []).map((v) => ({ spelled: v.spelled, raw: one.raw, provenance: one.provenance, expands: v.glob === true ? "glob" : false, denyOnly: true }));
|
|
274
|
-
return [...[one, ...(alternatives ?? []).map((
|
|
274
|
+
return [...[one, ...(alternatives ?? []).map((a) => ({ ...renamed(one, a.spelled), ...a }))].map(unsettle), ...shadows];
|
|
275
275
|
};
|
|
276
276
|
const move = facts.baseMove;
|
|
277
277
|
return {
|
|
@@ -460,6 +460,20 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
|
|
|
460
460
|
let reentry;
|
|
461
461
|
if (pos.argvFromStdin)
|
|
462
462
|
reentry = "argv";
|
|
463
|
+
const reenterShell = (verb, argvs) => {
|
|
464
|
+
const stdin = stdinTextsOf(segment, piped);
|
|
465
|
+
const scripts = argvs.flatMap(({ shell, texts: t, args: a }) => [reentryText(shell, t), reentryText(shell, a), ...stdin.map((s) => (s.literal === undefined ? undefined : reentryText(shell, t, s.literal)))]);
|
|
466
|
+
for (const t of new Set(scripts))
|
|
467
|
+
if (t !== undefined)
|
|
468
|
+
nested.push(nestedPlan(t, "later"));
|
|
469
|
+
for (const { shell, args: a, words } of argvs) {
|
|
470
|
+
const at = shell === "source" || shell === "." ? a.findIndex((w) => !w.startsWith("-") || w === "-") : scriptFileAt(shell, a);
|
|
471
|
+
const file = at === undefined ? undefined : words[at];
|
|
472
|
+
if (file !== undefined)
|
|
473
|
+
reads.push(bare(file, "operand"));
|
|
474
|
+
}
|
|
475
|
+
return done({ empty: false, launcherChain, verb, reentry: "shell", declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), ...(stdin.length > 0 ? { stdin } : {}) });
|
|
476
|
+
};
|
|
463
477
|
if (pos.refusal !== undefined) {
|
|
464
478
|
const commandLine = commandLineOptionSpellings(programRow(programBasename(pos.refusal.owner)));
|
|
465
479
|
if (pos.refusal.kind === "option_at_program_position" && commandLine.length > 0) {
|
|
@@ -472,16 +486,16 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
|
|
|
472
486
|
}
|
|
473
487
|
}
|
|
474
488
|
if (programPositionRunsShell(pos, runsShellText)) {
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return
|
|
489
|
+
const launched = launchedInput(p.argv, pos, { text: (k) => literalTextOf(toks[k].word.pieces) ?? programTextOf(toks[k]), folded: (k) => programTextOf(toks[k]), values: (k) => settledFieldsOf(toks[k]) });
|
|
490
|
+
for (const k of launched.programWords)
|
|
491
|
+
if (isPathShapedToken(toks[k].folded))
|
|
492
|
+
reads.push(bare(toks[k], "program_path"));
|
|
493
|
+
for (const text of launched.lines)
|
|
494
|
+
nested.push(nestedPlan(text, "substitution"));
|
|
495
|
+
const argvs = launched.shells.map((a) => ({ ...a, words: a.words.map((k) => (k === undefined ? undefined : toks[k])) }));
|
|
496
|
+
if (argvs.length === 0)
|
|
497
|
+
return done({ empty: false, launcherChain, reentry: "shell", declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
|
|
498
|
+
return reenterShell({ name: argvs[0].shell, basename: argvs[0].shell, listed: decl.readers.has(argvs[0].shell) }, argvs);
|
|
485
499
|
}
|
|
486
500
|
const from = pos.launchers[0]?.index ?? written;
|
|
487
501
|
unlistedWords(reads, toks, from, launcherIndices, "unlisted_word", ctx);
|
|
@@ -493,31 +507,13 @@ function segmentFactsRead(segment, p, decl, table, nestedPlan, piped) {
|
|
|
493
507
|
return done({ empty: false, launcherChain, ...(reentry !== undefined ? { reentry } : {}), declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}) });
|
|
494
508
|
}
|
|
495
509
|
const argReads = toks.slice(program.index + 1);
|
|
496
|
-
const args = argReads.map(
|
|
497
|
-
const texts = argReads.map((r) => literalTextOf(r.word.pieces) ?? r
|
|
510
|
+
const args = argReads.map(programTextOf);
|
|
511
|
+
const texts = argReads.map((r) => literalTextOf(r.word.pieces) ?? programTextOf(r));
|
|
498
512
|
const programRead = toks[program.index];
|
|
499
513
|
if (rewritten(programRead) || isPathShapedToken(programRead.folded))
|
|
500
514
|
reads.push(bare(programRead, "program_path"));
|
|
501
|
-
if (runsShellText(basename))
|
|
502
|
-
|
|
503
|
-
const stdin = stdinTextsOf(segment, piped);
|
|
504
|
-
const text = reentryText(basename, texts);
|
|
505
|
-
const scripts = [text, reentryText(basename, args), ...stdin.map((s) => (s.literal === undefined ? undefined : reentryText(basename, texts, s.literal)))];
|
|
506
|
-
for (const t of new Set(scripts))
|
|
507
|
-
if (t !== undefined)
|
|
508
|
-
nested.push(nestedPlan(t, "later"));
|
|
509
|
-
if (basename === "source" || basename === ".") {
|
|
510
|
-
const file = argReads.find((a) => !a.folded.startsWith("-") || a.folded === "-");
|
|
511
|
-
if (file !== undefined)
|
|
512
|
-
reads.push(bare(file, "operand"));
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
const file = scriptFileAt(basename, args);
|
|
516
|
-
if (file !== undefined)
|
|
517
|
-
reads.push(bare(argReads[file], "operand"));
|
|
518
|
-
}
|
|
519
|
-
return done({ empty: false, launcherChain, verb: verb, reentry, declines, reads, moveProvable: false, ...(loopHead !== undefined ? { loopHead } : {}), ...(stdin.length > 0 ? { stdin } : {}) });
|
|
520
|
-
}
|
|
515
|
+
if (runsShellText(basename))
|
|
516
|
+
return reenterShell(verb, [{ shell: basename, texts, args, words: argReads }]);
|
|
521
517
|
const row = programRow(basename);
|
|
522
518
|
const delivery = programDelivery(row, args);
|
|
523
519
|
if (delivery !== undefined)
|
|
@@ -665,7 +661,7 @@ function commandStringValue(options, toks, option) {
|
|
|
665
661
|
return undefined;
|
|
666
662
|
for (const spelling of options) {
|
|
667
663
|
if (option === spelling) {
|
|
668
|
-
const value = toks[at + 1]
|
|
664
|
+
const value = toks[at + 1] === undefined ? undefined : programTextOf(toks[at + 1]);
|
|
669
665
|
return value === undefined ? undefined : { value, next: at + 2 };
|
|
670
666
|
}
|
|
671
667
|
if (spelling.startsWith("--") && option.startsWith(`${spelling}=`))
|
|
@@ -5,34 +5,65 @@
|
|
|
5
5
|
* does to the word before the program sees it.
|
|
6
6
|
*/
|
|
7
7
|
import { type LexedCommand, type LexedWord, type WordPiece } from "../../shell/lex.js";
|
|
8
|
-
import { type BindingTable, type BoundValue } from "./shell-read-bindings.js";
|
|
8
|
+
import { type BindingTable, type BoundValue, type FloorValue } from "./shell-read-bindings.js";
|
|
9
9
|
/** `"glob"` = an unquoted pathname-expansion character; `"unresolvable"` = the shell rewrites the word (a brace,
|
|
10
10
|
* variable or command substitution) so its text is not the path. The read candidate's own field, spelled here. */
|
|
11
11
|
export type WordExpands = false | "glob" | "unresolvable";
|
|
12
|
-
/** A word as the candidate readers name it: the lexer's word, the spelling
|
|
13
|
-
* refers to read through the binding table ({@link wordValues}: `"$F"` after `F=/x` is `/x
|
|
14
|
-
* it before the program sees it.
|
|
12
|
+
/** A word as the candidate readers name it: the lexer's word, the spelling a reader names it by — the names the word
|
|
13
|
+
* refers to read through the binding table ({@link wordValues}: `"$F"` after `F=/x` is `/x`; {@link spellingOf}) — and
|
|
14
|
+
* what the shell does to it before the program sees it. A text handed to a shell that re-parses it is the program's
|
|
15
|
+
* ({@link programTextOf}), never this one. */
|
|
15
16
|
export interface Read {
|
|
16
17
|
readonly word: LexedWord;
|
|
17
18
|
readonly raw: string;
|
|
18
19
|
readonly folded: string;
|
|
20
|
+
/** A pattern's names are matched by this spelling where it is not {@link folded} (a class the judge widens, read as
|
|
21
|
+
* any one character — {@link LexedWord.judged}, {@link patternOf}); `folded` stays the name opened where none match. */
|
|
22
|
+
readonly pattern?: string;
|
|
19
23
|
readonly expands: WordExpands;
|
|
20
24
|
/** The names the word refers to were read through the table: its expansion IS the spelling above (and, where the
|
|
21
25
|
* names may hold several values, {@link values}). */
|
|
22
26
|
readonly settled?: true;
|
|
23
|
-
/** Every
|
|
24
|
-
|
|
27
|
+
/** Every value the word may expand to (the first spelled {@link folded}, {@link spellingOf}; matched by {@link pattern}),
|
|
28
|
+
* each with the parts of it the shell expands as a pattern ({@link BoundValue}); present wherever the names were read
|
|
29
|
+
* through the table ({@link settled}). */
|
|
30
|
+
readonly values?: readonly BoundValue[];
|
|
25
31
|
/** The word refers to `HOME` and `HOME` may hold something other than the declared home here: the spelling the
|
|
26
32
|
* word has with the declared home, for the deny judge alone. */
|
|
27
33
|
readonly homeFloor?: string;
|
|
28
34
|
/** The word's names are unknown here, but their bindings left spellings for the deny judge (an array's elements):
|
|
29
35
|
* what the word may then spell, for the deny judge alone. */
|
|
30
|
-
readonly floorValues?: readonly
|
|
36
|
+
readonly floorValues?: readonly FloorValue[];
|
|
31
37
|
}
|
|
32
38
|
/** The reads of a segment's words against the binding table at that segment (`homeDir` = the declared home, the
|
|
33
39
|
* state `HOME` starts in). A word that refers to no name, or only to a `HOME` the text has not touched (`~/x`, placed
|
|
34
40
|
* by the establishment at the declared home), reads as written. */
|
|
35
41
|
export declare function readsOf(words: readonly LexedWord[], table: BindingTable | undefined, homeDir: string | undefined): Read[];
|
|
42
|
+
/** The spelling a reader names a value by, read directly as a path — for a pattern, the name the program opens where it
|
|
43
|
+
* matches nothing: its text, a leading `~` spelled as the literal tilde it is ({@link literalTildeSpelling}, the lexer's
|
|
44
|
+
* spelling of a written one) — the shell never tilde-expands what a quoted piece or an expansion produced, so a value that
|
|
45
|
+
* starts with `~` starts with a literal one (`X=/w; rm \~$X/*` reads `./~/w/*`, as `rm \~/w/*` does). A shell that
|
|
46
|
+
* re-parses the value reads {@link programTextOf} instead. */
|
|
47
|
+
export declare const spellingOf: (v: BoundValue) => string;
|
|
48
|
+
/** The spelling a pattern value's names are matched by, where it is not {@link spellingOf}: its runs as a reader judges
|
|
49
|
+
* them (a class the judge widens as `?`, the lexer's `WordExpansion.pattern.judged`) — a candidate's
|
|
50
|
+
* `pattern`. */
|
|
51
|
+
export declare function patternOf(v: BoundValue): string | undefined;
|
|
52
|
+
/** A candidate under another name (a value of several, a backup, a copy's child): the word's `pattern` matched the word,
|
|
53
|
+
* not that name — the name is matched by its own text. */
|
|
54
|
+
export declare function renamed<C extends {
|
|
55
|
+
readonly spelled: string;
|
|
56
|
+
readonly pattern?: string;
|
|
57
|
+
}>(c: C, spelled: string): Omit<C, "pattern">;
|
|
58
|
+
/** The text a read's word hands a program, as a shell re-parsing it reads it (`eval`, `sh -c`, a launcher's line): its
|
|
59
|
+
* first established value as the program receives it, or the lexer's folded spelling — never {@link spellingOf}'s path
|
|
60
|
+
* spelling (`X='~'; eval rm -rf "$X"` re-parses `rm -rf ~`, the home). */
|
|
61
|
+
export declare const programTextOf: (r: Read) => string;
|
|
62
|
+
/** What a read's word hands the program, where the reader knows it: each value its names settled into — one field each
|
|
63
|
+
* (an unquoted expansion whose value is not one word is never settled, {@link wordValues}), or, where the word is a
|
|
64
|
+
* pattern, the pattern whose matches are the fields, its parts telling pattern characters from literal ones;
|
|
65
|
+
* `undefined` for a word the table did not settle (or that names nothing). */
|
|
66
|
+
export declare function settledFieldsOf(r: Read): readonly BoundValue[] | undefined;
|
|
36
67
|
/** The shell REWRITES this read's word before the program sees it (a parameter, command, process, arithmetic or
|
|
37
68
|
* brace expansion, or the character of one) — a word whose names the table settled excepted: its expansion is its
|
|
38
69
|
* spelling. */
|
|
@@ -1,33 +1,51 @@
|
|
|
1
|
-
import { argvOf, lexShell, UNRESOLVED_SENTENCE } from "../../shell/lex.js";
|
|
1
|
+
import { argvOf, lexShell, literalTildeSpelling, UNRESOLVED_SENTENCE } from "../../shell/lex.js";
|
|
2
2
|
import { expansionInWord, extglobOpenerIn, globInWord, homeExpansionOf } from "./bash-readonly-classifier.js";
|
|
3
3
|
import { wordFloorValues, wordValues } from "./shell-read-bindings.js";
|
|
4
4
|
export function readsOf(words, table, homeDir) {
|
|
5
5
|
return words.map((word) => {
|
|
6
6
|
const refersToName = word.pieces.some((p) => p.expansion?.kind === "parameter");
|
|
7
|
+
const asWritten = { word, raw: word.raw, folded: word.folded, ...(word.judged !== undefined ? { pattern: word.judged } : {}) };
|
|
7
8
|
if (!refersToName && table?.homeTouched !== true)
|
|
8
|
-
return {
|
|
9
|
+
return { ...asWritten, expands: expandsOf(word) };
|
|
9
10
|
const values = wordValues(word, table, false);
|
|
10
11
|
if (values === undefined)
|
|
11
|
-
return {
|
|
12
|
+
return { ...asWritten, expands: expandsOf(word) };
|
|
12
13
|
if (values === "unknown") {
|
|
13
14
|
const floors = refersToName ? wordFloorValues(word, table) : [];
|
|
14
|
-
return {
|
|
15
|
+
return { ...asWritten, expands: refersToName ? "unresolvable" : expandsOf(word), ...(floors.length > 0 ? { floorValues: floors } : {}) };
|
|
15
16
|
}
|
|
16
17
|
const glob = values.some((v) => v.glob === true) || globInWord(word);
|
|
17
18
|
const refersToHome = word.pieces.some((p) => p.expansion?.kind === "tilde" || (p.expansion?.kind === "parameter" && p.expansion.name === "HOME"));
|
|
18
19
|
const declared = refersToHome && homeDir !== undefined && table?.homeTouched === true ? wordValues(word, table, false, new Map([["HOME", [{ spelled: homeDir }]]])) : undefined;
|
|
19
|
-
const floor = declared !== undefined && declared !== "unknown" ? declared[0]
|
|
20
|
+
const floor = declared !== undefined && declared !== "unknown" && declared[0] !== undefined ? spellingOf(declared[0]) : undefined;
|
|
21
|
+
const pattern = patternOf(values[0]);
|
|
20
22
|
return {
|
|
21
23
|
word,
|
|
22
24
|
raw: word.raw,
|
|
23
|
-
folded: values[0]
|
|
25
|
+
folded: spellingOf(values[0]),
|
|
26
|
+
...(pattern !== undefined ? { pattern } : {}),
|
|
24
27
|
expands: glob ? "glob" : false,
|
|
25
28
|
settled: true,
|
|
26
|
-
|
|
27
|
-
...(floor !== undefined && !values.every((v) => v
|
|
29
|
+
values,
|
|
30
|
+
...(floor !== undefined && !values.every((v) => spellingOf(v) === floor) ? { homeFloor: floor } : {}),
|
|
28
31
|
};
|
|
29
32
|
});
|
|
30
33
|
}
|
|
34
|
+
export const spellingOf = (v) => literalTildeSpelling(v.spelled);
|
|
35
|
+
export function patternOf(v) {
|
|
36
|
+
if (v.glob !== true)
|
|
37
|
+
return undefined;
|
|
38
|
+
const judged = literalTildeSpelling(v.parts.map((p) => (p.pattern ? p.judged : p.text)).join(""));
|
|
39
|
+
return judged === spellingOf(v) ? undefined : judged;
|
|
40
|
+
}
|
|
41
|
+
export function renamed(c, spelled) {
|
|
42
|
+
const { pattern: _matched, ...rest } = c;
|
|
43
|
+
return { ...rest, spelled };
|
|
44
|
+
}
|
|
45
|
+
export const programTextOf = (r) => r.values?.[0]?.spelled ?? r.word.folded;
|
|
46
|
+
export function settledFieldsOf(r) {
|
|
47
|
+
return r.settled === true ? r.values : undefined;
|
|
48
|
+
}
|
|
31
49
|
export const rewritten = (r) => r.settled !== true && expansionInWord(r.word);
|
|
32
50
|
export function readOfText(text) {
|
|
33
51
|
const l = lexShell(text);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { denyFloorShadowsOf } from "./shell-read-judge.js";
|
|
2
|
+
import { renamed } from "./shell-read-word.js";
|
|
2
3
|
import { implementationsOf, programRow, selectedOptions, writeReadings } from "./program-read-model.js";
|
|
3
4
|
import { programBasename, readProgramPosition } from "./bash-program-position.js";
|
|
4
5
|
export function programArgs(segment) {
|
|
@@ -85,8 +86,8 @@ function backupTargetsOf(edited, readings, override) {
|
|
|
85
86
|
const suffixes = [...new Set(readings.flatMap((r) => r.suffixes))];
|
|
86
87
|
const follows = override !== undefined && !override.includes("entry");
|
|
87
88
|
return edited.flatMap(({ candidate: f, onlyIfExists }) => suffixes.flatMap((suffix) => follows
|
|
88
|
-
? [{ candidate: { ...f,
|
|
89
|
-
: sedBackupNames(f.spelled, suffix).map((spelled) => ({ candidate: { ...f, spelled, face: "write" }, seats: ["entry"], ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) }))));
|
|
89
|
+
? [{ candidate: { ...renamed(f, `${f.spelled}${suffix}`), face: "write" }, seats: ["entry"], followed: { operand: f, suffix }, ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) }]
|
|
90
|
+
: sedBackupNames(f.spelled, suffix).map((spelled) => ({ candidate: { ...renamed(f, spelled), face: "write" }, seats: ["entry"], ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) }))));
|
|
90
91
|
}
|
|
91
92
|
function copyDestinationChildren(segment, words, readings) {
|
|
92
93
|
const candidateOf = (raw) => segment.reads.find((c) => c.raw === raw && WORD_PROVENANCES.has(c.provenance));
|
|
@@ -107,7 +108,7 @@ function copyDestinationChildren(segment, words, readings) {
|
|
|
107
108
|
const child = `${dest.spelled.replace(/\/+$/, "")}/${src.spelled.replace(/\/+$/, "").split("/").pop() ?? ""}`;
|
|
108
109
|
if (child.endsWith("/") || children.has(child))
|
|
109
110
|
continue;
|
|
110
|
-
children.set(child, destCandidate !== undefined && source !== undefined ?
|
|
111
|
+
children.set(child, destCandidate !== undefined && source !== undefined ? renamed(destCandidate, child) : { spelled: child, raw: src.raw, provenance: "unlisted_word", expands: "unresolvable", bases: [] });
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
return [...children.values()];
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* re-entry (`shell-read-plan.ts`); this module is the one reader of which operand — or which input — that text is.
|
|
6
6
|
*/
|
|
7
7
|
import { type LexedSegment, type LexedWord } from "../../shell/lex.js";
|
|
8
|
+
import { type ProgramPosition } from "./bash-program-position.js";
|
|
9
|
+
import type { BoundValue } from "./shell-read-bindings.js";
|
|
8
10
|
/**
|
|
9
11
|
* The code bodies a builtin keeps among its operands, one per body; empty for every other program. The program read
|
|
10
12
|
* model's row says where the body is: the action of a `trap` (its first operand after the options), the value of an
|
|
@@ -24,6 +26,102 @@ export declare function codeBodies(basename: string, args: readonly string[]): r
|
|
|
24
26
|
* the argv names no text and the program runs its standard input ({@link scriptSourceOf}: the row runs it unnamed, or
|
|
25
27
|
* the script file named is standard input), the text is `stdin` — one text that may arrive there, as the caller spells it. */
|
|
26
28
|
export declare function reentryText(basename: string, args: readonly string[], stdin?: string): string | undefined;
|
|
29
|
+
/** One argument of the argv a launcher hands the SHELL it runs: a word of the command line passed on as it stands (`at`,
|
|
30
|
+
* its argv index; `from` — only the word's characters from there on, an option value attached to its option word), a word
|
|
31
|
+
* of the launcher's own (`fixed`: the `-c` in front of a text it builds), or a text it builds from several words
|
|
32
|
+
* (`joined`: their argv indices, joined by a space; `escaped` — every character of a word's value but a letter, a digit,
|
|
33
|
+
* `_`, `-` and `$` escaped with a backslash, as `sudo -s` builds its shell's `-c` text). */
|
|
34
|
+
export type LaunchedArg = {
|
|
35
|
+
readonly at: number;
|
|
36
|
+
readonly from?: number;
|
|
37
|
+
} | {
|
|
38
|
+
readonly fixed: string;
|
|
39
|
+
} | {
|
|
40
|
+
readonly joined: readonly number[];
|
|
41
|
+
readonly escaped?: true;
|
|
42
|
+
};
|
|
43
|
+
/** What a launcher that runs a program named by no word of its own runs: `program` — absent, a SHELL named by no word
|
|
44
|
+
* (sudo's, chroot's, the user's login shell `su` runs); `at` — the word naming it (`su -s PROGRAM`, from its `from`-th
|
|
45
|
+
* character where the value is attached to its option word); `spelled` — a program the launcher takes from elsewhere,
|
|
46
|
+
* as a shell spells it (`su -m`: the caller's `"$SHELL"`) — and the argv it hands that program. */
|
|
47
|
+
export interface LaunchedCommand {
|
|
48
|
+
readonly program?: {
|
|
49
|
+
readonly at: number;
|
|
50
|
+
readonly from?: number;
|
|
51
|
+
} | {
|
|
52
|
+
readonly spelled: string;
|
|
53
|
+
};
|
|
54
|
+
readonly args: readonly LaunchedArg[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What a launcher that runs a program named by no word of its own runs ({@link readProgramPosition}'s `runsShell`: the
|
|
58
|
+
* launcher, and the first word it hands on), one command per reading of the launcher's row where its implementations
|
|
59
|
+
* read it differently; empty when no launcher of the segment runs one. A shell's argv is read by that shell's row
|
|
60
|
+
* ({@link reentryText}, {@link scriptFileAt}): its `-c` text, its script file, else its standard input. By the launcher
|
|
61
|
+
* row (`LauncherSpec`, program-read-model.ts):
|
|
62
|
+
* · no word handed on (`sudo -s`, `chroot DIR`, a bare `su`): the shell is handed nothing;
|
|
63
|
+
* · `shellCommand: "escaped"` (`sudo -s CMD…`): `-c` and the words, escaped and joined;
|
|
64
|
+
* · `shellText: "joined"` (`watch CMD…`): `-c` and the non-option words, joined — and `-c` with each shell-text value
|
|
65
|
+
* attached to its option word (`script -c'CMD'`);
|
|
66
|
+
* · `shellText: "user"` (`su [-] [USER [ARG…]]`): per implementation — its first operand past a lone `-` names the user;
|
|
67
|
+
* one that reads its options anywhere hands `-c TEXT` for each shell-text value of its own (a command each) and its
|
|
68
|
+
* operands after the user, one that stops at the user hands every word after the user. The program is the value of
|
|
69
|
+
* its option of role `command` (`-s PROGRAM`), else the program a selected `names-program` switch takes from the
|
|
70
|
+
* caller's environment (`-m`: `"$SHELL"`), else the user's login shell.
|
|
71
|
+
*/
|
|
72
|
+
export declare function launchedCommands(argv: readonly LexedWord[], pos?: ProgramPosition): readonly LaunchedCommand[];
|
|
73
|
+
/** How a launched command is read: as a SHELL's argv (`shell`: the shell whose row reads it — a program that runs its
|
|
74
|
+
* `-c` text, `sh`, `bash`, `fish`, …, or `sh` for the shell named by no word), as the command line it is (`line`: a
|
|
75
|
+
* program that is no shell, `su -s /bin/rm …`, its words as written, so its expansions are the command line's), or
|
|
76
|
+
* both — a program the text does not spell (`su -s "$X"`, `su -m`: the caller's `"$SHELL"`) may be any program, a shell
|
|
77
|
+
* among them. */
|
|
78
|
+
export declare function launchedReadings(argv: readonly LexedWord[], command: LaunchedCommand): {
|
|
79
|
+
readonly shell?: string;
|
|
80
|
+
readonly line?: string;
|
|
81
|
+
};
|
|
82
|
+
/** What a launcher runs ({@link launchedCommands}, {@link launchedReadings}), as a reader of the command line reads it:
|
|
83
|
+
* each shell's argv in two spellings — `texts` (a word's literal value, else `spell.text`) and `args` (`spell.folded`) —
|
|
84
|
+
* with the argv index each position stands for (`words`), one argv per reading of the values its words may take
|
|
85
|
+
* (`spell.values`, {@link spellLaunched}); the word naming a shell (`su -s /bin/bash`, `programWords`); and each command
|
|
86
|
+
* line a program that is no shell runs (`lines`, once each). */
|
|
87
|
+
export declare function launchedInput(argv: readonly LexedWord[], pos: ProgramPosition, spell: {
|
|
88
|
+
readonly text: (k: number) => string;
|
|
89
|
+
readonly folded: (k: number) => string;
|
|
90
|
+
readonly values: (k: number) => readonly BoundValue[] | undefined;
|
|
91
|
+
}): {
|
|
92
|
+
readonly shells: readonly {
|
|
93
|
+
readonly shell: string;
|
|
94
|
+
readonly texts: readonly string[];
|
|
95
|
+
readonly args: readonly string[];
|
|
96
|
+
readonly words: readonly (number | undefined)[];
|
|
97
|
+
}[];
|
|
98
|
+
readonly programWords: readonly number[];
|
|
99
|
+
readonly lines: readonly string[];
|
|
100
|
+
};
|
|
101
|
+
/** How a reader spells the words of a command line it hands a launched shell ({@link spellLaunched}): `word` — a word as
|
|
102
|
+
* it stands, from its `from`-th character; `values` — for a word the shell rewrites before the launcher sees it, the
|
|
103
|
+
* values it may take where the reader knows them (each one field, or a pattern whose matches are the fields — its parts
|
|
104
|
+
* say which characters the shell expands), `undefined` where it does not; absent — the reader knows none. */
|
|
105
|
+
export interface LaunchedSpelling {
|
|
106
|
+
readonly word: (at: number, from: number) => string;
|
|
107
|
+
readonly values?: (at: number) => readonly BoundValue[] | undefined;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The argv a launched shell is handed ({@link launchedCommands}), spelled as `spell` spells the command line's words — one
|
|
111
|
+
* argv per reading of the values its words may take. A word an escaping launcher joins (`sudo -s CMD…`) is handed as the
|
|
112
|
+
* launcher hands it: each FIELD the command line's shell gives the launcher, escaped ({@link sudoEscaped}) — the field a
|
|
113
|
+
* literal word spells, or each field the shell established for a word it rewrites first (`X='#'; sudo -s rm -r "$X" d`
|
|
114
|
+
* hands `rm -r \# d`: the value is an operand, never a comment), a pattern's literal characters escaped and its pattern
|
|
115
|
+
* characters bare ({@link launchedSpellingOf}). A word whose fields the reader does not know is handed AS WRITTEN: the
|
|
116
|
+
* launched shell re-reads the expansion with the command line's quoting, so the word stays one argument (an unknown there)
|
|
117
|
+
* and its neighbours stay theirs (`"#$Y" d` is never `#$Y d`). A reading gives each distinct word spelling one value — the
|
|
118
|
+
* same word spelled twice expands alike — and words with several values are read in every combination of them (a value in
|
|
119
|
+
* the program's position meets each operand); past {@link LAUNCHED_READINGS_MAX}
|
|
120
|
+
* combinations reading `i` hands each word its `i`-th value (its last where it holds fewer) — every value of every word is
|
|
121
|
+
* still read, the same word spelled twice takes one value per reading as the shell expands it, and the readings stay as
|
|
122
|
+
* many as one word's values: their cost is linear in the command, whatever its length.
|
|
123
|
+
*/
|
|
124
|
+
export declare function spellLaunched(argv: readonly LexedWord[], args: readonly LaunchedArg[], spell: LaunchedSpelling): string[][];
|
|
27
125
|
/** The argv index of the SCRIPT FILE a shell runs where its argv carries no text ({@link scriptSourceOf}). */
|
|
28
126
|
export declare function scriptFileAt(basename: string, args: readonly string[]): number | undefined;
|
|
29
127
|
/** One text that may arrive on a segment's standard input: `from` = its source as written (a here-document's body,
|