""" cmd2-like CLI framework using prompt_toolkit with: - Color support - Command history search - Script execution - Nested commands - Session logging """ from prompt_toolkit import PromptSession, HTML from prompt_toolkit.history import FileHistory from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.styles import Style from pygments.lexers.shell import BashLexer import inspect import os import datetime import subprocess import sys # Custom color style custom_style = Style.from_dict({ 'prompt': 'ansicyan bold', 'command': 'ansigreen', 'error': 'ansired bold', 'warning': 'ansiyellow', 'info': 'ansiblue', 'success': 'ansigreen bold', }) class CommandCompleter(Completer): """Provides autocompletion for commands and their arguments""" def __init__(self, cli): self.cli = cli def get_completions(self, document, complete_event): text = document.text_before_cursor.split() # Command name completion if len(text) == 0 or (len(text) == 1 and not document.text.endswith(' ')): prefix = text[0] if text else '' for cmd in self.cli.get_commands(): if cmd.startswith(prefix): yield Completion( cmd, start_position=-len(prefix), display=cmd, display_meta=self.cli.get_command_help(cmd) ) # Command argument completion elif len(text) >= 1: cmd = text[0] arg_completer = getattr(self.cli, f'argcompleter_{cmd}', None) if arg_completer: args = ' '.join(text[1:]) for comp in arg_completer(args): yield Completion(comp, start_position=-len(document.current_line)) class Cmd2LikeCLI: """Main CLI class with advanced features""" def __init__(self): self.session = PromptSession( history=FileHistory('.cmd_history'), completer=CommandCompleter(self), lexer=PygmentsLexer(BashLexer), complete_while_typing=True, enable_history_search=True, style=custom_style ) self.prompt = HTML('>>> ') self.intro = self.colorize("Welcome to Advanced CLI (Type 'help' for commands)", "info") self.logging_active = False self.log_file = None self.log_file_path = "cli_session.log" def colorize(self, text, style_type): """Apply color to text based on style type""" styles = { "command": "{}", "error": "{}", "warning": "{}", "info": "{}", "success": "{}", } return HTML(styles.get(style_type, "{}").format(text)) def get_commands(self): """Retrieves all available commands (methods starting with 'do_')""" return [ name[3:] for name, _ in inspect.getmembers(self, inspect.ismethod) if name.startswith('do_') ] def get_command_help(self, cmd): """Gets help text from command's docstring""" method = getattr(self, f'do_{cmd}', None) return method.__doc__.strip() if method and method.__doc__ else "No help available" def start_logging(self): """Start session logging""" try: self.log_file = open(self.log_file_path, 'a') self.logging_active = True timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.log_file.write(f"\n\n--- Session started at {timestamp} ---\n") print(self.colorize(f"Logging started to {self.log_file_path}", "success")) except Exception as e: print(self.colorize(f"Error starting logging: {e}", "error")) def stop_logging(self): """Stop session logging""" if self.logging_active: try: timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.log_file.write(f"\n--- Session ended at {timestamp} ---\n") self.log_file.close() self.logging_active = False print(self.colorize("Logging stopped", "success")) except Exception as e: print(self.colorize(f"Error stopping logging: {e}", "error")) def log_command(self, command): """Log command to file if logging is active""" if self.logging_active: try: timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.log_file.write(f"[{timestamp}] {command}\n") except Exception as e: print(self.colorize(f"Logging error: {e}", "error")) def cmdloop(self): """Main REPL loop with nested command support""" print(self.intro) while True: try: user_input = self.session.prompt(self.prompt) # Process nested commands (separated by semicolons) for cmd in user_input.split(';'): cmd = cmd.strip() if cmd: self.onecmd(cmd) except KeyboardInterrupt: print(self.colorize("\nUse 'exit' to quit", "warning")) except EOFError: self.do_exit("") break def onecmd(self, line): """Executes a single command""" line = line.strip() if not line: return # Log command before execution self.log_command(line) parts = line.split(maxsplit=1) cmd_name = parts[0] args = parts[1] if len(parts) > 1 else "" if cmd_name == "help": self.do_help(args) return method_name = f"do_{cmd_name}" method = getattr(self, method_name, None) if method: try: method(args) except Exception as e: print(self.colorize(f"Error executing command: {e}", "error")) else: print(self.colorize(f"Unknown command: {cmd_name}", "error")) def run_script(self, file_path): """Execute commands from a script file""" try: with open(file_path, 'r') as f: for line in f: line = line.strip() if line and not line.startswith('#'): # Skip empty lines and comments print(self.colorize(f"Executing: {line}", "info")) self.onecmd(line) print(self.colorize(f"Script '{file_path}' executed successfully", "success")) except FileNotFoundError: print(self.colorize(f"Script file not found: {file_path}", "error")) except Exception as e: print(self.colorize(f"Error executing script: {e}", "error")) # ------------ Built-in commands ------------ def do_help(self, arg): """Show help for commands""" if arg: # Command-specific help help_text = self.get_command_help(arg) print(f"{self.colorize(arg, 'command')}: {help_text}") else: # List all commands print(self.colorize("\nAvailable commands:", "info")) for cmd in self.get_commands(): print(f" {self.colorize(cmd, 'command'):15} {self.get_command_help(cmd)}") print() def do_echo(self, arg): """Echo back the input""" print(f"Echo: {arg}") def do_exit(self, arg): """Exit the application""" self.stop_logging() print(self.colorize("Goodbye!", "success")) raise SystemExit def do_ls(self, arg): """List directory contents""" path = arg.strip() if arg else "." try: files = os.listdir(path) for f in files: full_path = os.path.join(path, f) if os.path.isdir(full_path): print(self.colorize(f + "/", "info")) else: print(f) except FileNotFoundError: print(self.colorize(f"Directory not found: {path}", "error")) def do_run_script(self, arg): """Execute commands from a script file""" if not arg: print(self.colorize("Please specify a script file", "warning")) return self.run_script(arg.strip()) def do_log_start(self, arg): """Start session logging""" if arg: self.log_file_path = arg.strip() self.start_logging() def do_log_stop(self, arg): """Stop session logging""" self.stop_logging() def do_save_history(self, arg): """Save command history to file""" file_path = arg.strip() if arg else "command_history.txt" try: with open('.cmd_history', 'r') as src, open(file_path, 'w') as dest: dest.write(f"Command History - Saved at {datetime.datetime.now()}\n\n") dest.writelines(src.readlines()) print(self.colorize(f"History saved to {file_path}", "success")) except Exception as e: print(self.colorize(f"Error saving history: {e}", "error")) def do_system(self, arg): """Execute a system shell command""" if not arg: print(self.colorize("Please specify a command to execute", "warning")) return try: result = subprocess.run( arg, shell=True, capture_output=True, text=True ) if result.stdout: print(result.stdout) if result.stderr: print(self.colorize(result.stderr, "error")) print(self.colorize(f"Command exited with code: {result.returncode}", "info")) except Exception as e: print(self.colorize(f"Error executing system command: {e}", "error")) # ------------ Custom argument completers ------------ def argcompleter_ls(self, arg): """Directory completer for ls command""" current_dir = os.path.dirname(arg) if arg else "." base_name = os.path.basename(arg) if arg else "" try: return [ f.name for f in os.scandir(current_dir) if f.name.startswith(base_name) ] except FileNotFoundError: return [] def argcompleter_run_script(self, arg): """File completer for run_script command""" current_dir = os.path.dirname(arg) if arg else "." base_name = os.path.basename(arg) if arg else "" try: return [ f.name for f in os.scandir(current_dir) if f.is_file() and f.name.startswith(base_name) ] except FileNotFoundError: return [] if __name__ == "__main__": cli = Cmd2LikeCLI() cli.cmdloop()