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:
279
shell_alias_gen/cli.py
Normal file
279
shell_alias_gen/cli.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""CLI interface for Shell History Alias Generator."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from .parsers import HistoryParserFactory, BashHistoryParser, ZshHistoryParser, FishHistoryParser
|
||||
from .command_analyzer import CommandAnalyzer
|
||||
from .interactive_ui import InteractiveUI
|
||||
from .export_manager import ExportManager
|
||||
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="0.1.0")
|
||||
def main():
|
||||
"""Shell History Alias Generator - Analyze shell history and create aliases."""
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument(
|
||||
'history_file',
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
|
||||
required=False
|
||||
)
|
||||
@click.option(
|
||||
'--shell',
|
||||
type=click.Choice(['bash', 'zsh', 'fish', 'auto']),
|
||||
default='auto',
|
||||
help='Shell type for history parsing'
|
||||
)
|
||||
@click.option(
|
||||
'--min-length',
|
||||
type=int,
|
||||
default=15,
|
||||
help='Minimum command length to consider'
|
||||
)
|
||||
@click.option(
|
||||
'--min-frequency',
|
||||
type=int,
|
||||
default=2,
|
||||
help='Minimum command frequency to consider'
|
||||
)
|
||||
@click.option(
|
||||
'--export',
|
||||
type=click.Choice(['bash', 'zsh', 'fish']),
|
||||
default=None,
|
||||
help='Export format for generated aliases'
|
||||
)
|
||||
@click.option(
|
||||
'--output',
|
||||
type=click.Path(path_type=Path),
|
||||
default=None,
|
||||
help='Output file path for export'
|
||||
)
|
||||
@click.option(
|
||||
'--non-interactive',
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help='Skip interactive review mode'
|
||||
)
|
||||
def analyze(
|
||||
history_file: Optional[Path],
|
||||
shell: str,
|
||||
min_length: int,
|
||||
min_frequency: int,
|
||||
export: Optional[str],
|
||||
output: Optional[Path],
|
||||
non_interactive: bool
|
||||
):
|
||||
"""Analyze shell history and generate alias suggestions."""
|
||||
console.print(Panel.fit(
|
||||
Text("Shell History Alias Generator", style="bold magenta"),
|
||||
subtitle="Analyzing your command history..."
|
||||
))
|
||||
|
||||
parser = _get_parser(shell, history_file)
|
||||
|
||||
if parser is None:
|
||||
console.print("[red]Error: Could not determine shell type.[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
if history_file:
|
||||
commands = parser.parse_file(str(history_file))
|
||||
else:
|
||||
commands = parser.parse_all_history_files() if hasattr(parser, 'parse_all_history_files') else []
|
||||
if not commands:
|
||||
default_paths = parser.get_default_history_locations()
|
||||
if default_paths:
|
||||
if isinstance(default_paths, list) and default_paths:
|
||||
commands = parser.parse_file(default_paths[0]) if isinstance(default_paths[0], str) else []
|
||||
else:
|
||||
commands = []
|
||||
|
||||
if not commands:
|
||||
console.print("[yellow]No commands found in history.[/yellow]")
|
||||
sys.exit(0)
|
||||
|
||||
console.print(f"[dim]Parsed {len(commands)} commands[/dim]")
|
||||
|
||||
analyzer = CommandAnalyzer()
|
||||
suggestions = analyzer.analyze_commands(commands)
|
||||
|
||||
if not suggestions:
|
||||
console.print("[yellow]No alias suggestions generated.[/yellow]")
|
||||
sys.exit(0)
|
||||
|
||||
if non_interactive:
|
||||
selected = suggestions
|
||||
else:
|
||||
ui = InteractiveUI()
|
||||
ui.display_suggestions(suggestions, "Generated Alias Suggestions")
|
||||
|
||||
console.print("\n[bold]Reviewing suggestions...[/bold]")
|
||||
selected = ui.run_review_session(suggestions)
|
||||
|
||||
if not selected:
|
||||
console.print("[yellow]No aliases selected.[/yellow]")
|
||||
sys.exit(0)
|
||||
|
||||
ui.display_summary(selected)
|
||||
|
||||
if not ui.confirm_selection():
|
||||
console.print("[yellow]Cancelled.[/yellow]")
|
||||
sys.exit(0)
|
||||
|
||||
if export:
|
||||
export_manager = ExportManager()
|
||||
if output:
|
||||
content = export_manager.export(selected, export, str(output))
|
||||
console.print(f"[green]Exported to {output}[/green]")
|
||||
else:
|
||||
content = export_manager.export(selected, export)
|
||||
console.print("\n[bold]Generated aliases:[/bold]")
|
||||
console.print(content)
|
||||
else:
|
||||
_print_selected_aliases(selected)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument(
|
||||
'history_file',
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
|
||||
required=False
|
||||
)
|
||||
@click.option(
|
||||
'--shell',
|
||||
type=click.Choice(['bash', 'zsh', 'fish', 'auto']),
|
||||
default='auto',
|
||||
help='Shell type for history parsing'
|
||||
)
|
||||
@click.option(
|
||||
'--format',
|
||||
'output_format',
|
||||
type=click.Choice(['bash', 'zsh', 'fish']),
|
||||
default='bash',
|
||||
help='Export format'
|
||||
)
|
||||
@click.option(
|
||||
'--append/--replace',
|
||||
default=False,
|
||||
help='Append to rcfile or replace aliases section'
|
||||
)
|
||||
def install(
|
||||
history_file: Optional[Path],
|
||||
shell: str,
|
||||
output_format: str,
|
||||
append: bool
|
||||
):
|
||||
"""Install aliases directly to shell rc file."""
|
||||
parser = _get_parser(shell, history_file)
|
||||
|
||||
if parser is None:
|
||||
console.print("[red]Error: Could not determine shell type.[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
if history_file:
|
||||
commands = parser.parse_file(str(history_file))
|
||||
else:
|
||||
commands = parser.parse_all_history_files() if hasattr(parser, 'parse_all_history_files') else []
|
||||
|
||||
if not commands:
|
||||
console.print("[yellow]No commands found in history.[/yellow]")
|
||||
sys.exit(0)
|
||||
|
||||
analyzer = CommandAnalyzer()
|
||||
suggestions = analyzer.analyze_commands(commands)
|
||||
|
||||
if not suggestions:
|
||||
console.print("[yellow]No alias suggestions generated.[/yellow]")
|
||||
sys.exit(0)
|
||||
|
||||
rcfile = _get_rcfile_path(output_format)
|
||||
|
||||
if rcfile is None or not rcfile.exists():
|
||||
console.print(f"[red]Could not find {output_format} rc file.[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
export_manager = ExportManager()
|
||||
if export_manager.export_to_rcfile(suggestions, output_format, str(rcfile)):
|
||||
console.print(f"[green]Aliases added to {rcfile}[/green]")
|
||||
console.print(f"[dim]Restart your shell or run: source {rcfile}[/dim]")
|
||||
else:
|
||||
console.print("[red]Failed to write to rc file.[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@main.command()
|
||||
def shells():
|
||||
"""List supported shell types."""
|
||||
shells_list = HistoryParserFactory.get_supported_shells()
|
||||
console.print("Supported shell types:")
|
||||
for shell in shells_list:
|
||||
console.print(f" - {shell}")
|
||||
|
||||
|
||||
@main.command()
|
||||
def locations():
|
||||
"""Show default history file locations."""
|
||||
console.print("Default history file locations:\n")
|
||||
|
||||
for shell_name in ['bash', 'zsh', 'fish']:
|
||||
parser = HistoryParserFactory.get_parser(shell_name)
|
||||
if parser:
|
||||
locations = parser.get_default_history_locations()
|
||||
console.print(f"[bold]{shell_name.upper()}[/bold]:")
|
||||
for loc in locations:
|
||||
exists = "[green]✓[/green]" if Path(loc).exists() else "[red]✗[/red]"
|
||||
console.print(f" {exists} {loc}")
|
||||
console.print()
|
||||
|
||||
|
||||
def _get_parser(shell: str, history_file: Optional[Path]):
|
||||
"""Get appropriate history parser."""
|
||||
if shell == 'auto' and history_file:
|
||||
detected = HistoryParserFactory.detect_shell_from_file(str(history_file))
|
||||
if detected:
|
||||
shell = detected
|
||||
|
||||
if shell == 'auto':
|
||||
shell = 'bash'
|
||||
|
||||
return HistoryParserFactory.get_parser(shell)
|
||||
|
||||
|
||||
def _get_rcfile_path(shell: str):
|
||||
"""Get the rc file path for a shell."""
|
||||
from os.path import expanduser
|
||||
|
||||
rcfiles = {
|
||||
'bash': expanduser('~/.bashrc'),
|
||||
'zsh': expanduser('~/.zshrc'),
|
||||
'fish': expanduser('~/.config/fish/config.fish'),
|
||||
}
|
||||
|
||||
path = rcfiles.get(shell)
|
||||
return Path(path) if path else None
|
||||
|
||||
|
||||
def _print_selected_aliases(suggestions: List):
|
||||
"""Print selected aliases in a nice format."""
|
||||
console.print("\n[bold green]Generated Aliases:[/bold green]\n")
|
||||
|
||||
for suggestion in suggestions:
|
||||
console.print(f"[bold green]={suggestion.alias_name}[/bold green]='{suggestion.original_command}'")
|
||||
|
||||
console.print("\n[dim]Add these to your shell rc file or run with --export[/dim]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user