diff --git a/shellhist/core/__init__.py b/shellhist/core/__init__.py index a5290e8..7093ada 100644 --- a/shellhist/core/__init__.py +++ b/shellhist/core/__init__.py @@ -1,176 +1 @@ -"""Core module for shell history loading and parsing.""" - -import os -import re -from dataclasses import dataclass, field -from datetime import datetime -from pathlib import Path -from typing import Optional - - -@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): - return hash(self.command) - - def __eq__(self, other): - 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, 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, 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() +bmFtZTogQ0kKCm9uOgogIHB1c2g6CiAgICBicmFuY2hlczogW21haW5dCiAgcHVsbF9yZXF1ZXN0OgogICAgYnJhbmNoZXM6IFttYWluXQoKam9iczogCiAgdGVzdDoKICAgIHJ1bnMtb246IHVidW50dS1sYXRlc3QKICAgIHN0ZXBzOgogICAgICAtIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXQdjNFgKICAgICAgdXNlczogYWN0aW9ucy9zZXR1cC1weXRob25fdjUKICAgICAgd2l0aDoKICAgICAgICBweXRob24tdmVyc2lvbjogJzMuMTEnCiAgICAtIHJ1bjogcGlwIGluc3RhbGwgLWUgIltcImRldlwiXSIKICAgIC0gcnVuOiBweXRlc3QgdGVzdHMvIC12CiAgICAtIHJ1bjogcnVmZiBjaGVjayAu \ No newline at end of file