Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/semantics/conditions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Deciding conditions (docs/design/semantics.md, 4.14): what bash's `test` builtin gives for words whose values are
// known, and whether a pattern of `case` or `[[ == ]]` matches a known word. The walk of the shell state
// (src/semantics/state.ts) reads the words and calls these; anything not known here is `undefined`.
// (src/semantics/walk/conditions.ts) reads the words and calls these; anything not known here is `undefined`.
import { globToRegex, hasExtglob } from '../examples/shellwords';
import { parseTest, type TestExpr } from '../testexpr';

Expand Down
2 changes: 1 addition & 1 deletion src/semantics/effects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// What a builtin does to the shell's state, read from its parse by its spec (src/argv/spec.ts: `shellEffect`, and the
// values and operands of `type: 'variable'` or `'function'`). The walk of the state (src/semantics/state.ts) and the
// values and operands of `type: 'variable'` or `'function'`). The walk of the state (src/semantics/walk/) and the
// trust scan (src/semantics/trust.ts) both read the effects here, so that they never disagree on what a builtin sets.
import { interpret, type ArgRole, type ArgvParse } from '../argv/interpret';
import type { CommandSpec, ValueSpec, VariableSet } from '../argv/spec';
Expand Down
2 changes: 1 addition & 1 deletion src/semantics/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// - A subshell link comes from the node's place in the tree: `( )`, `$( )` and backquotes, `<( )` and `>( )`, a
// statement followed by `&`, a coproc's command, and every stage of a pipeline but the last one. The last stage
// is a subshell unless lastpipe is on and job control off, which the shell state where the pipeline runs says
// (`sure: false` when only some paths turn lastpipe on). The walk of src/semantics/state.ts drops the changes
// (`sure: false` when only some paths turn lastpipe on). The walk of the state (src/semantics/walk/) drops the changes
// of the same subshells, with the same rules (`subshellReasons`, `lastStageInShell`).
// - A function body runs where the function is called (the walk records the calls): its chain starts with a
// `call` link that holds the chains of its calls. A function no call reaches runs where it is defined.
Expand Down
383 changes: 383 additions & 0 deletions src/semantics/shellstate.ts

Large diffs are not rendered by default.

