first rev

This commit is contained in:
gwen 2026-08-30 12:38:03 +02:00
commit e6466fc51d
6 changed files with 580 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.venv

310
cli_tool.py Normal file
View file

@ -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('<prompt>>>> </prompt>')
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": "<command>{}</command>",
"error": "<error>{}</error>",
"warning": "<warning>{}</warning>",
"info": "<info>{}</info>",
"success": "<success>{}</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()

19
licence.txt Normal file
View file

@ -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.

228
readme.rst Normal file
View file

@ -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_<command>`` 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

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
prompt_toolkit
pygments

20
todo.txt Normal file
View file

@ -0,0 +1,20 @@
- à ajouter au repos defder
pourquoi HTML output ?
cli_tool.py
HTML("<info>Welcome to Advanced CLI (Type 'help' for commands)</info>")
>>> help
HTML('<info>\nAvailable commands:</info>')
Traceback (most recent call last):
File "/media/gwen/68b01d87-e6a9-4152-acec-dc46320b5c78/gwen/defder/repos_lab/clitool/cli_tool.py", line 310, in <module>
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__