syntax revamp
This commit is contained in:
+101
-12
@@ -70,6 +70,17 @@ def _tokenize_stage(stage_text: str) -> list[str]:
|
||||
return text.split()
|
||||
|
||||
|
||||
def _parse_pipeline_tokens(raw: str) -> list[tuple[str, list[str]]]:
|
||||
parsed: list[tuple[str, list[str]]] = []
|
||||
for stage in _split_pipeline_stages(raw):
|
||||
tokens = _tokenize_stage(stage)
|
||||
if not tokens:
|
||||
continue
|
||||
cmd = str(tokens[0]).replace("_", "-").strip().lower()
|
||||
parsed.append((cmd, tokens))
|
||||
return parsed
|
||||
|
||||
|
||||
def _has_flag(tokens: list[str], *flags: str) -> bool:
|
||||
want = {str(f).strip().lower() for f in flags if str(f).strip()}
|
||||
if not want:
|
||||
@@ -115,18 +126,10 @@ def _validate_add_note_requires_add_file_order(raw: str) -> Optional[SyntaxError
|
||||
Rationale: add-note requires a known (store, hash) target; piping before add-file
|
||||
means the item likely has no hash yet.
|
||||
"""
|
||||
stages = _split_pipeline_stages(raw)
|
||||
if len(stages) <= 1:
|
||||
parsed = _parse_pipeline_tokens(raw)
|
||||
if len(parsed) <= 1:
|
||||
return None
|
||||
|
||||
parsed: list[tuple[str, list[str]]] = []
|
||||
for stage in stages:
|
||||
tokens = _tokenize_stage(stage)
|
||||
if not tokens:
|
||||
continue
|
||||
cmd = str(tokens[0]).replace("_", "-").strip().lower()
|
||||
parsed.append((cmd, tokens))
|
||||
|
||||
add_file_positions = [i for i, (cmd, _toks) in enumerate(parsed) if cmd == "add-file"]
|
||||
if not add_file_positions:
|
||||
return None
|
||||
@@ -165,7 +168,85 @@ def _validate_add_note_requires_add_file_order(raw: str) -> Optional[SyntaxError
|
||||
return None
|
||||
|
||||
|
||||
def validate_pipeline_text(text: str) -> Optional[SyntaxErrorDetail]:
|
||||
def _validate_file_cmdlet_stage_actions(raw: str) -> Optional[SyntaxErrorDetail]:
|
||||
parsed = _parse_pipeline_tokens(raw)
|
||||
if not parsed:
|
||||
return None
|
||||
|
||||
try:
|
||||
from cmdlet.file_cmdlet import File as FileCmdlet
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
for cmd, tokens in parsed:
|
||||
if cmd != "file":
|
||||
continue
|
||||
|
||||
action, _passthrough, seen = FileCmdlet._extract_action(tokens[1:])
|
||||
if action is not None:
|
||||
continue
|
||||
|
||||
if seen:
|
||||
rendered = ", ".join(f"-{name}" for name in seen)
|
||||
return SyntaxErrorDetail(
|
||||
f"Pipeline error: 'file' has conflicting actions ({rendered}); choose exactly one."
|
||||
)
|
||||
|
||||
if _has_flag(tokens[1:], "-plugin", "--plugin", "-instance", "--instance", "-path", "--path"):
|
||||
return SyntaxErrorDetail(
|
||||
"Pipeline error: 'file' requires an explicit action here. "
|
||||
"Use 'file -add -plugin local -instance <name|path>' for local export, or 'file -search ...' for search."
|
||||
)
|
||||
|
||||
return SyntaxErrorDetail(
|
||||
"Pipeline error: 'file' requires -search/-query for search or exactly one action flag "
|
||||
"(-search, -add, -delete, -merge, -download, -convert, -trim, -archive, -screenshot)."
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _validate_add_file_stage_preflight(
|
||||
raw: str,
|
||||
config: Optional[Dict[str, Any]],
|
||||
) -> Optional[SyntaxErrorDetail]:
|
||||
if not isinstance(config, dict):
|
||||
return None
|
||||
|
||||
parsed = _parse_pipeline_tokens(raw)
|
||||
if not parsed:
|
||||
return None
|
||||
|
||||
try:
|
||||
from cmdlet.file.add import Add_File
|
||||
from cmdlet.file_cmdlet import File as FileCmdlet
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
for cmd, tokens in parsed:
|
||||
stage_args: Optional[list[str]] = None
|
||||
|
||||
if cmd == "add-file":
|
||||
stage_args = tokens[1:]
|
||||
elif cmd == "file":
|
||||
action, passthrough, _seen = FileCmdlet._extract_action(tokens[1:])
|
||||
if action == "add":
|
||||
stage_args = passthrough
|
||||
|
||||
if stage_args is None:
|
||||
continue
|
||||
|
||||
message = Add_File.validate_preflight_args(stage_args, config)
|
||||
if message:
|
||||
return SyntaxErrorDetail(message)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def validate_pipeline_text(
|
||||
text: str,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
) -> Optional[SyntaxErrorDetail]:
|
||||
"""Validate raw CLI input before tokenization/execution.
|
||||
|
||||
This is intentionally lightweight and focuses on user-facing syntax issues:
|
||||
@@ -252,11 +333,19 @@ def validate_pipeline_text(text: str) -> Optional[SyntaxErrorDetail]:
|
||||
if not in_single and not in_double and not ch.isspace():
|
||||
seen_nonspace_since_pipe = True
|
||||
|
||||
# Semantic rules (still lightweight; no cmdlet imports)
|
||||
# Pipeline-only semantic rules.
|
||||
semantic_error = _validate_add_note_requires_add_file_order(raw)
|
||||
if semantic_error is not None:
|
||||
return semantic_error
|
||||
|
||||
semantic_error = _validate_file_cmdlet_stage_actions(raw)
|
||||
if semantic_error is not None:
|
||||
return semantic_error
|
||||
|
||||
semantic_error = _validate_add_file_stage_preflight(raw, config)
|
||||
if semantic_error is not None:
|
||||
return semantic_error
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user