3,344 changes: 20 additions & 3,324 deletions src/semantics/state.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/semantics/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// an assignment, a pipeline's last stage (or with pipefail the last that failed; `!` negates it), the last command
// a list or a compound command ran, an arithmetic value, a number given to `exit` or `return`, a job's for `wait`.
// - `errexitOf(n)` says whether `set -e` stops the shell when the statement fails: errexit where it runs (bash turns
// it off in `$( )` unless inherit_errexit, which POSIX mode turns on: the walk of src/semantics/state.ts does it),
// it off in `$( )` unless inherit_errexit, which POSIX mode turns on: the walk, src/semantics/walk/walker.ts, does it),
// unless bash ignores it there: in the condition of `if`, `while` and `until`, left of `&&` and `||` (every
// command but the last of the list), after `!`, and in all of a function called from such a place (B2's calls).
// Lists and compound commands other than `( )` are not checked themselves: their commands are.
Expand Down
2 changes: 1 addition & 1 deletion src/semantics/trust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ class Scan {
if (hosted) return;
switch (name) {
case 'eval': {
// (The string is the words of its command slot, as the walk reads it: `Walker.builtin`.)
// (The string is the words of its command slot, as the walk reads it: `WalkBuiltins.builtin`.)
const at = parse.roles.find((r) => r.role.role === 'command');
if (at) this.foreign(this.strings.script(args.slice(at.word)), 'eval of text not known', w, true);
return;
Expand Down
2 changes: 1 addition & 1 deletion src/semantics/vars.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// What the walk of the shell state (src/semantics/state.ts) and the trust of its decisions (src/semantics/trust.ts)
// What the walk of the shell state (src/semantics/walk/) and the trust of its decisions (src/semantics/trust.ts)
// both read: declaration commands read with their builtin's spec, the catalog of the variables bash sets, and the names
// an arithmetic text assigns.
import { interpret, type ArgRole, type ArgvParse } from '../argv/interpret';
Expand Down
89 changes: 89 additions & 0 deletions src/semantics/walk/aliases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// The aliases of the walk of the shell state (src/semantics/walk/walker.ts): the `alias` commands of the script, and
// a command whose name is an alias, which runs the alias's text with the rest of the command, read again.
import { staticWordValue } from '../../staticword';
import { descendants, field, fields, type SyntaxNode, type SyntaxTree } from '../../syntax/tree';
import { commandName } from '../effects';
import { type AliasState, merge, option, type ShellState, withStatus } from '../shellstate';
import { parser } from './shared';
import type { Walker } from './walker';

/** Trees of alias texts, least recently used first; bounded, as a long-lived process (LSP, web) sees many texts. */
const aliasTrees = new Map<string, SyntaxTree>();
const ALIAS_TREES = 256;

/** `alias name=value` (or `'name=value'`): the name and what it stands for (static, or the text without its outer quotes). */
export function aliasOf(a: SyntaxNode): { name: string; value: string } | undefined {
const v = staticWordValue(a);
const text = v ?? a.text;
const m = /^([\w.:@%+-]+)=(['"]?)([\s\S]*)\2$/.exec(text);
if (!m) return undefined;
return { name: m[1], value: v !== undefined ? v.slice(v.indexOf('=') + 1) : m[3] };
}

/** The aliases the walk expands, and the `alias` commands that define them. */
export class WalkAliases {
constructor(private readonly w: Walker) {}

/** The `alias` commands that define each name, in the trees the walk reads. */
readonly defs = new Map<string, SyntaxNode[]>();
/** The aliases being expanded (an alias is not expanded again inside itself). */
private readonly expanding = new Set<string>();

/** Records the `alias` commands under `root`. */
collect(root: SyntaxNode): void {
for (const c of descendants(root, ['command'])) {
if (commandName(c) !== 'alias') continue;
for (const a of fields(c, 'argument')) {
const name = aliasOf(a)?.name;
const defs = name ? this.defs.get(name) ?? [] : [];
if (name && !defs.includes(c)) this.defs.set(name, [...defs, c]);
}
}
}

/**
* The alias `name` as something the script may have changed unseen (`Walker.widen`): `a`, its state in `st`, as not
* known; or, when `st` has none, the last `alias` command of the name, as one that may have run. Undefined when it
* is already so, or no command defines it.
*/
unknown(name: string, a: AliasState | undefined): AliasState | undefined {
if (a) return a.state !== 'unknown' ? { ...a, state: 'unknown' } : undefined;
const defs = this.defs.get(name);
const def = defs?.[defs.length - 1];
const value = def ? fields(def, 'argument').map(aliasOf).find((x) => x?.name === name)?.value : undefined;
return def && value !== undefined ? { state: 'unknown', value, at: def } : undefined;
}

/**
* The state after the simple command `n` named `name`, run from `st` once its words are expanded, when its name is
* an alias that may be expanded (in the state its aliases come from, `Walker.expand`); undefined otherwise.
*/
expand(n: SyntaxNode, name: string | undefined, st: ShellState): ShellState | undefined {
const nameNode = field(n, 'name')!;
const word = nameNode.namedChildren[0];
const from = this.w.expand;
const alias = name !== undefined && word.type === 'word' && word.text === name && !this.expanding.has(name) ? from.aliases.get(name) : undefined;
const expands = !this.w.bash ? 'on' : option(from, 'shopt', 'expand_aliases');
if (!alias || alias.state === 'off' || !(expands === 'on' || expands === 'unknown')) return undefined;
// The alias's text, then the rest of the command, read and run instead.
const tree = this.parseText(alias.value + n.text.slice(nameNode.endIndex - n.startIndex));
this.expanding.add(name!);
const host = this.w.aliasHost;
this.w.aliasHost ??= n;
const after = this.w.sequence(tree.root.children, st);
this.w.aliasHost = host;
this.expanding.delete(name!);
return alias.state === 'on' && expands === 'on' ? after : merge([withStatus(st, undefined), after], this.w.posix);
}

private parseText(text: string): SyntaxTree {
let tree = aliasTrees.get(text);
if (tree) aliasTrees.delete(text);
else {
tree = parser().parse(text);
if (aliasTrees.size >= ALIAS_TREES) aliasTrees.delete(aliasTrees.keys().next().value!);
}
aliasTrees.set(text, tree);
return tree;
}
}
Loading
Loading