click — Cursor rule: what it does, how to install
This rule guides Python CLI work with the click library, covering command structure, parameters, output, and help text.
.cursor/rules/click.mdcWhat click does
The rule covers a src layout with command groups, the distinction between arguments and options, type hints, and CLI help. It recommends click.echo or click.secho for output instead of print(), citing Unicode handling and styling across platforms.
Save it as .cursor/rules/click.mdc; it applies to **/*.py. The modular layout is aimed at non-trivial CLIs, so it may be more structure than a one-command tool needs. The examples use Python and click.
What it helps with
- split CLI commands into modules
- keep required positional values separate from optional settings
- add type hints to command parameters
- send CLI output through
click.echoorclick.secho - write clearer help with practical command examples
Example prompts for the agent
- Restructure this Python CLI built with
clickinto asrclayout with separate commands. - Review whether each CLI value should be an argument or an option in this
clickproject. - Add type hints and improve the help text for these
clickcommands.
How to install click
Manually: put the files in place
.cursor/rules/click.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/click.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/click.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/click.mdManually: put the files in place
.clinerules/click.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about click
How do I install click in Cursor?
Add the rule at .cursor/rules/click.mdc. It applies to files matching **/*.py.
What does the click rule do?
It guides Python CLI code using click, including command organization, parameters, output, typing, and help text.
How does the rule suggest organizing click commands?
For a non-trivial CLI, it recommends a src layout, a separate entry point, and commands grouped with click.Group. That structure may be unnecessary for a small CLI.
Source text
click.mdc · CC0-1.0
---
description: This guide outlines definitive best practices for building robust, maintainable, and user-friendly command-line interfaces (CLIs) using the `click` Python library, emphasizing modern patterns and common pitfalls.
globs: **/*.py
---
# click Best Practices
`click` is the undisputed standard for building Python CLIs. Follow these rules to ensure your tools are consistent, maintainable, and a joy to use.
## 1. Code Organization & Structure
For any non-trivial CLI, structure your project to promote modularity and testability. Always use a `src` layout.
### ✅ GOOD: Modular `src` Layout
Separate your main CLI entry point from individual commands. Use `click.Group` to organize subcommands into logical modules.
```python
# src/mycli/__init__.py
import click
from .commands.greet import greet_command
from .commands.config import config_group
@click.group()
@click.version_option(version="1.0.0")
@click.pass_context
def cli(ctx: click.Context) -> None:
"""My Awesome CLI Tool."""
ctx.ensure_object(dict) # Initialize context object
# Any global setup or configuration can go here
cli.add_command(greet_command)
cli.add_command(config_group)
# src/mycli/commands/greet.py
import click
@click.command(name="greet")
@click.argument("name", type=str)
@click.option("--formal", is_flag=True, help="Use a formal greeting.")
def greet_command(name: str, formal: bool) -> None:
"""Greets the given NAME."""
if formal:
click.secho(f"Greetings, {name}!", fg="blue")
else:
click.echo(f"Hello, {name}!")
# src/mycli/commands/config.py
import click
@click.group(name="config")
def config_group() -> None:
"""Manage configuration settings."""
pass
@config_group.command(name="set")
@click.argument("key", type=str)
@click.argument("value", type=str)
def config_set(key: str, value: str) -> None:
"""Set a configuration KEY to VALUE."""
click.echo(f"Setting '{key}' to '{value}'")
@config_group.command(name="get")
@click.argument("key", type=str)
def config_get(key: str) -> None:
"""Get the value of a configuration KEY."""
click.echo(f"Getting value for '{key}'")
```
### ❌ BAD: Monolithic `cli.py`
Avoid dumping all commands and logic into a single file. This quickly becomes unmanageable.
```python
# cli.py (BAD)
import click
@click.group()
def cli():
pass
@cli.command()
@click.argument("name")
def greet(name): # Logic for greet command
click.echo(f"Hello {name}!")
@cli.command()
@click.argument("key")
@click.argument("value")
def config_set(key, value): # Logic for config set command
click.echo(f"Setting {key} to {value}")
```
## 2. Argument Parsing: Arguments vs. Options
Follow `click`'s conventions:
* **Arguments**: Positional, required values.
* **Options**: Flag-based, optional settings.
### ✅ GOOD: Clear Distinction
```python
import click
@click.command()
@click.argument("input_file", type=click.Path(exists=True)) # Positional, required file
@click.option("--output", "-o", type=click.Path(), help="Output file path.") # Optional flag
@click.option("--force", is_flag=True, help="Overwrite existing output.") # Flag without value
def process(input_file: str, output: str | None, force: bool) -> None:
"""Processes INPUT_FILE with optional OUTPUT."""
click.echo(f"Processing {input_file}...")
if output:
click.echo(f"Outputting to {output} (Force: {force})")
```
### ❌ BAD: Misusing Arguments for Optional Settings
Using positional arguments for optional values makes `help` output confusing and usage less intuitive.
```python
import click
@click.command()
@click.argument("input_file")
@click.argument("output_file", required=False) # BAD: Optional positional argument
def process_bad(input_file: str, output_file: str | None) -> None:
"""Processes INPUT_FILE. Can optionally specify OUTPUT_FILE."""
# This makes the CLI harder to use and read.
click.echo(f"Input: {input_file}, Output: {output_file}")
```
## 3. Output: `click.echo` and `click.secho`
**Always** use `click.echo` or `click.secho` instead of `print()`. This ensures consistent Unicode handling and proper styling across all platforms, especially Windows.
### ✅ GOOD: Platform-Agnostic, Styled Output
```python
import click
@click.command()
def log_messages() -> None:
"""Demonstrates various output types."""
click.echo("This is a standard message.")
click.secho("This is an important warning!", fg="yellow", err=True) # To stderr, colored
click.secho("Success!", fg="green", bold=True)
click.echo(f"Unicode support: こんにちは世界") # Works everywhere
```
### ❌ BAD: Inconsistent Output
`print()` lacks cross-platform Unicode support and styling capabilities.
```python
import click
@click.command()
def log_messages_bad() -> None:
"""Demonstrates bad output types."""
print("This message might break on Windows terminals with unicode.") # BAD
print("\033[91mThis is a red error. (Manual ANSI codes are fragile)\033[0m") # BAD
```
## 4. Type Hints
Mandatory. Use Python's type hints with `click`'s built-in types (e.g., `click.Path`, `click.File`) for robust validation and improved IDE support.
### ✅ GOOD: Fully Type-Hinted
```python
import click
from typing import TextIO
@click.command()
@click.argument("count", type=int)
@click.option("--name", type=str, default="World")
@click.option("--config", type=click.Path(exists=True, dir_okay=False), help="Path to config file.")
@click.option("--output", type=click.File("w"), help="Output stream.")
def greet_typed(count: int, name: str, config: str | None, output: TextIO | None) -> None:
"""Greets NAME COUNT times."""
for _ in range(count):
(output or click.get_text_stream("stdout")).write(f"Hello, {name}!\n")
if config:
click.echo(f"Using config from: {config}")
```
### ❌ BAD: Untyped Parameters
Lack of type hints makes code harder to understand, validate, and refactor.
```python
import click
@click.command()
@click.argument("count") # BAD: No type hint
@click.option("--name", default="World") # BAD: No type hint
def greet_untyped(count, name):
"""Greets NAME COUNT times."""
# Type errors will only appear at runtime
for _ in range(int(count)):
click.echo(f"Hello, {name}!")
```
## 5. Comprehensive Help Text & Examples
Good help text is crucial. Always provide a concise summary, detailed explanations for options, and practical usage examples.
### ✅ GOOD: Detailed Help with Examples
```python
import click
@click.command()
@click.argument("source", type=click.Path(exists=True))
@click.argument("destination", type=click.Path())
@click.option("--recursive", "-r", is_flag=True, help="Copy directories recursively.")
@click.option("--force", "-f", is_flag=True, help="Overwrite existing files without prompting.")
def copy(source: str, destination: str, recursive: bool, force: bool) -> None:
"""
Copies files or directories from SOURCE to DESTINATION.
Examples:
\b
# Copy a single file
mycli copy file.txt backup/file.txt
# Copy a directory recursively
mycli copy -r my_dir backup/my_dir
# Force overwrite a file
mycli copy -f important.log /var/log/important.log
"""
click.echo(f"Copying '{source}' to '{destination}' (Recursive: {recursive}, Force: {force})")
```
### ❌ BAD: Minimalist Help Text
Vague help text leaves users guessing.
```python
import click
@click.command()
@click.argument("source")
@click.argument("destination")
@click.option("--recursive", "-r", is_flag=True) # BAD: No help text
def copy_bad(source: str, destination: str, recursive: bool) -> None:
"""Copies things.""" # BAD: Vague summary
click.echo(f"Copying {source} to {destination}")
```
## 6. Packaging with `pyproject.toml`
Distribute your CLI tools reliably using `pyproject.toml` for metadata and entry points. This is the modern Python packaging standard.
```toml
# pyproject.toml
[project]
name = "mycli"
version = "1.0.0"
description = "My Awesome CLI Tool"
requires-python = ">=3.10"
dependencies = [
"click>=8.3.1",
]
[project.scripts]
mycli = "mycli:cli" # Maps 'mycli' command to 'src/mycli/__init__.py:cli'
```
Install with `pipx install .` for isolated execution.
## 7. Testing with `CliRunner`
Automated testing is non-negotiable. Use `click.testing.CliRunner` to simulate CLI invocations and assert outputs.
```python
# tests/test_cli.py
from click.testing import CliRunner
from mycli import cli # Assuming 'mycli' is installed or in PYTHONPATH
def test_greet_command() -> None:
runner = CliRunner()
result = runner.invoke(cli, ["greet", "Alice"])
assert result.exit_code == 0
assert "Hello, Alice!" in result.output
def test_greet_formal_option() -> None:
runner = CliRunner()
result = runner.invoke(cli, ["greet", "Bob", "--formal"])
assert result.exit_code == 0
assert "Greetings, Bob!" in result.output
assert "blue" in result.output # Check for color codes if applicable
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc