"""Core module for shell history loading and parsing.""" import os import re from dataclasses import dataclass, field from datetime import datetime from typing import Optional, TextIO @dataclass class HistoryEntry: """Represents a single entry in shell history.""" command: str timestamp: Optional[datetime] = None line_number: int = 0 shell_type: str = "unknown" def __hash__(self) -> int: return hash(self.command) def __eq__(self, other: object) -> bool: if isinstance(other, HistoryEntry): return self.command == other.command return False @dataclass class HistoryStore: """Stores and manages shell history entries.""" entries: list[HistoryEntry] = field(default_factory=list) command_frequency: dict[str, int] = field(default_factory=dict) def add_entry(self, entry: HistoryEntry) -> None: """Add a history entry to the store.""" self.entries.append(entry) self.command_frequency[entry.command] = self.command_frequency.get(entry.command, 0) + 1 def get_frequency(self, command: str) -> int: """Get the frequency of a command.""" return self.command_frequency.get(command, 0) def get_most_frequent(self, limit: int = 10) -> list[tuple[str, int]]: """Get the most frequently used commands.""" sorted_freq = sorted(self.command_frequency.items(), key=lambda x: x[1], reverse=True) return sorted_freq[:limit] def get_unique_commands(self) -> list[str]: """Get list of unique commands.""" return list(self.command_frequency.keys()) class HistoryLoader: """Loads and parses shell history from various shell formats.""" BASH_TIMESTAMP_PATTERN = re.compile(r'^#(\d{10})$') ZSH_TIMESTAMP_PATTERN = re.compile(r'^: (\d{10}):(\d+);(.*)$') def __init__(self, history_path: Optional[str] = None): """Initialize the history loader. Args: history_path: Optional path to history file. If not provided, will attempt to detect from environment. """ self.history_path = history_path self.shell_type = self._detect_shell() def _detect_shell(self) -> str: """Detect the shell type from environment.""" shell = os.environ.get("SHELL", "") if "zsh" in shell: return "zsh" elif "bash" in shell: return "bash" return "bash" def _get_default_history_path(self) -> Optional[str]: """Get the default history file path based on shell type.""" home = os.path.expanduser("~") if self.shell_type == "zsh": return os.environ.get("HISTFILE", f"{home}/.zsh_history") return os.environ.get("HISTFILE", f"{home}/.bash_history") def load(self) -> HistoryStore: """Load history from the configured path. Returns: HistoryStore containing all parsed entries. """ path = self.history_path or self._get_default_history_path() if not path or not os.path.exists(path): raise FileNotFoundError( f"History file not found: {path}. " "Set HISTFILE environment variable or use --history flag." ) store = HistoryStore() with open(path, "r", encoding="utf-8", errors="replace") as f: if self.shell_type == "zsh": self._parse_zsh_history(f, store) else: self._parse_bash_history(f, store) return store def _parse_bash_history(self, file: TextIO, store: HistoryStore) -> None: """Parse bash history format.""" line_number = 0 pending_timestamp: Optional[datetime] = None for line in file: line = line.rstrip("\n") line_number += 1 timestamp_match = self.BASH_TIMESTAMP_PATTERN.match(line) if timestamp_match: ts = int(timestamp_match.group(1)) pending_timestamp = datetime.fromtimestamp(ts) continue if line and not line.startswith("#"): command = line entry = HistoryEntry( command=command, timestamp=pending_timestamp, line_number=line_number, shell_type="bash" ) store.add_entry(entry) pending_timestamp = None def _parse_zsh_history(self, file: TextIO, store: HistoryStore) -> None: """Parse zsh history format.""" line_number = 0 for line in file: line = line.rstrip("\n") line_number += 1 timestamp_match = self.ZSH_TIMESTAMP_PATTERN.match(line) if timestamp_match: ts = int(timestamp_match.group(1)) command = timestamp_match.group(3) timestamp = datetime.fromtimestamp(ts) entry = HistoryEntry( command=command, timestamp=timestamp, line_number=line_number, shell_type="zsh" ) store.add_entry(entry) elif line: entry = HistoryEntry( command=line, timestamp=None, line_number=line_number, shell_type="zsh" ) store.add_entry(entry) @staticmethod def from_file(path: str) -> HistoryStore: """Convenience method to load history from a specific file. Args: path: Path to history file. Returns: HistoryStore containing parsed entries. """ loader = HistoryLoader(history_path=path) return loader.load()