If you’ve ever wanted to make your CLI more interactive and dynamic, building a real-time command interaction system could be the answer. By leveraging Python’s introspection capabilities, Click for managing commands, and Rich for formatting output, you can create a powerful, flexible CLI that responds intelligently to user input. Instead of manually hardcoding each command, your CLI can automatically discover and execute commands, making the user experience smoother and more engaging.
Colorful console chaos: where Click commands meet Rich output—because even the terminal likes to show off in style!
Why Use Click and Markdown?
Click simplifies the management of commands, argument parsing, and help generation. It also allows for easy command structuring and option handling.
Rich enables you to output beautifully formatted Markdown directly in the terminal, making results not just functional but also visually engaging.
By combining these two libraries with Python introspection, you can build an interactive chat feature that dynamically discovers and executes commands while displaying output in a rich, readable format. For a practical example, see how StoryCraftr uses a similar approach to streamline AI-driven writing workflows: https://storycraftr.app.
Building the Interactive Chat System
1. Setting Up the Basic Chat Command
The chat command initializes the session, allowing users to interact with the CLI. Here, we capture user input, which will be dynamically mapped to the appropriate Click commands.
import os
import click
import shlex
from rich.console import Console
from rich.markdown import Markdown
console = Console()
@click.command()
@click.option("--project-path", type=click.Path(), help="Path to the project directory")
def chat(project_path=None):
"""
Start a chat session with the assistant for the given project.
"""
if not project_path:
project_path = os.getcwd()
console.print(
f"Starting chat for [bold]{project_path}[/bold]. Type [bold green]exit()[/bold green] to quit."
)
# Start the interactive session
while True:
user_input = console.input("[bold blue]You:[/bold blue] ")
# Handle exit
if user_input.lower() == "exit()":
console.print("[bold red]Exiting chat...[/bold red]")
break
# Call the function to handle command execution
execute_cli_command(user_input)
2. Introspection to Discover and Execute Commands
Using Python introspection, we dynamically discover available commands and execute them. One crucial part here is that Click commands are decorated functions. To execute the actual logic, we need to call the undecorated function (i.e., the callback).
Here’s how you can dynamically execute commands using introspection and handle Click’s decorators:
import inspect
import your_project_cmd # Replace with your actual module containing commands
command_modules = {"project": your_project_cmd} # List your command modules here
def execute_cli_command(user_input):
"""
Function to execute CLI commands dynamically based on the available modules,
calling the undecorated function directly.
"""
try:
# Use shlex.split to handle quotes and separate arguments correctly
parts = shlex.split(user_input)
module_name = parts[0]
command_name = parts[1].replace("-", "_") # Replace hyphens with underscores
command_args = parts[2:] # Keep the rest of the arguments as a list
# Check if the module exists in command_modules
if module_name in command_modules:
module = command_modules[module_name]
# Introspection: Get the function by name
if hasattr(module, command_name):
cmd_func = getattr(module, command_name)
# Check if it's a Click command and strip the decorator
if hasattr(cmd_func, "callback"):
# Call the underlying undecorated function
cmd_func = cmd_func.callback
# Check if it's a callable (function)
if callable(cmd_func):
console.print(
f"Executing command from module: [bold]{module_name}[/bold]"
)
# Directly call the function with the argument list
cmd_func(*command_args)
else:
console.print(
f"[bold red]'{command_name}' is not a valid command[/bold red]"
)
else:
console.print(
f"[bold red]Command '{command_name}' not found in {module_name}[/bold red]"
)
else:
console.print(f"[bold red]Module {module_name} not found[/bold red]")
except Exception as e:
console.print(f"[bold red]Error executing command: {str(e)}[/bold red]")
How Does This Work?
-
Input Parsing: We use
shlex.split
to handle input like command-line arguments. This ensures that quoted strings and special characters are processed correctly. -
Module and Command Lookup: The input is split into
module_name
andcommand_name
. The command name is processed to replace hyphens with underscores to match the Python function names. -
Introspection: We use
getattr()
to dynamically fetch the command function from the module. If it is a Click command (i.e., has thecallback
attribute), we access the actual function logic by stripping the Click decorator. - Command Execution: Once we retrieve the undecorated function, we pass the arguments and call it, just as if we were directly invoking a Python function.
3. Example CLI Commands
Let’s consider some sample commands within a project module that users can call interactively via the chat:
@click.group()
def project():
"""Project management CLI."""
pass
@project.command()
def init():
"""Initialize a new project."""
console.print("[bold green]Project initialized![/bold green]")
@project.command()
@click.argument("name")
def create(name):
"""Create a new component in the project."""
console.print(f"[bold cyan]Component {name} created.[/bold cyan]")
@project.command()
def status():
"""Check the project status."""
console.print("[bold yellow]All systems operational.[/bold yellow]")
Executing the Chat Interface
To run the interactive chat system:
- Make sure your modules (like
project
) are listed incommand_modules
. - Run the command:
python your_cli.py chat --project-path /path/to/project
Once the session starts, users can input commands like:
You: project init You: project create "Homepage"
The output will be displayed in a well-formatted manner using Rich Markdown:
[bold green]Project initialized![/bold green] [bold cyan]Component Homepage created.[/bold cyan]
Conclusion
By combining Click for command management, Rich for Markdown formatting, and Python introspection, we can build a powerful and interactive chat system for CLIs. This approach allows you to dynamically discover and execute commands while presenting output in an elegant, readable format.
Key Highlights:
- Dynamic Command Execution: Introspection enables you to discover and run commands without hardcoding them.
- Rich Output: Using Rich Markdown ensures the output is easy to read and visually appealing.
- Flexibility: This setup allows for flexibility in command structure and execution.