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