Add script explainer module
This commit is contained in:
300
shellgenius/explainer.py
Normal file
300
shellgenius/explainer.py
Normal file
@@ -0,0 +1,300 @@
|
||||
"""Script explainer module for ShellGenius."""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from shellgenius.config import get_config
|
||||
from shellgenius.generation import ShellParser, get_ollama_client
|
||||
|
||||
|
||||
@dataclass
|
||||
class LineExplanation:
|
||||
"""Explanation for a single line."""
|
||||
|
||||
line_number: int
|
||||
content: str
|
||||
explanation: str
|
||||
is_command: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScriptExplanation:
|
||||
"""Complete script explanation."""
|
||||
|
||||
shell_type: str
|
||||
line_explanations: List[LineExplanation]
|
||||
summary: str
|
||||
overall_purpose: str
|
||||
|
||||
|
||||
class ShellExplainer:
|
||||
"""Shell script explainer."""
|
||||
|
||||
KEYWORDS = {
|
||||
"if": "Conditional statement",
|
||||
"then": "Then clause of conditional",
|
||||
"else": "Else clause of conditional",
|
||||
"fi": "End of conditional",
|
||||
"for": "Loop iteration",
|
||||
"while": "While loop",
|
||||
"until": "Until loop",
|
||||
"do": "Loop body start",
|
||||
"done": "Loop body end",
|
||||
"case": "Case statement",
|
||||
"esac": "End of case statement",
|
||||
"function": "Function definition",
|
||||
"return": "Return from function",
|
||||
"export": "Export variable to environment",
|
||||
"unset": "Remove variable",
|
||||
"local": "Local variable declaration",
|
||||
"readonly": "Mark variable as read-only",
|
||||
"source": "Execute script in current shell",
|
||||
"alias": "Create command alias",
|
||||
"unalias": "Remove command alias",
|
||||
}
|
||||
|
||||
COMMON_PATTERNS = [
|
||||
(r"^#!", "Shebang - specifies interpreter"),
|
||||
(r"^\s*#", "Comment"),
|
||||
(r"\$[a-zA-Z_][a-zA-Z0-9_]*", "Variable reference"),
|
||||
(r"\$\{[^}]+\}", "Brace variable reference"),
|
||||
(r"\$\(([^)]+)\)", "Command substitution"),
|
||||
(r"`([^`]+)`", "Backtick command substitution"),
|
||||
(r"-eq|-ne|-lt|-le|-gt|-ge", "Numeric comparison operators"),
|
||||
(r"=~|!~", "Pattern matching operators"),
|
||||
(r"-z|-n", "String length checks"),
|
||||
(r"-f|-d|-L|-e", "File tests"),
|
||||
(r"2>&1|&>", "Redirect stderr to stdout"),
|
||||
(r"\|", "Pipe output to next command"),
|
||||
(r">>|>>", "Append redirection"),
|
||||
(r">\s*\S+", "Output redirection"),
|
||||
(r"<\s*\S+", "Input redirection"),
|
||||
(r"set\s+-", "Set shell options"),
|
||||
(r"trap\s+", "Signal handler setup"),
|
||||
(r"exec\s+", "Replace current process"),
|
||||
(r"cd\s+|pushd|popd", "Directory navigation"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize explainer."""
|
||||
self.parser = ShellParser()
|
||||
self.client = get_ollama_client()
|
||||
|
||||
def explain(
|
||||
self, script: str, detailed: bool = True
|
||||
) -> ScriptExplanation:
|
||||
"""Explain a shell script.
|
||||
|
||||
Args:
|
||||
script: Shell script content
|
||||
detailed: Use detailed LLM-based explanation
|
||||
|
||||
Returns:
|
||||
ScriptExplanation with line-by-line explanations
|
||||
"""
|
||||
shell_type = self.parser.detect_shell(script)
|
||||
|
||||
if detailed:
|
||||
return self._explain_with_ai(script, shell_type)
|
||||
else:
|
||||
return self._explain_basic(script, shell_type)
|
||||
|
||||
def _explain_basic(self, script: str, shell_type: str) -> ScriptExplanation:
|
||||
"""Provide basic script explanation.
|
||||
|
||||
Args:
|
||||
script: Shell script content
|
||||
shell_type: Detected shell type
|
||||
|
||||
Returns:
|
||||
ScriptExplanation
|
||||
"""
|
||||
lines = script.split("\n")
|
||||
line_explanations = []
|
||||
|
||||
for i, line in enumerate(lines, 1):
|
||||
stripped = line.strip()
|
||||
is_command = bool(stripped) and not stripped.startswith("#")
|
||||
|
||||
if is_command:
|
||||
explanation = self._explain_line_basic(line)
|
||||
else:
|
||||
explanation = "Comment" if stripped.startswith("#") else "Empty line"
|
||||
|
||||
line_explanations.append(
|
||||
LineExplanation(
|
||||
line_number=i,
|
||||
content=stripped,
|
||||
explanation=explanation,
|
||||
is_command=is_command,
|
||||
)
|
||||
)
|
||||
|
||||
summary = self._generate_summary(line_explanations, shell_type)
|
||||
|
||||
return ScriptExplanation(
|
||||
shell_type=shell_type,
|
||||
line_explanations=line_explanations,
|
||||
summary=summary,
|
||||
overall_purpose=self._detect_purpose(line_explanations),
|
||||
)
|
||||
|
||||
def _explain_line_basic(self, line: str) -> str:
|
||||
"""Provide basic explanation for a single line.
|
||||
|
||||
Args:
|
||||
line: Shell command line
|
||||
|
||||
Returns:
|
||||
Explanation string
|
||||
"""
|
||||
for keyword, description in self.KEYWORDS.items():
|
||||
if re.search(r"\b" + keyword + r"\b", line):
|
||||
return description
|
||||
|
||||
for pattern, description in self.COMMON_PATTERNS:
|
||||
if re.search(pattern, line):
|
||||
return description
|
||||
|
||||
return "Shell command"
|
||||
|
||||
def _explain_with_ai(
|
||||
self, script: str, shell_type: str
|
||||
) -> ScriptExplanation:
|
||||
"""Use AI to explain script in detail.
|
||||
|
||||
Args:
|
||||
script: Shell script content
|
||||
shell_type: Shell type
|
||||
|
||||
Returns:
|
||||
ScriptExplanation with AI explanations
|
||||
"""
|
||||
from shellgenius.generation import PromptTemplates
|
||||
|
||||
prompt = PromptTemplates.get_explain_prompt(script, shell_type)
|
||||
result = self.client.generate(prompt)
|
||||
|
||||
if result["success"]:
|
||||
raw_explanation = result["response"].get("response", "")
|
||||
return self._parse_ai_explanation(script, raw_explanation, shell_type)
|
||||
else:
|
||||
return self._explain_basic(script, shell_type)
|
||||
|
||||
def _parse_ai_explanation(
|
||||
self, script: str, raw: str, shell_type: str
|
||||
) -> ScriptExplanation:
|
||||
"""Parse AI explanation response.
|
||||
|
||||
Args:
|
||||
script: Original script
|
||||
raw: Raw AI response
|
||||
shell_type: Shell type
|
||||
|
||||
Returns:
|
||||
ScriptExplanation
|
||||
"""
|
||||
lines = script.split("\n")
|
||||
line_explanations = []
|
||||
explanations_by_line = {}
|
||||
|
||||
for line in raw.split("\n"):
|
||||
match = re.match(r"Line\s+(\d+):\s*(.+?)\s*-\s*(.+)", line)
|
||||
if match:
|
||||
line_num = int(match.group(1))
|
||||
command = match.group(2)
|
||||
explanation = match.group(3)
|
||||
explanations_by_line[line_num] = (command, explanation)
|
||||
|
||||
for i, line in enumerate(lines, 1):
|
||||
stripped = line.strip()
|
||||
is_command = bool(stripped) and not stripped.startswith("#")
|
||||
|
||||
if i in explanations_by_line:
|
||||
cmd, exp = explanations_by_line[i]
|
||||
explanation = exp
|
||||
else:
|
||||
explanation = self._explain_line_basic(stripped) if is_command else "Empty/Comment"
|
||||
|
||||
line_explanations.append(
|
||||
LineExplanation(
|
||||
line_number=i,
|
||||
content=stripped,
|
||||
explanation=explanation,
|
||||
is_command=is_command,
|
||||
)
|
||||
)
|
||||
|
||||
summary = self._generate_summary(line_explanations, shell_type)
|
||||
|
||||
return ScriptExplanation(
|
||||
shell_type=shell_type,
|
||||
line_explanations=line_explanations,
|
||||
summary=summary,
|
||||
overall_purpose=self._detect_purpose(line_explanations),
|
||||
)
|
||||
|
||||
def _generate_summary(
|
||||
self, explanations: List[LineExplanation], shell_type: str
|
||||
) -> str:
|
||||
"""Generate overall summary.
|
||||
|
||||
Args:
|
||||
explanations: List of line explanations
|
||||
shell_type: Shell type
|
||||
|
||||
Returns:
|
||||
Summary string
|
||||
"""
|
||||
command_count = sum(1 for e in explanations if e.is_command)
|
||||
function_count = sum(
|
||||
1 for e in explanations if "function" in e.explanation.lower()
|
||||
)
|
||||
loop_count = sum(
|
||||
1 for e in explanations if "loop" in e.explanation.lower()
|
||||
)
|
||||
|
||||
return f"{shell_type} script with {command_count} commands, {function_count} function(s), {loop_count} loop(s)."
|
||||
|
||||
def _detect_purpose(self, explanations: List[LineExplanation]) -> str:
|
||||
"""Detect overall script purpose.
|
||||
|
||||
Args:
|
||||
explanations: List of line explanations
|
||||
|
||||
Returns:
|
||||
Detected purpose string
|
||||
"""
|
||||
keywords = []
|
||||
for exp in explanations:
|
||||
if exp.is_command:
|
||||
cmd = exp.content.lower()
|
||||
if "git" in cmd:
|
||||
keywords.append("Git operations")
|
||||
if "docker" in cmd or "kubectl" in cmd:
|
||||
keywords.append("Container operations")
|
||||
if "npm" in cmd or "pip" in cmd:
|
||||
keywords.append("Package management")
|
||||
if "curl" in cmd or "wget" in cmd:
|
||||
keywords.append("Network operations")
|
||||
if any(x in cmd for x in ["cd", "ls", "mkdir", "rm"]):
|
||||
keywords.append("File system operations")
|
||||
|
||||
if keywords:
|
||||
return " | ".join(set(keywords))
|
||||
return "General shell script"
|
||||
|
||||
|
||||
def explain_script(script: str, detailed: bool = True) -> ScriptExplanation:
|
||||
"""Convenience function to explain a shell script.
|
||||
|
||||
Args:
|
||||
script: Shell script content
|
||||
detailed: Use detailed AI-based explanation
|
||||
|
||||
Returns:
|
||||
ScriptExplanation
|
||||
"""
|
||||
explainer = ShellExplainer()
|
||||
return explainer.explain(script, detailed)
|
||||
Reference in New Issue
Block a user