Initial upload: Shell History Alias Generator with full test suite
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit is contained in:
161
shell_alias_gen/command_analyzer.py
Normal file
161
shell_alias_gen/command_analyzer.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Command analysis and alias generation module."""
|
||||
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from .parsers import ParsedCommand
|
||||
|
||||
|
||||
@dataclass
|
||||
class AliasSuggestion:
|
||||
"""Represents a suggested alias."""
|
||||
alias_name: str
|
||||
original_command: str
|
||||
frequency: int
|
||||
score: float
|
||||
|
||||
def to_shell(self, shell: str = 'bash') -> str:
|
||||
"""Generate alias string for specified shell."""
|
||||
if shell == 'fish':
|
||||
return f"alias {self.alias_name}='{self.original_command}'"
|
||||
return f"alias {self.alias_name}='{self.original_command}'"
|
||||
|
||||
|
||||
class CommandAnalyzer:
|
||||
"""Analyzes shell commands and generates alias suggestions."""
|
||||
|
||||
MIN_COMMAND_LENGTH = 15
|
||||
MIN_FREQUENCY = 2
|
||||
SCORE_WEIGHTS = {
|
||||
'frequency': 1.0,
|
||||
'length': 0.5,
|
||||
'complexity': 0.3,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.used_aliases: Dict[str, int] = {}
|
||||
|
||||
def analyze_commands(self, commands: List[ParsedCommand]) -> List[AliasSuggestion]:
|
||||
"""Analyze commands and return alias suggestions."""
|
||||
command_counter = Counter()
|
||||
|
||||
for cmd in commands:
|
||||
normalized = cmd.normalized
|
||||
if len(normalized) >= self.MIN_COMMAND_LENGTH:
|
||||
command_counter[normalized] += 1
|
||||
|
||||
suggestions = []
|
||||
for command, frequency in command_counter.most_common():
|
||||
if frequency >= self.MIN_FREQUENCY:
|
||||
score = self._calculate_score(command, frequency)
|
||||
alias_name = self._generate_alias_name(command, frequency)
|
||||
|
||||
if self._is_valid_alias(alias_name, command):
|
||||
suggestion = AliasSuggestion(
|
||||
alias_name=alias_name,
|
||||
original_command=command,
|
||||
frequency=frequency,
|
||||
score=score,
|
||||
)
|
||||
suggestions.append(suggestion)
|
||||
|
||||
suggestions.sort(key=lambda x: x.score, reverse=True)
|
||||
return suggestions[:50]
|
||||
|
||||
def _calculate_score(self, command: str, frequency: int) -> float:
|
||||
"""Calculate a score for an alias suggestion."""
|
||||
length_score = min(len(command) / 100, 1.0)
|
||||
freq_score = min(frequency / 10, 1.0)
|
||||
complexity = self._calculate_complexity(command)
|
||||
complexity_score = min(complexity / 20, 1.0)
|
||||
|
||||
return (
|
||||
freq_score * self.SCORE_WEIGHTS['frequency'] +
|
||||
length_score * self.SCORE_WEIGHTS['length'] +
|
||||
complexity_score * self.SCORE_WEIGHTS['complexity']
|
||||
)
|
||||
|
||||
def _calculate_complexity(self, command: str) -> int:
|
||||
"""Calculate command complexity based on various factors."""
|
||||
complexity = 0
|
||||
words = command.split()
|
||||
|
||||
complexity += len(words) * 2
|
||||
complexity += sum(1 for w in words if len(w) > 5)
|
||||
complexity += sum(1 for c in command if c in '-/')
|
||||
complexity += command.count('--') * 2
|
||||
|
||||
flags = [w for w in words if w.startswith('-')]
|
||||
complexity += len(flags) * 2
|
||||
|
||||
return complexity
|
||||
|
||||
def _generate_alias_name(self, command: str, frequency: int) -> str:
|
||||
"""Generate a short alias name from a command."""
|
||||
words = command.split()
|
||||
|
||||
if not words:
|
||||
return 'alias_cmd'
|
||||
|
||||
candidates = []
|
||||
|
||||
first_letters = ''.join(w[0] for w in words if w and len(w) > 0)
|
||||
if len(first_letters) >= 2:
|
||||
candidates.append(first_letters[:8].lower())
|
||||
|
||||
main_words = [w for w in words if not w.startswith('-') and not w.startswith('$')]
|
||||
if main_words:
|
||||
meaningful = [w.rstrip(',.;:') for w in main_words if len(w) > 2]
|
||||
if meaningful:
|
||||
base = meaningful[0]
|
||||
candidates.append(base[:10].lower())
|
||||
if len(meaningful) > 1:
|
||||
candidates.append((meaningful[0][:4] + meaningful[1][:4]).lower())
|
||||
|
||||
keywords = ['git', 'docker', 'npm', 'pip', 'python', 'ssh', 'curl', 'grep']
|
||||
for kw in keywords:
|
||||
if kw in command.lower():
|
||||
candidates.insert(0, kw)
|
||||
break
|
||||
|
||||
base_name = candidates[0] if candidates else 'cmd'
|
||||
|
||||
suffix = 0
|
||||
final_name = base_name
|
||||
while final_name in self.used_aliases or not self._is_valid_alias_name(final_name):
|
||||
suffix += 1
|
||||
final_name = f"{base_name}{suffix}"
|
||||
if suffix > 100:
|
||||
final_name = f"alias_{hash(command) % 10000}"
|
||||
break
|
||||
|
||||
self.used_aliases[final_name] = frequency
|
||||
return final_name
|
||||
|
||||
def _is_valid_alias_name(self, name: str) -> bool:
|
||||
"""Check if name is a valid shell alias identifier."""
|
||||
if not name:
|
||||
return False
|
||||
if not name[0].isalpha() and name[0] != '_':
|
||||
return False
|
||||
return all(c.isalnum() or c == '_' for c in name)
|
||||
|
||||
def _is_valid_alias(self, alias_name: str, command: str) -> bool:
|
||||
"""Validate alias is suitable for creation."""
|
||||
if not self._is_valid_alias_name(alias_name):
|
||||
return False
|
||||
if len(alias_name) > 64:
|
||||
return False
|
||||
if len(command) > 1000:
|
||||
return False
|
||||
return True
|
||||
|
||||
def filter_suggestions(
|
||||
self,
|
||||
suggestions: List[AliasSuggestion],
|
||||
min_score: float = 0.1,
|
||||
max_count: int = 20
|
||||
) -> List[AliasSuggestion]:
|
||||
"""Filter suggestions by score and count."""
|
||||
filtered = [s for s in suggestions if s.score >= min_score]
|
||||
return filtered[:max_count]
|
||||
Reference in New Issue
Block a user