Examples gallery¶
The manifests in the examples/ directory of the
repository. Each entry shows the file as it is in the repository. The code cliffy generates for them is
committed next to them in
examples/generated/.
To try one, clone the repository and run the command shown from its root.
Files written in a custom spec, and the blueprints that turn them into CLIs, are in
examples/blueprints/ and
described in Blueprints.
| Example | Backend | Requires | Has tests |
|---|---|---|---|
| hello.yaml | Typer | nothing | yes |
| nested-cli.yaml | Typer | nothing | no |
| town.yaml | Typer | nothing | no |
| template.yaml | Typer | nothing | no |
| environ.yaml | Typer | nothing | no |
| db.yaml | Typer | rich |
no |
| penv.yaml | Typer | nothing | no |
| pydev.yaml | Typer | nothing | no |
| taskmaster.yaml | Typer | tabulate>=0.9.0 |
yes |
| todo.yaml | Typer | tabulate, rich |
no |
| requires.yaml | Typer | requests >= 2.30.0, six < 1.0.0 |
no |
| callbacks.yaml | Typer | nothing | yes |
| click_hello.yaml | Click | nothing | yes |
| aiprompt.yaml | Typer | openai>=1.0.0, rich>=13.0.0, keyring>=24.0.0 |
no |
hello¶
The smallest useful manifest: one shell command, one Python command and a tests section.
examples/hello.yaml
# yaml-language-server: $schema=cliffy_schema.json
name: hello
version: 0.1.0
help: Hello world!
commands:
shell: $echo "hello from shell"
python: print("hello from python")
examples:
- command: hello shell
description: Say hello from a shell command.
- command: hello python
description: Say hello from Python.
tests:
- shell: assert result.exit_code == 0
- python: assert result.output == "hello from python\n"
- shell --help: assert "Show this message and exit" in result.output
- -h: assert "Hello world!" in result.output
- -h: assert "Examples:" in result.output and "$ hello shell" in result.output
nested-cli¶
Command groups nested two levels deep with dotted keys.
examples/nested-cli.yaml
town¶
A larger CLI: groups with help text, shared types, short option aliases, command aliases and a (*).list command that is added to every group.
examples/town.yaml
# yaml-language-server: $schema=cliffy_schema.json
manifestVersion: v3 # optional (default is the latest)
name: town
version: 0.1.1
imports: |
import re
import time
functions:
- |
def format_money(money: float):
return "${:.2f}".format(money)
commands:
people:
help: Manage people
shops:
help: Manage shops
home:
help: Manage homes
land:
help: Manage land
land.build:
params: [name: Name!, address: str!, --value|-v: int = 100]
run: |
"""Build land"""
print(f"building land {name}")
land.sell:
params: [name: Name!, money: Money]
run: |
"""Sell land"""
print(f"selling land {name}")
land.buy:
params: [name: Name!, money: Money]
run: |
"""Buy land"""
print(f"buying land {name} for {format_money(money)}")
people.add:
params: [fullname: str!, age: int!, --home|-h: str]
run: |
"""Add a person to town"""
print(f"adding person {fullname}, {age}, {home}")
people.remove:
params: [fullname: str!]
run: |
"""Remove person from town"""
print(f"removing person {fullname}")
shops.build:
params: [name: Name!, land: str!, --type|-t: str]
run: |
"""Build a shop"""
print(f"building shop {name} ({type}) on land {land}")
shops.sell:
params: [name: Name!, money: Money]
run: |
"""Sell a shop"""
print(f"selling shop {name} for ${money}")
shops.buy:
params: [name: Name!, money: Money]
run: |
"""Buy a shop"""
print(f"buying shop {name} for ${money}")
home.build|bu:
params: [address: str!, land: str, owner: str]
run: |
"""Build a home"""
print(f"building home at {address} for {owner} on land {land}")
home.sell:
params: [address: str!, money: Money]
aliases: ["s"]
run: |
"""Sell a home"""
print(f"selling home {address} for {format_money(money)}")
home.buy:
params: [address: str!, money: Money]
aliases: ["b"]
run: |
"""Buy a home"""
print(f"buying home {address} for {money}")
(*).list:
params: [--limit|-l: int]
run: |
"""Get a list of {(*)}"""
print(f"listing {(*)}")
types:
Money: float = typer.Option(..., help="Amount of money", min=0)
Name: str = typer.Argument(..., help="Name")
# TODO
db: sqlite
template¶
A command template that gives two commands the same params, a run script written as a list, and per-command config.
examples/template.yaml
# yaml-language-server: $schema=cliffy_schema.json
name: template
version: 0.1.0
vars:
GLOBAL_VAR: hello
command_templates:
common_params:
params:
- local_arg: str!
- --local_arg_2: str = ""
commands:
hello:
help: |
Demo. global var and command args usage.
Runs hello command by default based on GLOBAL_VAR.
template: common_params
run:
- $ echo "{{GLOBAL_VAR}} f{local_arg} --f{local_arg_2}"
config:
deprecated: True
context_settings:
help_option_names: ["-h", "--helpme"]
debug:
help: ""
template: common_params
run: |
print(f"arg1: {local_arg}, arg2: --{local_arg_2}")
environ¶
Environment variables read when the manifest is loaded (vars with env) and when a command runs.
examples/environ.yaml
# yaml-language-server: $schema=cliffy_schema.json
name: environ
description: Read environment variables
version: 0.1.0
help: Environment variable reader
imports:
- import os
vars:
# var setup on load time
default_env_var: "{{ env['ENVIRON_LOAD_VAR'] or 'hello' }}"
commands:
read:
params:
- env-var: str!
run: $ echo f{os.environ[env_var]}
hello: $ echo {{ default_env_var }}
bye: $ echo f{os.environ['ENVIRON_BYE_TEXT']} # on run time
hello-bye: |
$ echo {{ default_env_var }} bye # the var was filled in on load time
db¶
Prompting options defined in types, YAML anchors to share params, pre_run, command aliases and cli_options.
examples/db.yaml
# yaml-language-server: $schema=cliffy_schema.json
manifestVersion: v3 # optional (Default will be the latest)
name: db
version: 0.1.0
help: Database CLI
cli_options:
add_completion: False
requires:
- rich
imports: |
from rich.console import Console
console = Console()
commands:
create|mk:
params: &db_args
- name: DatabaseName
run: |
"""Create a new database"""
console.print(f"Creating database {name}", style="green")
pre_run: |
console.print("Running pre-run hook", style="green")
delete|rm:
params: *db_args
run: |
"""Delete a database"""
sure = typer.confirm("Are you really really really sure?")
if sure:
console.print(f"Deleting database {name}", style="red")
else:
console.print(f"Back to safety!", style="green")
list|ls: |
"""List databases"""
print("Listing all databases")
view|v:
params:
- <<: *db_args
- table: TableName
run: |
"""View database table"""
console.print(f"Viewing {table} table for {name} DB")
types:
DatabaseName: str = typer.Option(..., prompt="What is the name of the database?", confirmation_prompt=True)
TableName: str = typer.Option(..., prompt="What is the name of the table?")
penv¶
A virtualenv manager: vars holding Python expressions, helper functions and shell lines that interpolate Python values with f{...}.
examples/penv.yaml
# yaml-language-server: $schema=cliffy_schema.json
name: penv
version: 0.1.0
help: |
~ Virtualenv store ~
Simplify virtualenvs
imports: |
import os
from pathlib import Path
from shutil import rmtree
vars:
HOME_PATH: {str(Path.home())}
DEFAULT_VENV_STORE: {'f"{HOME_PATH}/venv-store/"'}
functions:
- |
def get_venv_path(name: str) -> str:
return f"{DEFAULT_VENV_STORE}{name}"
- |
def venv_exists(name: str) -> bool:
return os.path.exists(get_venv_path(name))
commands:
ls:
help: List venvs in the store
run: $ ls f{DEFAULT_VENV_STORE}
rm:
help: Remove a venv
params:
- name: str!
run: rmtree(get_venv_path(name))
go:
help: Activate a venv
params:
- name: str!
- --interpreter | -i: str = "python"
run: |
if venv_exists(name):
print(f"~ sourcing {name}")
else:
print(f"~ creating {name}")
$ f{interpreter} -m venv f{os.path.join(DEFAULT_VENV_STORE, name)}
os.system(f"""bash -c ". {get_venv_path(name)}/bin/activate; env PS1='\\[\\e[38;5;211m\\]({name})\\[\\e[\033[00m\\] \\w $ ' bash --norc\"""")
pydev¶
Developer chores as run-script lists that mix shell and Python lines, with helper functions.
examples/pydev.yaml
# yaml-language-server: $schema=cliffy_schema.json
manifestVersion: v3 # optional (default is the latest)
name: pydev
version: 0.1.0
help: Python developer utilities
imports: |
import sys
functions:
- |
def run_cmd(cmd: str):
"""Helper function to run a command."""
try:
result = subprocess.run(cmd, shell=True, check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
except subprocess.CalledProcessError as e:
print(f"Error running command: {cmd}")
print(f"Exit code: {e.returncode}")
print(f"Output: {e.output.decode('utf-8').strip()}")
sys.exit(1)
return result.stdout.decode('utf-8').strip()
- |
def get_file_contents(file_name: str):
with open(file_name, 'r') as f:
contents = f.read()
return contents
- |
def write_to_file(file_name: str, contents: str):
with open(file_name, 'w') as f:
f.write(contents)
commands:
clean:
help: Clean pycache artifacts
run:
- $ find . -type f -name "*.pyc" -delete
- $ find . -type d -name "__pycache__" -delete
- $ echo Cleaned!
format:
help: Format code with black
run:
- $ black .
- $ echo Formatted!
lint:
help: Run linters on the code
run:
- $ flake8 .
- $ black --check .
- $ mypy .
- print("Linting successful!")
test:
help: Run tests
run:
- $ pytest
- print("Tests passed!")
bump:
help: Bump the package version
params:
- --major: VersionPart
- --minor: VersionPart
- --patch: VersionPart
run: |
current_version = run_cmd("python setup.py --version")
print(f"Current version: {current_version}")
parts = current_version.split('.')
new_version = '.'.join(parts[:-1] + [str(int(parts[-1]) + 1)])
print(f"New version: {new_version}")
contents = get_file_contents("setup.py")
contents = contents.replace(current_version, new_version)
write_to_file("setup.py", contents)
print(f"Bumped version from {current_version} to {new_version}.")
types:
VersionPart: int = typer.Option(..., help="Version part", min=0)
taskmaster¶
A task manager with JSON storage, a vars placeholder used inside functions, shared types, nested groups and a full tests section.
examples/taskmaster.yaml
# yaml-language-server: $schema=cliffy_schema.json
name: taskmaster
version: 0.1.0
help: A task management CLI
requires:
- tabulate>=0.9.0
imports: |
import json
from datetime import datetime
from tabulate import tabulate
vars:
data_file: "tasks.json"
functions:
- |
def load_data():
try:
with open("{{data_file}}", "r") as f:
return json.load(f)
except FileNotFoundError:
return {"projects": [], "tasks": []}
- |
def save_data(data):
with open("{{data_file}}", "w") as f:
json.dump(data, f, indent=2)
- |
def format_date(date_str):
return datetime.strptime(date_str, "%Y-%m-%d").strftime("%b %d, %Y")
types:
ProjectName: str = typer.Argument(..., help="Name of the project")
TaskName: str = typer.Argument(..., help="Name of the task")
DueDate: str = typer.Option(None, "--due", "-d", help="Due date (YYYY-MM-DD)")
Priority: int = typer.Option(1, "--priority", "-p", help="Priority (1-5)", min=1, max=5)
commands:
project.add:
params: [name: ProjectName]
run: |
data = load_data()
if name not in data["projects"]:
data["projects"].append(name)
save_data(data)
print(f"Project '{name}' added successfully.")
else:
print(f"Project '{name}' already exists.")
project.list:
run: |
data = load_data()
if data["projects"]:
print("Projects:")
for project in data["projects"]:
print(f"- {project}")
else:
print("No projects found.")
project.remove:
params: [name: ProjectName]
run: |
data = load_data()
if name in data["projects"]:
data["projects"].remove(name)
data["tasks"] = [task for task in data["tasks"] if task["project"] != name]
save_data(data)
print(f"Project '{name}' and its tasks removed successfully.")
else:
print(f"Project '{name}' not found.")
task.add:
params: [project: ProjectName, name: TaskName, due_date: DueDate, priority: Priority]
run: |
data = load_data()
if project not in data["projects"]:
print(f"Project '{project}' not found. Please create the project first.")
return
task = {
"project": project,
"name": name,
"due_date": due_date,
"priority": priority,
"completed": False
}
data["tasks"].append(task)
save_data(data)
print(f"Task '{name}' added to project '{project}' successfully.")
task.list:
params: [project: ProjectName]
run: |
data = load_data()
tasks = data["tasks"]
if project:
tasks = [task for task in tasks if task["project"] == project]
if tasks:
table_data = [
[
task["project"],
task["name"],
format_date(task["due_date"]) if task["due_date"] else "N/A",
task["priority"],
"x" if task["completed"] else "o"
]
for task in tasks
]
headers = ["Project", "Task", "Due Date", "Priority", "Completed"]
print(tabulate(table_data, headers=headers, tablefmt="grid"))
else:
print("No tasks found.")
task.complete:
params: [project: ProjectName, name: TaskName]
run: |
data = load_data()
for task in data["tasks"]:
if task["project"] == project and task["name"] == name:
task["completed"] = True
save_data(data)
print(f"Task '{name}' in project '{project}' marked as completed.")
return
print(f"Task '{name}' in project '{project}' not found.")
task.remove:
params: [project: ProjectName, name: TaskName]
run: |
data = load_data()
data["tasks"] = [task for task in data["tasks"] if task["project"] != project or task["name"] != name]
save_data(data)
print(f"Task '{name}' removed from project '{project}' successfully.")
backup:
help: Create a backup of the task data
run: |
$ cp {{data_file}} {{data_file}}.backup
$ echo "Backup created: {{data_file}}.backup"
tests:
- project add test1: assert result.exit_code == 0
- project list: assert "test1" in result.output
- project add test2: assert result.exit_code == 0
- project list: assert "test1" in result.output and "test2" in result.output
- task add test1 task1: assert result.exit_code == 0
- task add test1 task2: assert result.exit_code == 0
- task list test1: assert "task1" in result.output and "task2" in result.output
- task remove test1 task1: assert "removed" in result.output
- project remove test1: assert "removed" in result.output
- task list test1: assert "No tasks found" in result.output
- $ rm tasks.json
todo¶
A sqlite-backed todo list that depends on tabulate and rich.
examples/todo.yaml
# yaml-language-server: $schema=cliffy_schema.json
name: todo
version: 1.0.0
requires:
- tabulate # For pretty table output
- rich # For colored terminal output
imports: |
import sqlite3
from pathlib import Path
from tabulate import tabulate
from rich import print
commands:
create:
help: Create a new database with tasks table
params:
- name: str = typer.Option(..., prompt=True, confirmation_prompt=True)
run: |
db_path = Path(f"{name}.db")
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE tasks (id INTEGER PRIMARY KEY, task TEXT NOT NULL, done BOOLEAN NOT NULL)")
# insert example tasks
conn.execute("INSERT INTO tasks (task, done) VALUES ('Fight for your right!', 0)")
conn.execute("INSERT INTO tasks (task, done) VALUES ('To party!', 1)")
conn.commit()
conn.close()
print(f"✨ Created database {db_path} with tasks table")
drop|rm:
help: Drop a database
params: [name: str!]
run: |
db_path = Path(f"{name}.db")
db_path.unlink()
print(f"🗑️ Removed database {db_path}")
tasks:
help: List tasks in database
params: [name: str!]
run: |
conn = sqlite3.connect(f"{name}.db")
cursor = conn.execute("SELECT * FROM tasks")
tasks = cursor.fetchall()
conn.close()
print(tabulate(tasks, headers=['ID', 'Task', 'Done'], tablefmt='grid'))
add:
help: Add a new task
params: [name: str!, task: str!]
run: |
conn = sqlite3.connect(f"{name}.db")
conn.execute("INSERT INTO tasks (task, done) VALUES (?, 0)", (task,))
conn.commit()
conn.close()
print(f"📝 Added task: {task}")
complete:
help: Mark a task as complete
params: [name: str!, id: int!]
run: |
conn = sqlite3.connect(f"{name}.db")
conn.execute("UPDATE tasks SET done = 1 WHERE id = ?", (id,))
conn.commit()
conn.close()
print(f"🎉 Marked task {id} as complete")
requires¶
Version specifiers in requires. Loading fails on purpose unless requests>=2.30.0 and six<1.0.0 are installed, which is what the test suite checks.
examples/requires.yaml
callbacks¶
Callbacks: code that runs before every command of the CLI or of one group, with params that become options of the CLI itself and access to the context as ctx.
examples/callbacks.yaml
# yaml-language-server: $schema=cliffy_schema.json
name: callbacks
version: 0.1.0
help: Callbacks run before the commands of a CLI or of a command group
callbacks:
# no `group`: runs before every command, and its params become options of the CLI itself
- params:
- name: --verbose
type: bool
default: false
short: -v
help: Print what is about to run
invoke_without_command: true
run: |
ctx.obj = {"verbose": verbose}
if ctx.invoked_subcommand is None:
print("no command given, try --help")
elif verbose:
print(f"running {ctx.invoked_subcommand}")
# runs before every command of the `db` group
- group: db
run: print("connecting to db")
commands:
hello: print("hello")
db.migrate: print("migrating")
db.status: print("up to date")
tests:
- hello: assert result.output == "hello\n"
- --verbose hello: assert result.output == "running hello\nhello\n"
- -v db migrate: assert result.output == "running db\nconnecting to db\nmigrating\n"
- db status: assert result.output == "connecting to db\nup to date\n"
- "": assert result.output == "no command given, try --help\n"
- --version: assert result.output == "callbacks, 0.1.0\n"
clickhello¶
The Click backend (use_click: true) with a command alias. Needs the rich extra.
examples/click_hello.yaml
name: clickhello
version: 0.1.0
use_click: true
help: Hello World with Click!
commands:
hello | hl: $ echo "world"
examples:
- command: clickhello hello
description: Print "world".
- clickhello hl
tests:
- hello: assert result.exit_code == 0
- --help: assert "Examples:" in result.output and "$ clickhello hl" in result.output
aiprompt¶
An OpenAI client written by an LLM: explicit params with help text, global_params and nested groups. Needs openai, rich and keyring.
examples/aiprompt.yaml
name: aiprompt
version: "1.0.0"
help: "Generated with Claude. Command-line interface for interacting with OpenAI's APIs."
requires:
- "openai>=1.0.0"
- "rich>=13.0.0"
- "keyring>=24.0.0"
imports: |
import openai
import keyring
import json
from rich.console import Console
from rich.markdown import Markdown
from rich.table import Table
from typing import Optional, List
from pathlib import Path
from datetime import datetime
functions: |
def load_api_key():
key = keyring.get_password("aiprompt", "openai_api_key")
if not key:
raise typer.BadParameter("API key not found. Please set it using 'aiprompt config set-key'")
return key
def format_chat_response(response):
console = Console()
content = response.choices[0].message.content
console.print(Markdown(content))
def format_completion_response(response):
console = Console()
content = response.choices[0].text
console.print(content)
global_params:
- name: "--verbose"
type: "bool"
help: "Enable verbose output"
default: False
commands:
"config.set-key":
help: "Set OpenAI API key"
params:
- name: "api_key"
type: "str"
help: "Your OpenAI API key"
required: true
run: |
keyring.set_password("aiprompt", "openai_api_key", api_key)
typer.echo("API key saved successfully!")
chat:
help: "Start a chat completion with GPT models"
params:
- name: "prompt"
type: "str"
help: "The prompt to send to the model"
required: true
- name: "--model"
type: "str"
help: "GPT model to use"
default: "gpt-3.5-turbo"
- name: "--temperature"
type: "float"
help: "Sampling temperature (0-2)"
default: 0.7
- name: "--max-tokens"
type: "int"
help: "Maximum tokens in response"
default: 1000
run: |
client = openai.OpenAI(api_key=load_api_key())
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
max_tokens=max_tokens
)
format_chat_response(response)
complete:
help: "Generate text completions"
params:
- name: "prompt"
type: "str"
help: "The prompt to complete"
required: true
- name: "--model"
type: "str"
help: "Model to use"
default: "gpt-3.5-turbo-instruct"
- name: "--temperature"
type: "float"
help: "Sampling temperature (0-2)"
default: 0.7
- name: "--max-tokens"
type: "int"
help: "Maximum tokens in response"
default: 500
run: |
client = openai.OpenAI(api_key=load_api_key())
response = client.completions.create(
model=model,
prompt=prompt,
temperature=temperature,
max_tokens=max_tokens
)
format_completion_response(response)
"chat.stream":
help: "Start a streaming chat completion"
params:
- name: "prompt"
type: "str"
help: "The prompt to send to the model"
required: true
- name: "--model"
type: "str"
help: "GPT model to use"
default: "gpt-3.5-turbo"
- name: "--temperature"
type: "float"
help: "Sampling temperature (0-2)"
default: 0.7
run: |
console = Console()
client = openai.OpenAI(api_key=load_api_key())
stream = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
console.print(chunk.choices[0].delta.content, end="")
console.print()
"chat.file":
help: "Send a file's content as a chat prompt"
params:
- name: "file"
type: "Path"
help: "Path to the input file"
required: true
- name: "--model"
type: "str"
help: "GPT model to use"
default: "gpt-3.5-turbo"
- name: "--system-prompt"
type: "str"
help: "Optional system prompt"
default: ""
run: |
client = openai.OpenAI(api_key=load_api_key())
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
with open(file, 'r') as f:
content = f.read()
messages.append({"role": "user", "content": content})
response = client.chat.completions.create(
model=model,
messages=messages
)
format_chat_response(response)
models:
help: "List available OpenAI models"
params:
- name: "--type"
type: "str"
help: "Filter models by type (chat, completion, embedding, etc.)"
default: ""
run: |
console = Console()
client = openai.OpenAI(api_key=load_api_key())
models = client.models.list()
table = Table(title="Available OpenAI Models")
table.add_column("ID", style="cyan")
table.add_column("Created", style="green")
table.add_column("Owner", style="blue")
for model in sorted(models.data, key=lambda x: x.created):
if type and type.lower() not in model.id.lower():
continue
created_date = datetime.fromtimestamp(model.created).strftime('%Y-%m-%d')
table.add_row(model.id, created_date, model.owned_by)
console.print(table)