Files
wehub-resource-sync bcbd1bdb22
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
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

341 lines
13 KiB
Python

# ========= 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. =========
from mirage.commands.spec.constants import NUMERIC_SHORT, flag_kwarg_name
from mirage.commands.spec.types import CommandSpec, OperandKind, ParsedArgs
from mirage.utils.path import resolve_path
def _set_value_flag(
flags: dict[str, str | bool | list[str]],
name: str,
value: str,
repeat_flags: set[str],
) -> None:
if name in repeat_flags:
prev = flags.get(name)
if isinstance(prev, list):
prev.append(value)
else:
flags[name] = [value]
else:
flags[name] = value
def _match_mixed_cluster(
tok: str,
bool_flags: set[str],
value_flags: set[str],
) -> tuple[list[str], str, str | None] | None:
"""Match a getopt-style cluster of bool flags ending in a value flag.
Args:
tok (str): token like "-ne" or "-nepat".
bool_flags (set[str]): single-dash boolean flag names.
value_flags (set[str]): single-dash value flag names.
Returns:
tuple[list[str], str, str | None] | None: (bool flag names, value
flag name, attached value or None when the value comes from the
next token), or None when any character is unknown or no value
flag terminates the cluster.
"""
bools: list[str] = []
chars = tok[1:]
for idx, ch in enumerate(chars):
name = f"-{ch}"
if name in bool_flags:
bools.append(name)
continue
if name in value_flags:
rest = chars[idx + 1:]
return bools, name, (rest if rest else None)
return None
return None
def parse_command(
spec: CommandSpec,
argv: list[str],
cwd: str,
) -> ParsedArgs:
bool_flags: set[str] = set()
value_flags: set[str] = set()
long_bool_flags: set[str] = set()
long_value_flags: set[str] = set()
long_optional_flags: set[str] = set()
value_flag_kinds: dict[str, OperandKind] = {}
repeat_flags: set[str] = set()
numeric_shorthand_flag: str | None = None
for opt in spec.options:
if opt.short:
if opt.value_kind == OperandKind.NONE:
bool_flags.add(opt.short)
else:
value_flags.add(opt.short)
value_flag_kinds[opt.short] = opt.value_kind
if opt.repeatable:
repeat_flags.add(opt.short)
if opt.numeric_shorthand:
numeric_shorthand_flag = opt.short
if opt.long:
if opt.value_kind == OperandKind.NONE:
long_bool_flags.add(opt.long)
elif opt.value_optional:
# GNU optional argument: bare form is boolean, value only
# attaches via `=`; a detached next token is an operand.
long_bool_flags.add(opt.long)
long_optional_flags.add(opt.long)
else:
long_value_flags.add(opt.long)
value_flag_kinds[opt.long] = opt.value_kind
if opt.repeatable:
repeat_flags.add(opt.long)
rest_kind: OperandKind | None = (spec.rest.kind
if spec.rest is not None else None)
cache_paths: list[str] = []
filtered_argv: list[str] = []
# orig_indices[j] = argv position of filtered_argv[j]
orig_indices: list[int] = []
i = 0
while i < len(argv):
if argv[i] == "--cache":
i += 1
while i < len(argv) and not argv[i].startswith("-"):
cache_paths.append(resolve_path(argv[i], cwd))
i += 1
else:
filtered_argv.append(argv[i])
orig_indices.append(i)
i += 1
flags: dict[str, str | bool | list[str]] = {}
raw_args: list[str] = []
# raw_indices[k] = argv position of raw_args[k]
raw_indices: list[int] = []
# Per-position operand kinds aligned with the caller's argv (None =
# flag token or ignored word). Positions, not value sets, so the
# same word can be TEXT in one slot and PATH in another:
# grep *.txt *.txt -> [TEXT, PATH]
# find /data -name *.txt -> [PATH, None, TEXT]
# grep --cache /c pat f.txt -> [None, None, TEXT, PATH]
# orig_indices/raw_indices map the parser's shrunken views back to
# argv slots (filtered_argv drops --cache tokens, raw_args keeps
# only operands); kinds must be written at the original positions
# or one dropped token shifts every later kind onto the wrong word.
word_kinds: list[OperandKind | None] = [None] * len(argv)
warnings: list[str] = []
invalid_options: list[str] = []
needs_value_options: list[str] = []
# Free-text commands (echo/python/bash-style TEXT rest) keep unknown
# dash tokens verbatim; elsewhere they are dropped with a warning so a
# stray flag never corrupts pattern/path classification.
lenient_dash_operands = rest_kind == OperandKind.TEXT
i = 0
end_of_flags = False
while i < len(filtered_argv):
tok = filtered_argv[i]
if tok == "--" and not end_of_flags:
end_of_flags = True
i += 1
continue
if end_of_flags:
raw_args.append(tok)
raw_indices.append(orig_indices[i])
i += 1
continue
if tok.startswith("--"):
if tok in long_bool_flags:
flags[tok] = True
i += 1
elif tok in long_value_flags and i + 1 < len(filtered_argv):
_set_value_flag(flags, tok, filtered_argv[i + 1], repeat_flags)
word_kinds[orig_indices[i + 1]] = value_flag_kinds[tok]
i += 2
else:
eq = tok.find("=")
if eq != -1 and (tok[:eq] in long_value_flags
or tok[:eq] in long_optional_flags):
_set_value_flag(flags, tok[:eq], tok[eq + 1:],
repeat_flags)
elif tok in long_value_flags:
# Declared value flag at end of line with no argument.
needs_value_options.append(tok)
elif lenient_dash_operands:
raw_args.append(tok)
raw_indices.append(orig_indices[i])
else:
invalid_options.append(tok)
i += 1
continue
if tok.startswith("-") and len(tok) > 1:
if numeric_shorthand_flag is not None and NUMERIC_SHORT.match(tok):
flags[numeric_shorthand_flag] = tok[1:]
i += 1
continue
matched_value = False
for vf in value_flags:
if tok == vf and i + 1 < len(filtered_argv):
_set_value_flag(flags, vf, filtered_argv[i + 1],
repeat_flags)
word_kinds[orig_indices[i + 1]] = value_flag_kinds[vf]
i += 2
matched_value = True
break
if tok.startswith(vf) and len(tok) > len(vf):
_set_value_flag(flags, vf, tok[len(vf):], repeat_flags)
i += 1
matched_value = True
break
if matched_value:
continue
if tok in bool_flags:
flags[tok] = True
i += 1
continue
all_bool = True
for ch in tok[1:]:
if f"-{ch}" not in bool_flags:
all_bool = False
break
if all_bool and len(tok) > 1:
for ch in tok[1:]:
flags[f"-{ch}"] = True
i += 1
continue
mixed = _match_mixed_cluster(tok, bool_flags, value_flags)
if mixed is not None:
cluster_bools, vflag, attached = mixed
if attached is not None:
for name in cluster_bools:
flags[name] = True
_set_value_flag(flags, vflag, attached, repeat_flags)
i += 1
continue
if i + 1 < len(filtered_argv):
for name in cluster_bools:
flags[name] = True
_set_value_flag(flags, vflag, filtered_argv[i + 1],
repeat_flags)
word_kinds[orig_indices[i + 1]] = value_flag_kinds[vflag]
i += 2
continue
if lenient_dash_operands or NUMERIC_SHORT.match(tok):
raw_args.append(tok)
raw_indices.append(orig_indices[i])
elif tok in value_flags or (mixed is not None
and mixed[2] is None):
# A declared value flag (alone or ending a cluster) with no
# argument left on the line. GNU reports the flag character.
needy = tok[1:] if tok in value_flags else mixed[1][1:]
needs_value_options.append(needy)
else:
# GNU reports the first offending character, not the token.
bad = tok[1:2]
for ch in tok[1:]:
if (f"-{ch}" not in bool_flags
and f"-{ch}" not in value_flags):
bad = ch
break
invalid_options.append(bad)
i += 1
continue
raw_args.append(tok)
raw_indices.append(orig_indices[i])
i += 1
positional: tuple[OperandKind,
...] = tuple(op.kind for op in spec.positional
if not any(name in flags
for name in op.provided_by))
# Overflow operands past the declared positional slots pass through
# classified like the last slot (TEXT when there is none), so a
# fixed-arity command receives them and raises its own extra-operand
# UsageError (#452). The parser classifies, it never drops or raises.
overflow_kind = positional[-1] if positional else OperandKind.TEXT
classified: list[tuple[str, OperandKind]] = []
raw_operands: list[tuple[str, OperandKind]] = []
for j, arg in enumerate(raw_args):
if j < len(positional):
kind = positional[j]
elif rest_kind is not None:
kind = rest_kind
else:
kind = overflow_kind
if kind == OperandKind.PATH:
classified.append((resolve_path(arg, cwd), OperandKind.PATH))
raw_operands.append((arg, OperandKind.PATH))
else:
classified.append((arg, OperandKind.TEXT))
raw_operands.append((arg, OperandKind.TEXT))
word_kinds[raw_indices[j]] = kind
path_flag_values: list[str] = []
for flag_name, kind in value_flag_kinds.items():
if kind != OperandKind.PATH or flag_name not in flags:
continue
value = flags[flag_name]
if isinstance(value, list):
resolved_list = [resolve_path(part, cwd) for part in value]
flags[flag_name] = resolved_list
path_flag_values.extend(resolved_list)
elif isinstance(value, str):
resolved = resolve_path(value, cwd)
flags[flag_name] = resolved
path_flag_values.append(resolved)
text_flag_values: list[str] = []
for flag_name, kind in value_flag_kinds.items():
if kind != OperandKind.TEXT or flag_name not in flags:
continue
value = flags[flag_name]
if isinstance(value, list):
text_flag_values.extend(value)
elif isinstance(value, str):
text_flag_values.append(value)
return ParsedArgs(
flags=flags,
args=classified,
cache_paths=cache_paths,
path_flag_values=path_flag_values,
raw_operands=raw_operands,
text_flag_values=text_flag_values,
warnings=warnings,
word_kinds=word_kinds,
invalid_options=invalid_options,
needs_value_options=needs_value_options,
)
def parse_to_kwargs(parsed: ParsedArgs) -> dict[str, str | bool | list[str]]:
result: dict[str, str | bool | list[str]] = {}
for key, value in parsed.flags.items():
result[flag_kwarg_name(key)] = value
return result