first rev
This commit is contained in:
commit
e6466fc51d
6 changed files with 580 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.venv
|
||||
310
cli_tool.py
Normal file
310
cli_tool.py
Normal 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
19
licence.txt
Normal 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
228
readme.rst
Normal 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
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
prompt_toolkit
|
||||
pygments
|
||||
20
todo.txt
Normal file
20
todo.txt
Normal 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__
|
||||
|
||||
Loading…
Reference in a new issue