chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from typing import Any, Callable
|
||||
|
||||
from mirage.io import IOResult
|
||||
from mirage.io.stream import async_chain
|
||||
from mirage.shell.arith import evaluate_arith
|
||||
from mirage.shell.call_stack import CallStack
|
||||
from mirage.shell.errors import ArithError
|
||||
from mirage.shell.job_table import JobTable
|
||||
from mirage.shell.node_kind import NodeKind, node_kind
|
||||
from mirage.shell.types import ERREXIT_EXEMPT_TYPES
|
||||
from mirage.shell.types import NodeType as NT
|
||||
from mirage.workspace.abort import MirageAbortError
|
||||
from mirage.workspace.executor.control import (handle_case, handle_for,
|
||||
handle_if, handle_select,
|
||||
handle_until, handle_while)
|
||||
from mirage.workspace.executor.pipes import (handle_connection, handle_pipe,
|
||||
handle_subshell)
|
||||
from mirage.workspace.executor.redirect import handle_redirect
|
||||
from mirage.workspace.expand import (expand_and_classify, expand_node,
|
||||
expand_redirects)
|
||||
from mirage.workspace.expand.globs import resolve_globs
|
||||
from mirage.workspace.expand.node import expand_arith
|
||||
from mirage.workspace.mount import MountRegistry
|
||||
from mirage.workspace.mount.namespace import Namespace
|
||||
from mirage.workspace.node.command_dispatch import execute_command
|
||||
from mirage.workspace.node.program import execute_program
|
||||
from mirage.workspace.node.test_expr import expand_test_expr
|
||||
from mirage.workspace.session import Session
|
||||
from mirage.workspace.types import ExecutionNode
|
||||
|
||||
from mirage.shell.helpers import ( # isort: skip
|
||||
get_case_items, get_case_word, get_declaration_keyword, get_for_parts,
|
||||
get_function_body, get_function_name, get_if_branches, get_list_parts,
|
||||
get_negated_command, get_pipeline_commands, get_redirects,
|
||||
get_subshell_body, get_text, get_unset_names, get_while_parts)
|
||||
from mirage.workspace.executor.builtins import ( # isort: skip
|
||||
handle_export, handle_local, handle_readonly, handle_test, handle_unset)
|
||||
|
||||
|
||||
async def _recurse_reassociated(
|
||||
recurse: Callable,
|
||||
dispatch: Callable,
|
||||
execute_fn: Callable,
|
||||
registry: MountRegistry,
|
||||
redirects: list,
|
||||
right: Any,
|
||||
node: Any,
|
||||
session: Session,
|
||||
stdin: Any = None,
|
||||
call_stack: CallStack | None = None,
|
||||
) -> tuple[Any, IOResult, ExecutionNode]:
|
||||
"""Recurse wrapper for a re-associated trailing redirect.
|
||||
|
||||
Executes the list's last command with the hoisted redirects,
|
||||
expanding targets only at that point (after the left side ran, so
|
||||
cwd changes apply); every other node recurses normally.
|
||||
|
||||
Args:
|
||||
recurse (Callable): the plain execute_node recursion.
|
||||
dispatch (Callable): VFS op dispatcher.
|
||||
execute_fn (Callable): recursive execute (for expansions).
|
||||
registry (MountRegistry): mount registry.
|
||||
redirects (list): parsed redirects hoisted off the list.
|
||||
right (Any): the list's last command node.
|
||||
node (Any): node being executed by handle_connection.
|
||||
session (Session): shell session state.
|
||||
stdin (Any): input stream.
|
||||
call_stack (CallStack | None): shell call stack.
|
||||
"""
|
||||
if node is not right:
|
||||
return await recurse(node, session, stdin, call_stack)
|
||||
expanded, pipe_node = await expand_redirects(redirects, session,
|
||||
execute_fn, registry,
|
||||
call_stack)
|
||||
stdout, io, exec_node = await handle_redirect(recurse, dispatch, right,
|
||||
expanded, session, stdin,
|
||||
call_stack)
|
||||
if pipe_node is not None and stdout is not None:
|
||||
stdout, io2, exec_node2 = await recurse(pipe_node, session, stdout,
|
||||
call_stack)
|
||||
io = await io.merge(io2)
|
||||
exec_node = exec_node2
|
||||
return stdout, io, exec_node
|
||||
|
||||
|
||||
async def execute_node(
|
||||
dispatch: Callable,
|
||||
registry: MountRegistry,
|
||||
namespace: Namespace,
|
||||
job_table: JobTable,
|
||||
execute_fn: Callable,
|
||||
agent_id: str,
|
||||
node: Any,
|
||||
session: Session,
|
||||
stdin: Any = None,
|
||||
call_stack: CallStack | None = None,
|
||||
cancel: asyncio.Event | None = None,
|
||||
) -> tuple[Any, IOResult, ExecutionNode]:
|
||||
"""Walk tree-sitter AST and dispatch each node.
|
||||
|
||||
Args:
|
||||
dispatch (Callable): VFS op dispatcher (op, path, **kw).
|
||||
registry (MountRegistry): mount registry for path resolution.
|
||||
namespace (Namespace): addressing authority for symlink ops.
|
||||
job_table (JobTable): background job management.
|
||||
execute_fn (Callable): recursive execute (for source/eval).
|
||||
agent_id (str): current agent ID for jobs.
|
||||
node (Any): tree-sitter node to execute.
|
||||
session (Session): shell session state.
|
||||
stdin (Any): input stream.
|
||||
call_stack (CallStack): shell call stack.
|
||||
cancel (asyncio.Event | None): event used to abort mid-flight.
|
||||
"""
|
||||
if cancel is not None and cancel.is_set():
|
||||
raise MirageAbortError()
|
||||
cs = call_stack or CallStack()
|
||||
|
||||
recurse = partial(execute_node,
|
||||
dispatch,
|
||||
registry,
|
||||
namespace,
|
||||
job_table,
|
||||
execute_fn,
|
||||
agent_id,
|
||||
cancel=cancel)
|
||||
|
||||
kind = node_kind(node)
|
||||
|
||||
if kind == NodeKind.COMMENT:
|
||||
return None, IOResult(), ExecutionNode(command="", exit_code=0)
|
||||
|
||||
# ── program (root / semicolons) ─────────────
|
||||
if kind == NodeKind.PROGRAM:
|
||||
return await execute_program(recurse, node, session, stdin, cs,
|
||||
job_table, agent_id)
|
||||
|
||||
# ── command ─────────────────────────────────
|
||||
if kind == NodeKind.COMMAND:
|
||||
return await execute_command(recurse,
|
||||
dispatch,
|
||||
registry,
|
||||
namespace,
|
||||
execute_fn,
|
||||
node,
|
||||
session,
|
||||
stdin,
|
||||
cs,
|
||||
job_table,
|
||||
cancel=cancel)
|
||||
|
||||
# ── pipeline ────────────────────────────────
|
||||
if kind == NodeKind.PIPELINE:
|
||||
commands, stderr_flags = get_pipeline_commands(node)
|
||||
return await handle_pipe(recurse, commands, stderr_flags, session,
|
||||
stdin, cs)
|
||||
|
||||
# ── list (&&, ||) ───────────────────────────
|
||||
if kind == NodeKind.LIST:
|
||||
left, op, right = get_list_parts(node)
|
||||
return await handle_connection(recurse, left, op, right, session,
|
||||
stdin, cs)
|
||||
|
||||
# ── redirected statement ────────────────────
|
||||
if kind == NodeKind.REDIRECT:
|
||||
command, redirects = get_redirects(node)
|
||||
if command.type == NT.LIST:
|
||||
# tree-sitter hoists a trailing redirect over the whole
|
||||
# &&/|| list; bash binds it to the last command:
|
||||
# redirected(list(L, op, R), r) == list(L, op, redirected(R, r))
|
||||
# Re-associate and defer target expansion until R runs, so
|
||||
# `cd /x && echo hi > f` writes under /x. Compound and
|
||||
# subshell bodies keep the whole-body redirect (bash group
|
||||
# semantics).
|
||||
left, op, right = get_list_parts(command)
|
||||
wrapped = partial(_recurse_reassociated, recurse, dispatch,
|
||||
execute_fn, registry, redirects, right)
|
||||
return await handle_connection(wrapped, left, op, right, session,
|
||||
stdin, cs)
|
||||
expanded_redirects, pipe_node = await expand_redirects(
|
||||
redirects, session, execute_fn, registry, cs)
|
||||
stdout, io, exec_node = await handle_redirect(recurse, dispatch,
|
||||
command,
|
||||
expanded_redirects,
|
||||
session, stdin, cs)
|
||||
if pipe_node is not None and stdout is not None:
|
||||
stdout, io2, exec_node2 = await recurse(pipe_node, session, stdout,
|
||||
cs)
|
||||
io = await io.merge(io2)
|
||||
exec_node = exec_node2
|
||||
return stdout, io, exec_node
|
||||
|
||||
# ── subshell ────────────────────────────────
|
||||
if kind == NodeKind.SUBSHELL:
|
||||
body = get_subshell_body(node)
|
||||
return await handle_subshell(recurse, body, session, stdin, cs)
|
||||
|
||||
# ── arithmetic command ((( ... ))) ──────────
|
||||
if (kind == NodeKind.COMPOUND and node.children
|
||||
and node.children[0].type == NT.ARITH_OPEN):
|
||||
text = get_text(node)
|
||||
expr = await expand_arith(node, session, execute_fn, cs)
|
||||
try:
|
||||
value, updates = evaluate_arith(expr, session.env)
|
||||
except ArithError as exc:
|
||||
err = f"bash: ((: {expr}: {exc}\n".encode()
|
||||
return None, IOResult(exit_code=1,
|
||||
stderr=err), ExecutionNode(command=text,
|
||||
exit_code=1,
|
||||
stderr=err)
|
||||
for name in updates:
|
||||
if name in session.readonly_vars:
|
||||
err = f"bash: {name}: readonly variable\n".encode()
|
||||
return None, IOResult(exit_code=1,
|
||||
stderr=err), ExecutionNode(command=text,
|
||||
exit_code=1,
|
||||
stderr=err)
|
||||
session.env.update(updates)
|
||||
code = 0 if value != 0 else 1
|
||||
return None, IOResult(exit_code=code), ExecutionNode(command=text,
|
||||
exit_code=code)
|
||||
|
||||
# ── compound statement ({ ... }) ───────────
|
||||
if kind == NodeKind.COMPOUND:
|
||||
all_stdout: list = []
|
||||
merged_io = IOResult()
|
||||
last_exec = ExecutionNode(command="{}", exit_code=0)
|
||||
for child in node.named_children:
|
||||
if child.type == NT.COMMENT:
|
||||
continue
|
||||
stdout, io, last_exec = await recurse(child, session, stdin, cs)
|
||||
if stdout is not None:
|
||||
all_stdout.append(stdout)
|
||||
merged_io = await merged_io.merge(io)
|
||||
if (io.exit_code != 0 and session.shell_options.get("errexit")
|
||||
and child.type not in ERREXIT_EXEMPT_TYPES):
|
||||
merged_io.exit_code = io.exit_code
|
||||
break
|
||||
if len(all_stdout) == 1:
|
||||
return all_stdout[0], merged_io, last_exec
|
||||
combined = async_chain(*all_stdout) if all_stdout else None
|
||||
return combined, merged_io, last_exec
|
||||
|
||||
# ── if ──────────────────────────────────────
|
||||
if kind == NodeKind.IF:
|
||||
branches, else_body = get_if_branches(node)
|
||||
return await handle_if(recurse, branches, else_body, session, stdin,
|
||||
cs)
|
||||
|
||||
# ── for / select ────────────────────────────
|
||||
if kind in (NodeKind.FOR, NodeKind.SELECT):
|
||||
var, values, body = get_for_parts(node)
|
||||
classified = await expand_and_classify(values, session, execute_fn,
|
||||
registry, session.cwd, cs)
|
||||
# The loop word list is consumed by the shell (WordPolicy.SHELL):
|
||||
# globs resolve to matches before iteration starts.
|
||||
classified = await resolve_globs(classified, registry)
|
||||
if kind == NodeKind.SELECT:
|
||||
return await handle_select(recurse, var, classified, body, session,
|
||||
stdin, cs)
|
||||
return await handle_for(recurse, var, classified, body, session, stdin,
|
||||
cs)
|
||||
|
||||
# ── while / until ───────────────────────────
|
||||
if kind in (NodeKind.WHILE, NodeKind.UNTIL):
|
||||
condition, body = get_while_parts(node)
|
||||
if kind == NodeKind.UNTIL:
|
||||
return await handle_until(recurse, condition, body, session, stdin,
|
||||
cs)
|
||||
return await handle_while(recurse, condition, body, session, stdin, cs)
|
||||
|
||||
# ── case ────────────────────────────────────
|
||||
if kind == NodeKind.CASE:
|
||||
word_node = get_case_word(node)
|
||||
word = await expand_node(word_node, session, execute_fn, cs)
|
||||
items = get_case_items(node)
|
||||
return await handle_case(recurse, word, items, session, stdin, cs)
|
||||
|
||||
# ── function definition ─────────────────────
|
||||
if kind == NodeKind.FUNCTION_DEF:
|
||||
name = get_function_name(node)
|
||||
body = get_function_body(node)
|
||||
session.functions[name] = body
|
||||
return None, IOResult(), ExecutionNode(command=f"function {name}",
|
||||
exit_code=0)
|
||||
|
||||
# ── declaration (export/local/declare/readonly) ──
|
||||
if kind == NodeKind.DECLARATION:
|
||||
keyword = get_declaration_keyword(node)
|
||||
assignments = []
|
||||
flag_chars: set[str] = set()
|
||||
for child in node.named_children:
|
||||
if child.type == NT.VARIABLE_ASSIGNMENT:
|
||||
val_nodes = [
|
||||
c for c in child.named_children
|
||||
if c.type != NT.VARIABLE_NAME
|
||||
]
|
||||
if val_nodes and val_nodes[0].type == NT.ARRAY:
|
||||
key = get_text(child).partition("=")[0]
|
||||
items = [
|
||||
await expand_node(ac, session, execute_fn, cs)
|
||||
for ac in val_nodes[0].named_children
|
||||
]
|
||||
session.arrays[key] = items
|
||||
continue
|
||||
expanded = await expand_node(child, session, execute_fn, cs)
|
||||
assignments.append(expanded)
|
||||
elif child.type in (NT.SIMPLE_EXPANSION, NT.EXPANSION,
|
||||
NT.CONCATENATION, NT.WORD):
|
||||
expanded = await expand_node(child, session, execute_fn, cs)
|
||||
if not expanded:
|
||||
continue
|
||||
if expanded.startswith("-") and len(expanded) > 1:
|
||||
flag_chars.update(expanded[1:])
|
||||
else:
|
||||
assignments.append(expanded)
|
||||
if keyword == NT.LOCAL:
|
||||
return await handle_local(assignments, session)
|
||||
if keyword == "readonly" or "r" in flag_chars:
|
||||
return await handle_readonly(assignments, session)
|
||||
return await handle_export(assignments, session)
|
||||
|
||||
# ── unset ───────────────────────────────────
|
||||
if kind == NodeKind.UNSET:
|
||||
names = get_unset_names(node)
|
||||
return await handle_unset(names, session)
|
||||
|
||||
# ── test ([ ] or [[ ]]) ─────────────────────
|
||||
if kind == NodeKind.TEST:
|
||||
expanded = await expand_test_expr(node, session, execute_fn, cs,
|
||||
registry)
|
||||
return await handle_test(dispatch, expanded, session)
|
||||
|
||||
# ── negated command ─────────────────────────
|
||||
if kind == NodeKind.NEGATED:
|
||||
inner = get_negated_command(node)
|
||||
stdout, io, exec_node = await recurse(inner, session, stdin, cs)
|
||||
io = IOResult(
|
||||
exit_code=0 if io.exit_code != 0 else 1,
|
||||
stderr=io.stderr,
|
||||
reads=io.reads,
|
||||
writes=io.writes,
|
||||
cache=io.cache,
|
||||
)
|
||||
exec_node.exit_code = io.exit_code
|
||||
return stdout, io, exec_node
|
||||
|
||||
# ── variable assignment at top level ────────
|
||||
if kind == NodeKind.VAR_ASSIGN:
|
||||
text = get_text(node)
|
||||
if "=" in text:
|
||||
key, _, val = text.partition("=")
|
||||
if key in session.readonly_vars:
|
||||
err = f"bash: {key}: readonly variable\n".encode()
|
||||
return None, IOResult(exit_code=1,
|
||||
stderr=err), ExecutionNode(command=text,
|
||||
exit_code=1,
|
||||
stderr=err)
|
||||
val_nodes = [
|
||||
c for c in node.named_children if c.type != NT.VARIABLE_NAME
|
||||
]
|
||||
if val_nodes and val_nodes[0].type == NT.ARRAY:
|
||||
items = []
|
||||
for ac in val_nodes[0].named_children:
|
||||
items.append(await expand_node(ac, session, execute_fn,
|
||||
cs))
|
||||
session.arrays[key] = items
|
||||
session.env.pop(key, None)
|
||||
return None, IOResult(), ExecutionNode(command=text,
|
||||
exit_code=0)
|
||||
if val_nodes:
|
||||
val = await expand_node(val_nodes[0], session, execute_fn, cs)
|
||||
session.env[key] = val
|
||||
session.arrays.pop(key, None)
|
||||
return None, IOResult(), ExecutionNode(command=text, exit_code=0)
|
||||
|
||||
raise TypeError(f"unsupported tree-sitter node type: {node.type}")
|
||||
Reference in New Issue
Block a user