commit e6466fc51d89bce3857c5f30f70bb8d762da9eab Author: gwen Date: Sun Aug 30 12:38:03 2026 +0200 first rev diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d17dae --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.venv diff --git a/cli_tool.py b/cli_tool.py new file mode 100644 index 0000000..deb7c81 --- /dev/null +++ b/cli_tool.py @@ -0,0 +1,310 @@ +""" +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() diff --git a/licence.txt b/licence.txt new file mode 100644 index 0000000..9d837fc --- /dev/null +++ b/licence.txt @@ -0,0 +1,19 @@ +Copyright (c) 2025 WhirlingAI (contact@whirlingai.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..b473555 --- /dev/null +++ b/readme.rst @@ -0,0 +1,228 @@ +Advanced CLI Framework with Prompt Toolkit +============================================ + +.. + + 1. Clear feature overview with emoji icons + 2. Installation instructions + 3. Basic usage example + 4. Command reference table + 5. Detailed advanced usage scenarios + 6. Customization guide for extending functionality + 7. Example session demonstrating key features + 8. Troubleshooting section + 9. License and contribution information + + + +A feature-rich Python command-line interface framework inspired by cmd2 but built on prompt_toolkit with: + +- Full color support +- Command history search +- Script execution capability +- Nested command support +- Session logging +- Command history saving + +.. contents:: This README provides comprehensive documentation for your advanced CLI framework, including: + :depth: 1 + :numbered: + + +Features +-------- + +- 🎨 **Color Support**: Custom color scheme for commands, errors, and information +- 🔍 **History Search**: Press ``Ctrl+R`` to search through command history +- 📜 **Script Execution**: Run batch commands from files +- 🔗 **Nested Commands**: Execute multiple commands separated by semicolons +- 📝 **Session Logging**: Record entire sessions to files +- 💾 **History Saving**: Export command history to files +- 💻 **System Commands**: Execute shell commands directly +- 🚦 **Error Handling**: Colored error messages with details +- 🤖 **Smart Autocompletion**: Context-aware suggestions for commands and arguments + +Installation +------------ + +.. code-block:: bash + + pip install prompt_toolkit pygments + +Basic Usage +----------- + +.. code-block:: bash + + python advanced_cli.py + +Key Commands +------------ ++---------------+-------------------------------------------------------+ +| Command | Description | ++===============+=======================================================+ +| help | Show help for commands | ++---------------+-------------------------------------------------------+ +| echo | Echo back the input | ++---------------+-------------------------------------------------------+ +| exit | Exit the application | ++---------------+-------------------------------------------------------+ +| ls | List directory contents (with color) | ++---------------+-------------------------------------------------------+ +| run_script | Execute commands from a script file | ++---------------+-------------------------------------------------------+ +| log_start | Start session logging (optional filename argument) | ++---------------+-------------------------------------------------------+ +| log_stop | Stop session logging | ++---------------+-------------------------------------------------------+ +| save_history | Save command history to file | ++---------------+-------------------------------------------------------+ +| system | Execute a system shell command | ++---------------+-------------------------------------------------------+ + +Advanced Usage +-------------- + +1. **Colorized Output**: + The CLI automatically colorizes different types of messages: + - Commands: Green + - Errors: Red + - Information: Blue + - Success messages: Bright Green + - Directories in listings: Blue + +2. **Nested Commands**: + Execute multiple commands in one line by separating them with semicolons: + + .. code-block:: text + + >>> echo Hello; echo World; ls + +3. **Script Execution**: + Create a script file (e.g., ``commands.txt``): + + .. code-block:: text + + # My command script + echo Running script + ls + system date + + Then execute it: + + .. code-block:: text + + >>> run_script commands.txt + +4. **Session Logging**: + + .. code-block:: text + + # Start logging to a file + >>> log_start session.log + + # Execute commands... + >>> echo This is being logged + >>> ls + + # Stop logging + >>> log_stop + +5. **History Management**: + + .. code-block:: text + + # Save command history to file + >>> save_history my_history.txt + +6. **System Commands**: + + .. code-block:: text + + # Execute any system command + >>> system ls -l + >>> system python --version + +Customization +------------- +1. **Add New Commands**: + Create methods starting with ``do_`` in the CLI class: + + .. code-block:: python + + def do_mycommand(self, arg): + """Description of mycommand""" + print(f"Executed with: {arg}") + +2. **Custom Argument Completion**: + Implement argument completers for your commands: + + .. code-block:: python + + def argcompleter_mycommand(self, arg): + return ["option1", "option2", "option3"] + +3. **Modify Colors**: + Edit the style dictionary in the ``__init__`` method: + + .. code-block:: python + + custom_style = Style.from_dict({ + 'prompt': 'ansicyan bold', + 'command': 'ansigreen', + 'error': 'ansired bold', + # ... other styles ... + }) + +Example Session +--------------- +.. code-block:: text + + Welcome to Advanced CLI (Type 'help' for commands) + >>> log_start session.log + Logging started to session.log + + >>> ls; system date + file1.txt + file2.py + docs/ + Tue Jun 19 15:30:45 CEST 2025 + + >>> run_script commands.txt + Executing: echo Running script + Echo: Running script + Executing: ls + file1.txt + file2.py + docs/ + Executing: system date + Tue Jun 19 15:31:22 CEST 2025 + Script 'commands.txt' executed successfully + + >>> save_history + History saved to command_history.txt + + >>> exit + Goodbye! + +Troubleshooting +--------------- + +- **Command not recognized**: Ensure your method starts with ``do_`` prefix +- **Autocompletion not working**: Implement ``argcompleter_`` method +- **Logging errors**: Check file write permissions + +License +------- + +MIT License - Free for personal and commercial use + +Contributing +------------ + +1. Fork the repository +2. Create your feature branch +3. Commit your changes +4. Push to the branch +5. Create a new Pull Request + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a3594ef --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +prompt_toolkit +pygments diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..53c4bb3 --- /dev/null +++ b/todo.txt @@ -0,0 +1,20 @@ +- à ajouter au repos defder + +pourquoi HTML output ? + + cli_tool.py +HTML("Welcome to Advanced CLI (Type 'help' for commands)") +>>> help +HTML('\nAvailable commands:') +Traceback (most recent call last): + File "/media/gwen/68b01d87-e6a9-4152-acec-dc46320b5c78/gwen/defder/repos_lab/clitool/cli_tool.py", line 310, in + cli.cmdloop() + File "/media/gwen/68b01d87-e6a9-4152-acec-dc46320b5c78/gwen/defder/repos_lab/clitool/cli_tool.py", line 144, in cmdloop + self.onecmd(cmd) + File "/media/gwen/68b01d87-e6a9-4152-acec-dc46320b5c78/gwen/defder/repos_lab/clitool/cli_tool.py", line 165, in onecmd + self.do_help(args) + File "/media/gwen/68b01d87-e6a9-4152-acec-dc46320b5c78/gwen/defder/repos_lab/clitool/cli_tool.py", line 205, in do_help + print(f" {self.colorize(cmd, 'command'):15} {self.get_command_help(cmd)}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +TypeError: unsupported format string passed to HTML.__format__ +