From 468d24e658ee9b0af9cdd500e3db4db4ff2a780e Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Wed, 4 Feb 2026 11:00:13 +0000 Subject: [PATCH] Add script explainer module --- shellgenius/explainer.py | 300 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 shellgenius/explainer.py diff --git a/shellgenius/explainer.py b/shellgenius/explainer.py new file mode 100644 index 0000000..8bb4d07 --- /dev/null +++ b/shellgenius/explainer.py @@ -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)