dfdsf
This commit is contained in:
120
cli_syntax.py
120
cli_syntax.py
@@ -12,6 +12,121 @@ class SyntaxErrorDetail:
|
||||
expected: Optional[str] = None
|
||||
|
||||
|
||||
def _split_pipeline_stages(text: str) -> list[str]:
|
||||
"""Split a pipeline command into stage strings on unquoted '|' characters."""
|
||||
raw = str(text or "")
|
||||
if not raw:
|
||||
return []
|
||||
|
||||
stages: list[str] = []
|
||||
buf: list[str] = []
|
||||
quote: Optional[str] = None
|
||||
escaped = False
|
||||
|
||||
for ch in raw:
|
||||
if escaped:
|
||||
buf.append(ch)
|
||||
escaped = False
|
||||
continue
|
||||
|
||||
if ch == "\\" and quote is not None:
|
||||
buf.append(ch)
|
||||
escaped = True
|
||||
continue
|
||||
|
||||
if ch in ("\"", "'"):
|
||||
if quote is None:
|
||||
quote = ch
|
||||
elif quote == ch:
|
||||
quote = None
|
||||
buf.append(ch)
|
||||
continue
|
||||
|
||||
if ch == "|" and quote is None:
|
||||
stage = "".join(buf).strip()
|
||||
if stage:
|
||||
stages.append(stage)
|
||||
buf = []
|
||||
continue
|
||||
|
||||
buf.append(ch)
|
||||
|
||||
tail = "".join(buf).strip()
|
||||
if tail:
|
||||
stages.append(tail)
|
||||
return stages
|
||||
|
||||
|
||||
def _tokenize_stage(stage_text: str) -> list[str]:
|
||||
"""Tokenize a stage string (best-effort)."""
|
||||
import shlex
|
||||
|
||||
text = str(stage_text or "").strip()
|
||||
if not text:
|
||||
return []
|
||||
try:
|
||||
return shlex.split(text)
|
||||
except Exception:
|
||||
return text.split()
|
||||
|
||||
|
||||
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:
|
||||
return False
|
||||
for tok in tokens:
|
||||
low = str(tok).strip().lower()
|
||||
if low in want:
|
||||
return True
|
||||
# Support -arg=value
|
||||
if "=" in low:
|
||||
head = low.split("=", 1)[0].strip()
|
||||
if head in want:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _validate_add_note_requires_add_file_order(raw: str) -> Optional[SyntaxErrorDetail]:
|
||||
"""Enforce: add-note in piped mode must occur after add-file.
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
for i, (cmd, tokens) in enumerate(parsed):
|
||||
if cmd != "add-note":
|
||||
continue
|
||||
|
||||
# If add-note occurs before any add-file stage, it must be explicitly targeted.
|
||||
if any(pos > i for pos in add_file_positions):
|
||||
has_hash = _has_flag(tokens, "-hash", "--hash")
|
||||
has_store = _has_flag(tokens, "-store", "--store")
|
||||
if has_hash and has_store:
|
||||
continue
|
||||
return SyntaxErrorDetail(
|
||||
"Pipeline error: 'add-note' must come after 'add-file' when used with piped input. "
|
||||
"Move 'add-note' after 'add-file', or call it with explicit targeting: "
|
||||
"add-note -store <store> -hash <sha256> -query \"title:<title>,text:<text>\"."
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def validate_pipeline_text(text: str) -> Optional[SyntaxErrorDetail]:
|
||||
"""Validate raw CLI input before tokenization/execution.
|
||||
|
||||
@@ -97,6 +212,11 @@ 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)
|
||||
semantic_error = _validate_add_note_requires_add_file_order(raw)
|
||||
if semantic_error is not None:
|
||||
return semantic_error
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user